<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>개발하는 북극곰</title>
    <link>https://p-bear.tistory.com/</link>
    <description>평범한 개발자의 개발로그입니다.</description>
    <language>ko</language>
    <pubDate>Wed, 15 Apr 2026 07:09:06 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>북극곰은콜라</managingEditor>
    <image>
      <title>개발하는 북극곰</title>
      <url>https://tistory1.daumcdn.net/tistory/3878493/attach/1d8c73555fb24df4bc55a0e9cc8b2dff</url>
      <link>https://p-bear.tistory.com</link>
    </image>
    <item>
      <title>Dead Letter Queue (DLQ) 패턴</title>
      <link>https://p-bear.tistory.com/90</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;101&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgI8dg/btsMglraoNo/PcD8yv7aUmXQM1h6xxSyRk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgI8dg/btsMglraoNo/PcD8yv7aUmXQM1h6xxSyRk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgI8dg/btsMglraoNo/PcD8yv7aUmXQM1h6xxSyRk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgI8dg%2FbtsMglraoNo%2FPcD8yv7aUmXQM1h6xxSyRk%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;318&quot; height=&quot;101&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;101&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;style5&quot; /&gt;
&lt;h2 id=&quot;개요&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;카프카는 Queue를 베이스로 하여 데이터의 발행/소비를 인프라 적으로 격리시킨 시스템으로 볼 수 있다. 이런 격리된 프로세스는 확장성과 가용성을 제공하지만 운영 상 복잡성이 늘어나게 된다.&lt;br /&gt;Queue의 데이터를 임의로 핸들링 하기 힘들다는 점을 집중적으로 분석하고 해결방안 또는 대처방안에 대해 정리하고자 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;Dead-Letter-Queue-(DLQ)-패턴이란?&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;880&quot; data-ke-size=&quot;size26&quot;&gt;Dead Letter Queue (DLQ) 패턴이란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnJr7e/btsMgJZwHnB/I1YgXoWN0gSahqsKYgBVK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnJr7e/btsMgJZwHnB/I1YgXoWN0gSahqsKYgBVK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnJr7e/btsMgJZwHnB/I1YgXoWN0gSahqsKYgBVK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnJr7e%2FbtsMgJZwHnB%2FI1YgXoWN0gSahqsKYgBVK1%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;830&quot; height=&quot;518&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;DLQ는 Consumer가 정상적으로 처리하지 못한 메시지를 따로 저장하는 Kafka의 보조적인 토픽&lt;br /&gt;즉, 오류가 발생한 메시지를 다른 곳에 보관하여 이후 다시 처리할 수 있도록 하는 전략&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;DLQ-핵심-전략&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1025&quot; data-ke-size=&quot;size23&quot;&gt;DLQ 핵심 전략&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- Consumer가 메시지를 처리하는 도중 오류 발생&lt;br /&gt;- 오류가 발생한 메시지를 DLQ 토픽에 저장&lt;br /&gt;- DLQ에 저장된 메시지를 모니터링 및 분석&lt;br /&gt;- DLQ에 저장된 메시지를 별도의 프로세스로 재처리&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;DLQ-패턴의-의미&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1157&quot; data-ke-size=&quot;size23&quot;&gt;DLQ 패턴의 의미&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;DLQ 패턴은 처리하지 못한 데이터를 적극적으로 핸들링하기 위한 방법입니다.&lt;br /&gt;따라서 DLQ 모니터링과 분석이 필수적이라 여겨집니다.&lt;br /&gt;궁극적으로 모니터링 도구와 연동으로 개발자가 에러의 원인을 빠르게 파악하고 재처리하여 전체 시스템의 신뢰도를 높일 수 있는 방법이란 의미가 있습니다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;장점&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1332&quot; data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;h4 id=&quot;1.-데이터-유실-방지-&amp;amp;-보존-가능-+-메시지-재처리/복구&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1336&quot; data-ke-size=&quot;size20&quot;&gt;1. 데이터 유실 방지 &amp;amp; 보존 가능 + 메시지 재처리/복구&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka에서 Consumer가 메시지를 정상적으로 처리하지 못하면 기본적으로 재시도 없이 메시지가 손실될 가능성이 있다 DLQ를 사용하면 실패한 메시지를 안전하게 저장하고 나중에 재처리 가능, 데이터 유실을 방지할 수 있다&lt;br /&gt;DLQ에 저장된 메시지를 특정 시점 이후에 다시 정상 토픽으로 재전송하여 재처리 가능재처리 로직을 통해 일괄적으로 복구하거나 특정 메시지만 선택적으로 복구 가능&lt;/blockquote&gt;
&lt;h4 id=&quot;2.-모니터링-이점-및-장애-발생-시-신속한-대응-가능&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1592&quot; data-ke-size=&quot;size20&quot;&gt;2. 모니터링 이점 및 장애 발생 시 신속한 대응 가능&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;실시간으로 Kafka Consumer가 처리하지 못한 메시지를 DLQ에 쌓이도록 설정하면 장애 감지 및 대응이 쉬워 짐 DLQ에 특정 유형의 메시지가 급격히 증가하면 Consumer의 코드 오류 또는 다운타임 발생 가능성을 빠르게 인지할 수 있다.&lt;br /&gt;이는 에러로그 사후분석에 비해 효율적이며, 상황에 따른 트리거링도 가능하다.&lt;/blockquote&gt;
&lt;h4 id=&quot;3.-비즈니스-로직에-따른-맞춤형-처리-가능&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1809&quot; data-ke-size=&quot;size20&quot;&gt;3. 비즈니스 로직에 따른 맞춤형 처리 가능&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;특정 비즈니스 요구사항에 따라 DLQ에 쌓인 메시지를 다양한 방식으로 처리 가능&lt;br /&gt;실패한 메시지를 즉시 재처리할지, 특정 시간 이후에 다시 시도할지, 별도 데이터베이스에 저장할지 등을 설정 가능&lt;/blockquote&gt;
&lt;h3 id=&quot;단점&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1946&quot; data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;h4 id=&quot;1.-시스템-복잡도-증가&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;1950&quot; data-ke-size=&quot;size20&quot;&gt;1. 시스템 복잡도 증가&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;DLQ를 도입하면 기존 Kafka Consumer와 함께 추가적인 DLQ Topic, DLQ Consumer, 재처리 로직 등이 필요 이러한 복잡성 증가로 인해 운영 부담이 커지고 유지보수가 어려워질 수 있음 또한 DLQ로 전송하는 로직도 구현해야 하며, 모니터링 시스템까지 구축해야 높은 효율을 보인다.&lt;br /&gt;이러한 복잡도는 운영 요소의 증가로 이어진다.&lt;/blockquote&gt;
&lt;h4 id=&quot;2.-DLQ-메시지가-쌓이면-운영-부담-증가&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2165&quot; data-ke-size=&quot;size20&quot;&gt;2. DLQ 메시지가 쌓이면 운영 부담 증가&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;DLQ 메시지를 재처리하지 않으면 계속해서 쌓이고, 결국 스토리지 비용 증가한다.&lt;br /&gt;또한 너무 쌓인 데이터는 분석 및 핸들링에 어려움을 가져와 DLQ 패턴의 도입 이점이 희석될 수 있다.&lt;/blockquote&gt;
&lt;h4 id=&quot;3.-메시지-재처리-시-데이터-순서-유지-어려움&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2298&quot; data-ke-size=&quot;size20&quot;&gt;3. 메시지 재처리 시 데이터 순서 유지 어려움&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka의 메시지는 Partition 단위로 순서를 보장하지만, DLQ에 저장된 메시지를 다시 처리할 때는 순서가 깨질 가능성이 높음&lt;br /&gt;이를 해결하려면 일부 메시지를 특정 시점에만 재처리하도록 조정하거나 다른 timestamp를 기준으로 재정렬하는 로직이 필요하다.&lt;br /&gt;하지만 운영이 복잡해짐&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;DLQ-도입&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2491&quot; data-ke-size=&quot;size23&quot;&gt;DLQ 도입 고려 요소&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50.3489%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;도입이 적절한 경우&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.5348%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;도입이 불필요한 경우&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50.3489%;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;- &lt;/span&gt;&lt;/span&gt;데이터 유실을 절대 허용할 수 없는 시스템&lt;span&gt; (&lt;/span&gt;예&lt;span&gt;: &lt;/span&gt;금융 거래&lt;span&gt;, &lt;/span&gt;주문 시스템&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;- Consumer&lt;/span&gt;가 일시적으로 장애가 발생할 가능성이 있는 경우&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;- 메시지 처리 오류가 발생할 수 있는 복잡한 비즈니스 로직을 운영하는 경우&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;- Kafka Consumer&lt;/span&gt;의 안정성을 높이고&lt;span&gt;, &lt;/span&gt;운영 효율성을 개선하고 싶은 경우&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.5348%;&quot;&gt;&lt;span&gt;- 메시지 처리 속도가 가장 중요한 실시간 시스템&lt;span&gt; (DLQ&lt;/span&gt;에서 재처리하는 시간이 부담이 될 수 있음&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;- Consumer &lt;/span&gt;코드가 안정적이고&lt;span&gt;, &lt;/span&gt;장애가 거의 발생하지 않는 경우&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;- Kafka&lt;/span&gt;의 기본적인 재시도 및 에러 핸들링 기능만으로 충분한 경우&lt;span&gt;&lt;/span&gt;&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;style5&quot; /&gt;
&lt;h2 id=&quot;False-Data-유입-상황&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;196&quot; data-ke-size=&quot;size26&quot;&gt;False Data 유입 상황&lt;/h2&gt;
&lt;h3 id=&quot;무중단-Producer/consumer-배포-시-record-호환성-문제&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;214&quot; data-ke-size=&quot;size23&quot;&gt;무중단 Producer/consumer 배포 시 record 호환성 문제&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;769&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn70KA/btsMhum50MS/bNjVQRcAoot5BV2vYXTLFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn70KA/btsMhum50MS/bNjVQRcAoot5BV2vYXTLFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn70KA/btsMhum50MS/bNjVQRcAoot5BV2vYXTLFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn70KA%2FbtsMhum50MS%2FbNjVQRcAoot5BV2vYXTLFK%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;954&quot; height=&quot;769&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;769&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;scale-out 구조에서 흔하게 발생하는 문제로 무중단 배포 시 일부 데이터가 처리되지 못해 유실되는 상황이다.따라서 무중단 배포 시 다음과 같은 상황 별 대처 방안이 필요하다.&lt;br /&gt;- 데이터 누락이 허용되는 경우&lt;br /&gt;- 데이터 누락이 허용되지 않는 경우&lt;/blockquote&gt;
&lt;h3 id=&quot;Human-Error가-포함된-배포&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;405&quot; data-ke-size=&quot;size23&quot;&gt;Human Error가 포함된 배포&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Producer나 Consumer의 record 발행/소비의 유지보수 과정에서 예상하지 못한 error가 발생할 수 있다.&lt;br /&gt;리뷰나 스테이징 운영을 통해 최대한 줄일 수 는 있지만 원천 차단은 불가할 것이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 id=&quot;해결-방안&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;546&quot; data-ke-size=&quot;size26&quot;&gt;해결 방안&lt;/h2&gt;
&lt;h3 id=&quot;1.-데이터-누락-허용의-경우&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;553&quot; data-ke-size=&quot;size23&quot;&gt;1. 데이터 누락 허용의 경우&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터 누락이 허용되는 경우 consumer의 예외 및 skip처리를 구현해 놓으면 된다.&lt;br /&gt;이후 효율성을 위해 producer &amp;rarr; consumer 순으로 배포하면 된다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;2.-데이터-누락-허용-불가의-경우&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;669&quot; data-ke-size=&quot;size23&quot;&gt;2. 데이터 누락 허용 불가의 경우&lt;/h3&gt;
&lt;h4 id=&quot;2-1)-Topic-분리-(version-or-temp-토픽)&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;690&quot; data-ke-size=&quot;size20&quot;&gt;2-1) Topic 분리 (version or temp 토픽)&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;790&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NqMsV/btsMgt3LL2P/XWCDxX2R81KtUO9TXAX1g0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NqMsV/btsMgt3LL2P/XWCDxX2R81KtUO9TXAX1g0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NqMsV/btsMgt3LL2P/XWCDxX2R81KtUO9TXAX1g0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNqMsV%2FbtsMgt3LL2P%2FXWCDxX2R81KtUO9TXAX1g0%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;1045&quot; height=&quot;790&quot; data-origin-width=&quot;1045&quot; data-origin-height=&quot;790&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;호환 불가능한 record를 임시로 수용할 temp 토픽 또는 v2 토픽을 통해 무중단 배포하는 방식이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 id=&quot;2-2)-DLQ-패턴-적용&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;790&quot; data-ke-size=&quot;size20&quot;&gt;2-2) DLQ 패턴 적용&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;445&quot; data-origin-height=&quot;765&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9COrr/btsMgJSMge1/vGoxjN79qHEmitfutX8uXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9COrr/btsMgJSMge1/vGoxjN79qHEmitfutX8uXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9COrr/btsMgJSMge1/vGoxjN79qHEmitfutX8uXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9COrr%2FbtsMgJSMge1%2FvGoxjN79qHEmitfutX8uXK%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;445&quot; height=&quot;765&quot; data-origin-width=&quot;445&quot; data-origin-height=&quot;765&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;DLQ 패턴은 시스템 신뢰도를 높일 수 있는 방법으로 패턴 특성 상 무중단 배포에서 발생하는 이슈를 커버해 줄 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;Conclusion&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;2861&quot; data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;- Kafka 무 중단 배포, 휴먼 에러 등 여러 이유로 처리되지 못하는 record가 발생할 수 있다.&lt;br /&gt;- Topic 분리를 통해 처리되지 못한 record를 발생시키지 않을 수 있다.&lt;br /&gt;- DLQ 패턴을 도입하여 처리되지 못한 record를 전반적으로 관리할 수 있다.&lt;br /&gt;- DLQ는 Kafka 운영 환경 시스템의 신뢰도를 높일 수 있는 좋은 방법 중 하나이다.&lt;br /&gt;- DLQ는 추가적인 개발 및 운영 비용이 들어간다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;REFERENCE&quot; style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;3118&quot; data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;3129&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://rudaks.tistory.com/entry/신뢰성-오류-복구-패턴-데드-레터-큐Dead-Letter-Queue-패턴&quot;&gt;https://rudaks.tistory.com/entry/신뢰성-오류-복구-패턴-데드-레터-큐Dead-Letter-Queue-패턴&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;3204&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devidea.tistory.com/111&quot;&gt;https://devidea.tistory.com/111&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #292a2e; text-align: start;&quot; data-renderer-start-pos=&quot;3237&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sjoongh.tistory.com/entry/DLQ-적용기&quot;&gt;https://sjoongh.tistory.com/entry/DLQ-적용기&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>dead letter queue</category>
      <category>dlq</category>
      <category>Kafka</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/90</guid>
      <comments>https://p-bear.tistory.com/90#entry90comment</comments>
      <pubDate>Thu, 13 Feb 2025 17:31:48 +0900</pubDate>
    </item>
    <item>
      <title>VoltDB란</title>
      <link>https://p-bear.tistory.com/89</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;201&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NL1oO/btsHNujJbRW/8QYiQTexv2iQGBu7Fwm370/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NL1oO/btsHNujJbRW/8QYiQTexv2iQGBu7Fwm370/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NL1oO/btsHNujJbRW/8QYiQTexv2iQGBu7Fwm370/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNL1oO%2FbtsHNujJbRW%2F8QYiQTexv2iQGBu7Fwm370%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;439&quot; height=&quot;201&quot; data-origin-width=&quot;439&quot; data-origin-height=&quot;201&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VoltDB 조사 및 사용성 검토&lt;br /&gt;어떤 목적 및 사상을 가지고 있는지, 해결한 문제, 한계점 등을 파악&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VoltDB란?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VoltDB는 기존 Database 대비 throughtput을 향상 시키기 위해 개발된 ACID를 준수하는 transactional database이다.&lt;br /&gt;기존 DB는 범용성을 위해서 최적화 범위를 제한하였는데, voltDB는 이를 개선하고자 했다.&lt;br /&gt;디자인 아이디어&lt;br /&gt;&amp;nbsp;- ACID 및 SQL 스펙을 만족하여 러닝커브를 줄임&lt;br /&gt;&amp;nbsp;- in-memory 방식을 통한 Disk-IO를 줄임&lt;br /&gt;&amp;nbsp;- data access를 serializing하여 locking, latching, transaction log 관리에 대한 오버헤드를 최소화 &lt;br /&gt;&amp;nbsp;- clustering, replication을 통한 고가용성 및 안정성 확보&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VoltDB 목적&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Focus&amp;nbsp;on&amp;nbsp;handling&amp;nbsp;fast&amp;nbsp;data&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VoltDB는 금융, SNS, IoT등 빠른 데이터 Store에 대한 문제를 해결합니다.&lt;br /&gt;이는 scalability, reliability, high availability, high throughput을 지원함을 의미합니다.&lt;/blockquote&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;SQL Support&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SQL을 지원하기 때문에 상대적으로 낮은 러닝커브를 갖고 있습니다.&lt;/blockquote&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;Inefficient&amp;nbsp;for&amp;nbsp;Large&amp;nbsp;&amp;amp;&amp;nbsp;complicated&amp;nbsp;Data&amp;nbsp;Structure&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;VoltDB는 히스토리성 데이터 같이 큰 데이터를 handling하기에는 부적합합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;또한 여러 테이블을 join하는 복잡한 데이터의 handling을 최적으로 지원하고 있지 않습니다.&lt;/span&gt;&lt;/blockquote&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;Architecture&amp;nbsp;Complicated&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;최적의 성능을 위해서는 설계의 복잡성이 증가합니다.&lt;br /&gt;Stored Procedure 기반, partition 개념 등 설계 및 운용 시 고려해야 할 사항이 늘어납니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VoltDB Idea&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.&amp;nbsp;In-memory&amp;nbsp;processing&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VoltDB는 DiskIO를 없에고, memory로 data store를 구성하여, performance를 향상시켰다.&lt;/blockquote&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;2. Stored&amp;nbsp;Procedure&amp;nbsp;Interface&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기존 Data Query는 네트워크 패킷의 overhead 및 Query 해석, 목적을 위한 다중 쿼리 전송 등 작업이 performance를 낮춥니다.&lt;br /&gt;VoltDB는 Stored Procedure만을 호출하여 위 overhead를 one round trip으로 수행할 수 있도록 합니다.&lt;br /&gt;또한 VoltDB는 하나의 procedure는 하나의 transaction을 의미하며, success 또는 rollback을 한 동작에서 가능하도록 합니다.&lt;/blockquote&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;3.&amp;nbsp;Serialized&amp;nbsp;Processing&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6GC7m/btsHLsgQsOk/pXfGNNMtW4u8DFAcXTw6g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6GC7m/btsHLsgQsOk/pXfGNNMtW4u8DFAcXTw6g1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6GC7m/btsHLsgQsOk/pXfGNNMtW4u8DFAcXTw6g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6GC7m%2FbtsHLsgQsOk%2FpXfGNNMtW4u8DFAcXTw6g1%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;830&quot; height=&quot;674&quot; data-origin-width=&quot;830&quot; data-origin-height=&quot;674&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VoltDB는 1 DataAccess == 1 transaction 이다. 이 요청들은 serial하게 execution engine에서 실행된다.&lt;br /&gt;Execution Engine은 VoltDB의 Partition 당 하나씩 생성되는데 이는 CPU cores와 cluster에 따라서 결정된다.&lt;br /&gt;결론적으로 voltDB는 모든 요청을 serial하게 처리하여, lock 및 latch 없이 transactional한 처리를 지원한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VoltDB 동작원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.&amp;nbsp;Partitioning&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;427&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T71Ip/btsHMxuMssp/rJgPljotw2TqAuGWrSCCN0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T71Ip/btsHMxuMssp/rJgPljotw2TqAuGWrSCCN0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T71Ip/btsHMxuMssp/rJgPljotw2TqAuGWrSCCN0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT71Ip%2FbtsHMxuMssp%2FrJgPljotw2TqAuGWrSCCN0%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;654&quot; height=&quot;427&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;427&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VoltDB는 모든 Stored Procedure를 Analyze하고 precompile하여 access data logic을 정리한다.&lt;br /&gt;이 과정을 통해, voltDB는 실행환경(CPU Core등)을 고려하여 execute engine을 포함한 partition을 initialize한다.&lt;br /&gt;각 partition은 기본적으로 유니크한 data의 집합을 가지고 있다.&lt;/blockquote&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;2.&amp;nbsp;Serialized&amp;nbsp;Processing&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d9ML7o/btsHKQCnjr0/XnWKmBF9qAPickKZo9zHBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d9ML7o/btsHKQCnjr0/XnWKmBF9qAPickKZo9zHBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d9ML7o/btsHKQCnjr0/XnWKmBF9qAPickKZo9zHBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd9ML7o%2FbtsHKQCnjr0%2FXnWKmBF9qAPickKZo9zHBK%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;667&quot; height=&quot;406&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VoltDB는 모든 쿼리가 Stored Procedure로 동작한다. 또한 Stored Procedure는 하나의 transaction을 의미한다.&lt;br /&gt;이는 하나의 쿼리 당 하나의 transaction을 의미한다.&lt;br /&gt;모든 요청을 serial하게 처리하면 transaction을 고려한 lock 및 latch를 사용할 필요가 없어진다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Stored procedure는 Analyze 단계에서 Single-partitioned 또는 Multiple-partitioned 2종류로 나누어진다.&lt;br /&gt;Single-partitioned procedure는 다른 partition과 상관없는 procedure로 execute engine에서 parallel하게 동작한다.&lt;br /&gt;Multiple-partitioned procedure는 연관 있는 특정 partition의 execute engine에서 다른 partition의 결과를 collect하는 방식으로 동작한다.&lt;br /&gt;당연하게도 Multiple-partitioned procedure는 성능이 떨어진다.&lt;/blockquote&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;3.&amp;nbsp;Replication&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccP2Ke/btsHLygSTyV/AMQa24Fw55qtCuVPDR9ghK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccP2Ke/btsHLygSTyV/AMQa24Fw55qtCuVPDR9ghK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccP2Ke/btsHLygSTyV/AMQa24Fw55qtCuVPDR9ghK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccP2Ke%2FbtsHLygSTyV%2FAMQa24Fw55qtCuVPDR9ghK%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;713&quot; height=&quot;446&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VoltDB는 Single-partitioned procedure가 최적의 성능을 제공한다.&lt;br /&gt;이에 join을 위한 largely read-only table을 각 partition에 replication하는 기능을 제공한다.&lt;br /&gt;위 그림의 D테이블은 read가 압도적으로 많으며, A,B,C간의 관계를 나타내는 테이블이다.&lt;br /&gt;D테이블이 없는 partition은 항상 Multiple-partitioned procedure로 동작하면 불합리하기에, D 테이블을 파티션에 복제하여, 모두 Single-partitioned procedure처럼 동작할 수 있도록 할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VoltDB&amp;nbsp;vs&amp;nbsp;RDBMS(MariaDB)&amp;nbsp;vs&amp;nbsp;MongoDB&amp;nbsp;vs&amp;nbsp;Redis&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/banAGd/btsHLWIxe7X/xJimAFmadeauwmNaCJ4pv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/banAGd/btsHLWIxe7X/xJimAFmadeauwmNaCJ4pv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/banAGd/btsHLWIxe7X/xJimAFmadeauwmNaCJ4pv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbanAGd%2FbtsHLWIxe7X%2FxJimAFmadeauwmNaCJ4pv1%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;1048&quot; height=&quot;550&quot; data-origin-width=&quot;1048&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 783px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 17px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Name&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;MariaDB&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 17px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;MongoDB&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 17px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Redis&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 17px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;VoltDB&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;Primary database model&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 34px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/RDBMS&quot;&gt;Relational DBMS&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 34px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/Document+Stores&quot;&gt;Document store&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 34px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/Key-value+Stores&quot;&gt;Key-value store&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 34px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/RDBMS&quot;&gt;Relational DBMS&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 102px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 102px;&quot;&gt;&lt;span&gt;&lt;span&gt;Secondary database models&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 102px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/Document+Stores&quot;&gt;Document store&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/Graph+DBMS&quot;&gt;Graph DBMS&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Spatial DBMS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 102px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/Spatial+DBMS&quot;&gt;Spatial DBMS&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/Search+Engines&quot;&gt;Search engine&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Time Series DBMS&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Vector DBMS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 102px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/Document+Stores&quot;&gt;Document store&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Graph DBMS&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/Spatial+DBMS&quot;&gt;Spatial DBMS&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/article/Search+Engines&quot;&gt;Search engine&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Time Series DBMS&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Vector DBMS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 102px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://db-engines.com/en/ranking&quot;&gt;DB-Engines Ranking&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Score: 93.81&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Score: 423.96&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Score: 156.44&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Score: 1.46&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 18px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 18px;&quot;&gt;&lt;span&gt;&lt;span&gt;Initial release&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 18px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;2009&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 18px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;2009&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 18px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;2009&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 18px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;2010&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;Current release&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;11.3.2, February 2024&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;6.0.7, June 2023&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;7.2.4, January 2024&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;11.3, April 2022&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;License&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Open Source&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Open Source&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Open Source&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Open Source&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;Implementation language&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;C and C++&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;C++&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;C&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Java, C++&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;Data scheme&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 17px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;schema-free&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 17px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;schema-free&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 17px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;Typing (predefined dataType)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;partial&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 34px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;Secondary indexes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 51px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 51px;&quot;&gt;&lt;span&gt;&lt;span&gt;SQL&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 51px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 51px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;Read-only SQL queries via the MongoDB Atlas SQL Interface&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 51px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;with RediSQL module&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 51px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 85px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 85px;&quot;&gt;&lt;span&gt;&lt;span&gt;APIs and other access methods&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 85px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;http://ADO.NET&quot;&gt;ADO.NET&lt;/a&gt;&lt;br /&gt;JDBC&lt;br /&gt;ODBC&lt;br /&gt;Proprietary native API&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 85px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;GraphQL&lt;br /&gt;HTTP REST&lt;br /&gt;Prisma&lt;br /&gt;proprietary protocol using JSON&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 85px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;proprietary protocol&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 85px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Java API&lt;br /&gt;JDBC&lt;br /&gt;RESTful HTTP/JSON API&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 51px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 51px;&quot;&gt;&lt;span&gt;&lt;span&gt;Triggers&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 51px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 51px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 51px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;publish/subscribe channels provide some trigger functionality; &lt;a href=&quot;https://oss.redis.com/redisgears/&quot;&gt;RedisGears&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 51px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;no&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 51px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 51px;&quot;&gt;&lt;span&gt;&lt;span&gt;Partitioning methods&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 51px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;several options for horizontal partitioning and Sharding&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 51px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Sharding&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 51px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Sharding&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 51px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Sharding&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 85px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 85px;&quot;&gt;&lt;span&gt;&lt;span&gt;Replication methods&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 85px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Multi-source replication&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Source-replica replication&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 85px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Multi-Source deployments with MongoDB Atlas Global Clusters&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Source-replica replication&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 85px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Multi-source replication&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Source-replica replication&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 85px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;Multi-source replication&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Source-replica replication&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;Foreign keys&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 17px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;no&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 17px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;no&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 17px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;no&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 51px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 51px;&quot;&gt;&lt;span&gt;&lt;span&gt;Transaction concepts&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 51px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;ACID&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 51px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;Multi-document ACID Transactions with snapshot isolation&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 51px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;Atomic execution of command blocks and scripts and optimistic locking&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 51px;&quot; data-highlight-colour=&quot;#e3fcef&quot;&gt;&lt;span&gt;&lt;span&gt;ACID&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;Concurrency&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;Durability&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 24.5349%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;In-memory capabilities&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4883%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.7676%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 19.651%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&lt;span&gt;&lt;span&gt;yes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 17.4419%; height: 17px;&quot; data-highlight-colour=&quot;#f8f8f8&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;VoltDB는 일부 세부적인 항목을 제외하면, 전통적인 RDBMS와 상당히 비슷한 기능을 제공한다.&lt;br /&gt;이는 RDB의 핵심 컨셉인 ACID transaction를 지원하기 때문이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VoltDB&amp;nbsp;vs&amp;nbsp;MongoDB&amp;nbsp;(performance)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;분산 환경에서 ACID transaction를 지원하는 두 DB의 성능을 비교 (MongoDB는 개념적인 ACID를 지원한다)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Test Env&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CPUs: 2&lt;br /&gt;RAM: 4GB&lt;br /&gt;HDD: 8GB&lt;br /&gt;OS: Windows Server 2016&lt;/blockquote&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;Test Data&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/30t9z/btsHLdjKFgR/Pt5K7N3KbLgpik6DhDk3fK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/30t9z/btsHLdjKFgR/Pt5K7N3KbLgpik6DhDk3fK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/30t9z/btsHLdjKFgR/Pt5K7N3KbLgpik6DhDk3fK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F30t9z%2FbtsHLdjKFgR%2FPt5K7N3KbLgpik6DhDk3fK%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;869&quot; height=&quot;501&quot; data-origin-width=&quot;869&quot; data-origin-height=&quot;501&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;RDB 기준 Data 구조&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c400ho/btsHLrCjwQI/nWE3mAoVrlvmjsKHhejDaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c400ho/btsHLrCjwQI/nWE3mAoVrlvmjsKHhejDaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c400ho/btsHLrCjwQI/nWE3mAoVrlvmjsKHhejDaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc400ho%2FbtsHLrCjwQI%2FnWE3mAoVrlvmjsKHhejDaK%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;1166&quot; height=&quot;281&quot; data-origin-width=&quot;1166&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Connection 개수를 변경하면서 테스트를 진행한다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;364&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E86Vh/btsHM5R7bbq/8hR6CjvGUzIF8v6PApv5Pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E86Vh/btsHM5R7bbq/8hR6CjvGUzIF8v6PApv5Pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E86Vh/btsHM5R7bbq/8hR6CjvGUzIF8v6PApv5Pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE86Vh%2FbtsHM5R7bbq%2F8hR6CjvGUzIF8v6PApv5Pk%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;879&quot; height=&quot;364&quot; data-origin-width=&quot;879&quot; data-origin-height=&quot;364&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;총 5 종류의 데이터 셋을 준비했으며, 개념적으로 같은 데이터를 각 DB에 맞게 구성했다.&lt;br /&gt;Size로 보았을 때 MongoDB의 data overhead가 더 큰 것을 볼 수 있다.&lt;/blockquote&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;CPU Usage&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;756&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csQZmQ/btsHMBqoWak/SwyRUNtLW2lWH7gDWkuh5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csQZmQ/btsHMBqoWak/SwyRUNtLW2lWH7gDWkuh5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csQZmQ/btsHMBqoWak/SwyRUNtLW2lWH7gDWkuh5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsQZmQ%2FbtsHMBqoWak%2FSwyRUNtLW2lWH7gDWkuh5k%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;611&quot; height=&quot;756&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;756&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;전체적으로 CPU 사용량은 VoltDB가 MongoDB보다 성능 상 이점이 있는 것을 볼 수 있다.&lt;br /&gt;이는 VoltDB의 Stored Procedure의 execute가 보다 효율적인 것이라 예상해 볼 수 있다.&lt;br /&gt;하지만, connection만 늘어나는 상황에서는 voltDB는 각 request를 serial하게 처리하기 때문에 상대적으로 좋지 못한 performance를 보인다.&lt;/blockquote&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;Insert Test&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1083&quot; data-origin-height=&quot;543&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dWNloq/btsHNzrEX0e/fgPKn9ffr05M1B6LIYFHBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dWNloq/btsHNzrEX0e/fgPKn9ffr05M1B6LIYFHBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWNloq/btsHNzrEX0e/fgPKn9ffr05M1B6LIYFHBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdWNloq%2FbtsHNzrEX0e%2FfgPKn9ffr05M1B6LIYFHBK%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;1083&quot; height=&quot;543&quot; data-origin-width=&quot;1083&quot; data-origin-height=&quot;543&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Insert는 MonogoDB가 안정적인 성능을 보장하는 것을 볼 수 있다.&lt;br /&gt;이는 insert 되는 Data의 partition 선정 등 작업에서 버든이 있을 것으로 추정된다.&lt;/blockquote&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;Update / Delete Test&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;591&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu0Rxa/btsHLNx5670/BqZEg14KjFBpnWdpWf6Xt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu0Rxa/btsHLNx5670/BqZEg14KjFBpnWdpWf6Xt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu0Rxa/btsHLNx5670/BqZEg14KjFBpnWdpWf6Xt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu0Rxa%2FbtsHLNx5670%2FBqZEg14KjFBpnWdpWf6Xt1%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;1074&quot; height=&quot;591&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;591&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kPZky/btsHMudLJbs/66yivv9qCfEN1tAbKI22EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kPZky/btsHMudLJbs/66yivv9qCfEN1tAbKI22EK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kPZky/btsHMudLJbs/66yivv9qCfEN1tAbKI22EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkPZky%2FbtsHMudLJbs%2F66yivv9qCfEN1tAbKI22EK%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;816&quot; height=&quot;371&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Update는 전반적으로 voltDB가 안정적으로 좋은 퍼포먼스를 보여준다.&lt;br /&gt;이는 update를 위해서 전체 컬랙션을 full-scan하는 경우가 있기 때문에 MongoDB는 transaction처리를 위한 비효율이 발생하는 것으로 예상된다.&lt;br /&gt;물론 index를 통해 성능 보정이 가능할 것이다.&lt;br /&gt;반면 VoltDB는 RDB의 특성으로 인하여 transaction 처리에 이점을 가지며, 성능의 차이를 보인다.&lt;/blockquote&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;VoltDB&amp;nbsp;vs&amp;nbsp;MongoDB&amp;nbsp;결론&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본적인 CPU 사용량은 VoltDB가 더 효율적이며, transaction 처리 성능에 큰 이점을 가진다.&lt;br /&gt;하지만 대량 / 고부하 insert는 MongoDB가 더 좋은 결과를 보였다.&lt;br /&gt;이에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;VoltDB는 Schema가 정해져 있으며, update가 빈번하게 일어나며, transaction처리가 필요한 요구사항에서 최적의 성능을 낼 수 있음을 확인할 수 있다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전통적인&amp;nbsp;RDBMS를&amp;nbsp;대체하는&amp;nbsp;상황&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;update가 빈번하게 일어나며, insert가 상대적으로 적은 상황&lt;br /&gt;schema가 과도하게 복잡하지 않으며, single partition execute로 대부분의 쿼리가 가능한 상황&lt;br /&gt;scale-out이 필요할 정도의 부하가 일어나는 상황&lt;/blockquote&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;NoSQL을&amp;nbsp;대신할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;상황&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ACID Transaction이 필요한 상황&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;VoltDB는 ACID를 지원하는 Database이다.&lt;br /&gt;전통적인 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;RDBMS의 빈번한 locking, latching의 비효율성을 개선하며, scale-out을 통한 수평적 확장을 효율적으로 보장&lt;/b&gt;&lt;/span&gt;한다.&lt;br /&gt;기본적으로 update / delete에 큰 이점을 가지고 있으며, insert / select 또한 전통 RDBMS보다 좋은 성능을 낼 수 있다.&lt;br /&gt;하지만 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;tradeoff 사항으로 복잡한 구조의 데이터를 쿼리하거나 큰 record를 쿼리하는 성능에 불이익&lt;/b&gt;&lt;/span&gt;을 가진다.&lt;br /&gt;또한 DB의 특성을 고려하여 설계가 필요하며, 이는 stored procedure 기반 아키텍처, partition을 고려한 data 구조 등을 말한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;결론적으로 RDB를 대체하거나 NoSQL을 대체하기에는 &lt;span style=&quot;color: #ee2323;&quot;&gt;범용성이 부족&lt;/span&gt;하며&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;application에서 상당한 책임을 가져가는 요즘 추세에서 사용할 가치가 있는지는 의문이 있다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6969&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.voltdb.com/UsingVoltDB/&quot;&gt;https://docs.voltdb.com/UsingVoltDB/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;7007&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.odbms.org/wp-content/uploads/2013/11/VoltDBTechnicalOverview.pdf&quot;&gt;https://www.odbms.org/wp-content/uploads/2013/11/VoltDBTechnicalOverview.pdf&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;7085&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.researchgate.net/publication/371731207_A_Performance_Comparison_between_Modern_Relational_Database_VoltDB_and_Graph_NoSQL_Database_Neo4j&quot;&gt;https://www.researchgate.net/publication/371731207_A_Performance_Comparison_between_Modern_Relational_Database_VoltDB_and_Graph_NoSQL_Database_Neo4j&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;7235&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://db-engines.com/en/system/MariaDB%3BMongoDB%3BRedis%3BVoltDB&quot;&gt;https://db-engines.com/en/system/MariaDB%3BMongoDB%3BRedis%3BVoltDB&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>voltdb</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/89</guid>
      <comments>https://p-bear.tistory.com/89#entry89comment</comments>
      <pubDate>Mon, 3 Jun 2024 21:37:27 +0900</pubDate>
    </item>
    <item>
      <title>ksqlDB 란</title>
      <link>https://p-bear.tistory.com/88</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mkYr4/btsHB92Q6qU/3M9BAK76tenI6BtyynJCKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mkYr4/btsHB92Q6qU/3M9BAK76tenI6BtyynJCKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mkYr4/btsHB92Q6qU/3M9BAK76tenI6BtyynJCKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmkYr4%2FbtsHB92Q6qU%2F3M9BAK76tenI6BtyynJCKk%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;875&quot; height=&quot;350&quot; data-origin-width=&quot;875&quot; data-origin-height=&quot;350&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ksqlDB란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uqwuH/btsHD8nJ5nV/rVoXQKzrZeQW5WmL5VGDiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uqwuH/btsHD8nJ5nV/rVoXQKzrZeQW5WmL5VGDiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uqwuH/btsHD8nJ5nV/rVoXQKzrZeQW5WmL5VGDiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuqwuH%2FbtsHD8nJ5nV%2FrVoXQKzrZeQW5WmL5VGDiK%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;681&quot; height=&quot;290&quot; data-origin-width=&quot;681&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2017년 Confluent에서 오픈소스로 공개한 이벤트 스트리밍 데이터베이스이다.&lt;br /&gt;Kafka Streams + Kafka Connect로 구성되며, SQL을 인터페이스로 채용했다.&lt;br /&gt;이는 스트림처리 및 데이터 통합을 단순화시키는 효과를 가진다.&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;대화형 작업환경을 제공&lt;br /&gt;SQL로 토폴로지를 표현하여 유지보수성 향상&lt;br /&gt;SQL 인터페이스로 인한 &amp;ldquo;상대적&amp;rdquo;으로 낮은 진입 장벽&lt;br /&gt;connect와 streams를 한번에 관리하여 아키텍처를 단순화&lt;br /&gt;Streams를 활용한 기능적 확장성을 제공&lt;br /&gt;간편한 설치 및 클라우드 지원&lt;br /&gt;데이터 탐색에 최적화&lt;/blockquote&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;License&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ksqlDB is licensed under the Confluent Community License.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;Confluent Community License: &lt;/span&gt;&lt;a href=&quot;https://www.confluent.io/ko-kr/confluent-community-license/&quot;&gt;https://www.confluent.io/ko-kr/confluent-community-license/&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ksqlDB는 Confluent Community이며, 요약하자면 다음과 같다&lt;br /&gt;1. 무단 변경 금지&lt;br /&gt;2. 라이센스의 변경 금지&lt;br /&gt;3. sublicensing 금지&lt;br /&gt;기본적으로 무료 라이선스에 가깝다고 볼 수 있다.&lt;/blockquote&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;배경 및 Idea&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.&amp;nbsp;Kafka&amp;nbsp;Streams&amp;nbsp;+&amp;nbsp;&amp;nbsp;SQL&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oukjX/btsHB4gdWHj/zCtYvsucxIKSlE3CNcgty1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oukjX/btsHB4gdWHj/zCtYvsucxIKSlE3CNcgty1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oukjX/btsHB4gdWHj/zCtYvsucxIKSlE3CNcgty1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoukjX%2FbtsHB4gdWHj%2FzCtYvsucxIKSlE3CNcgty1%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;318&quot; height=&quot;323&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;최초 ksqlDB는 Kafka Streams DSL의 가능성을 보고 시작되었으며 KSQL로 명명하여 시작된 프로젝트 었다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;핵심 아이디어는 전통적인 SQL 언어의 이점으로 Streams를 통해 data handling을 구성하는 것이다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;이는 Kafka에서 stream 되는 data를 무한 데이터로 볼 수 있다는 배경이 있다.&lt;br /&gt;SQL을 차용한 배경에는 data handling에 최적화된 언어로, 오랜 기간 사용되며 우수성을 증명한 언어이기 때문이라는 이유가 있다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;최초 개발 당시 KSQL은 무한 데이터를 쿼리 하는 것에 초점이 맞추어져 있었다.&lt;br /&gt;이에 SQL 문법에 방언(dialect)을 적용하는 방식으로 해결하고자 했다.&lt;br /&gt;DDL 및 DML에 추가적인 키워드를 정의했으며, 이를 통해 data stream을 구성하고, 데이터를 쿼리 할 수 있도록 했다. &lt;/blockquote&gt;
&lt;pre id=&quot;code_1716783493783&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# DDL EX)
CREATE STREAM XXX (id bigint, date varchar, ...);
DROP STREAM IF EXISTS XXX;

# DML EX)
SELECT id FROM XXX;
INSERT INTO XXX (id, date) VALUES (1, '20240101');&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. ksqlDB로 발전&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;312&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zyQs3/btsHDx9lHOj/J552kFaDQVFEFSvBvouwF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zyQs3/btsHDx9lHOj/J552kFaDQVFEFSvBvouwF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zyQs3/btsHDx9lHOj/J552kFaDQVFEFSvBvouwF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzyQs3%2FbtsHDx9lHOj%2FJ552kFaDQVFEFSvBvouwF0%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;312&quot; height=&quot;319&quot; data-origin-width=&quot;312&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka Streams가 발전하면서, KSQL로 구성할 수 있는 기능이 더 많아지게 되었다.&lt;br /&gt;이에 따라 KSQL에 많은 기능이 추가되었으며 ksqlDB로 명칭을 변경했다.&lt;br /&gt;그중 핵심이 되는 기능으로, Streams의 StateStore 기능이다.&lt;br /&gt;Streams는 메모리 또는 RocksDB로 topic에 대한 Key-Value Store를 구성할 수 있게 되었다.&lt;br /&gt;&lt;br /&gt;ksqlDB는 stateStore를 통해 data의 현재 상태를 쿼리 할 수 있는 기능이 추가되었고 query에 대한 type을 정의했다.&lt;br /&gt;&amp;nbsp;- push query: 무한 데이터를 조회하는 방식으로 업데이트되는 데이터를 지속적으로 푸시해 준다.&lt;br /&gt;&amp;nbsp;- pull query: 현재 상태를 조회하는 방식으로 키 기반으로 데이터를 조회할 수 있다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1716783545200&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# PULL QUERY EX)
CREATE TABLE YYY AS SELECT id, COUNT(id) AS FROM XXX GROUP BY date;
SELECT * FROM YYY;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Kafka Connect 통합&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;324&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DRKdH/btsHBQWQqXp/nXkHVKsJe9daPWxlQkKzRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DRKdH/btsHBQWQqXp/nXkHVKsJe9daPWxlQkKzRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DRKdH/btsHBQWQqXp/nXkHVKsJe9daPWxlQkKzRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDRKdH%2FbtsHBQWQqXp%2FnXkHVKsJe9daPWxlQkKzRK%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;586&quot; height=&quot;324&quot; data-origin-width=&quot;586&quot; data-origin-height=&quot;324&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;많은 비즈니스 요구사항은 data Streaming 뿐 아니라, sink와 source도 필요했다.&lt;br /&gt;ksqlDB는 Kafka Connect와 병행되어 사용되었고, 이에 connect에 대한 통합을 요구하는 피드백이 많아졌다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;ksqlDB의 핵심 사상은 고수준 인터페이스를 통해 사용성의 향상이다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;따라서 Kafka Connect의 통합은 자연스럽게 개발되었다.&lt;br /&gt;kafka connect의 sink, source connector를 구성하는 기능이 추가되었으며, 이를 ksqlDB ETL기능으로 명명되었다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1716783591461&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Connect EX)
CREATE SOURCE CONNECTOR `jdbc-connector` WITH (&quot;connector.class&quot;=&quot;io.confluent.connect.jdbc.JdbcSourceConnector&quot;, ...);&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;h2 data-ke-size=&quot;size26&quot;&gt;ksqlDB vs RDBMS&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유사점&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;SQL Interface&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;ksqlDB&lt;/span&gt;는&lt;span&gt; SQL &lt;/span&gt;문법&lt;span&gt;, &lt;/span&gt;파서&lt;span&gt;, &lt;/span&gt;실행 엔진을 포함하고 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;DDL, DML&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;ksqlDB&lt;/span&gt;의&lt;span&gt; SQL&lt;/span&gt;은 기존&lt;span&gt; SQL&lt;/span&gt;처럼&lt;span&gt; DDL&lt;/span&gt;과&lt;span&gt; DML&lt;/span&gt;로 나누어진다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;DDL&lt;/span&gt;은&lt;span&gt; table &lt;/span&gt;및&lt;span&gt; streams&lt;/span&gt;를 정의하고&lt;span&gt;, DML&lt;/span&gt;은&lt;span&gt; Data handling&lt;/span&gt;을 할 수 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Schema&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;name, type&lt;/span&gt;으로 정의되는&lt;span&gt; schema&lt;/span&gt;를 갖는다&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;내장함수&lt;span&gt; &amp;amp; &lt;/span&gt;연산자&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;ksqlDB &lt;/span&gt;또한 연산자 및 함수를 제공한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;차별점&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Pull, Push Query&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;DDL, DML &lt;/span&gt;외&lt;span&gt; ksqlDB&lt;/span&gt;는 쿼리를&lt;span&gt; PULL QUERY, PUSH QUERY&lt;/span&gt;로 나누어진다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;무한데이터를 쿼리하는 것인지&lt;span&gt;, &lt;/span&gt;상태를 쿼리하는 것인지로 나누어진다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Not Simple Query&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;일반적이&lt;span&gt; RDBMS &lt;/span&gt;처럼 쿼리를 위한 쿼리 기능은 지양한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;쿼리는 하나의&lt;span&gt; stream&lt;/span&gt;이며&lt;span&gt;, &lt;/span&gt;목적성을 가진 쿼리만 작성한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Schema Management&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;ksqlDB&lt;/span&gt;는&lt;span&gt; Kafka &lt;/span&gt;생태계의&lt;span&gt; Schema Registry&lt;/span&gt;를 활용하여&lt;span&gt;, schema&lt;/span&gt;의 진화를 관리할 수 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;High-Availability &amp;amp; Fault-Tolerant&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Kafka&lt;/span&gt;에서 보장하는 고가용성 및 장애 복구가 적용된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Eventually Consistency&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Data Model&lt;/span&gt;은 결과적인 일관성을 베이스로 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;event-driven architecture&lt;/span&gt;를 베이스로 설계되었기 때문&lt;span&gt;&lt;/span&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작 원리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ksqlDB는 Kafka 생태계 메인제품인 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Kafka Streams와 Kafka Connect를 활용&lt;/b&gt;&lt;/span&gt;하여 만들었다.&lt;br /&gt;따라서 동작은 해당 제품의 원리를 따른다.&lt;br /&gt;ksqlDB의 메인 동작은 SQL을 파싱 하여 각 제품의 기능을 사용해 사용자가 원하는 결과를 만드는 것이다.&lt;/blockquote&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.721%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.279%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.721%;&quot;&gt;&lt;span&gt;&lt;span&gt;ksqlDB Server&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.279%;&quot;&gt;&lt;span&gt;&lt;span&gt;SQL Engine, Kafka Streams &lt;/span&gt;등을 포함하며&lt;span&gt;, &lt;/span&gt;사용자의 요청을 수행하는 단위이다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;간단하게&lt;span&gt; ksqlDB Server &lt;/span&gt;하나는&lt;span&gt; Kafka Streams Application &lt;/span&gt;하나로 생각해도 무방하다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;ksql.service.id&lt;/span&gt;로 각 인스턴스는 아이디를 설정할 수 있는데&lt;span&gt;, &lt;/span&gt;같은 아이디끼리 하나로 묶여서 동작하며&lt;span&gt;, &lt;/span&gt;이를 클러스터라고 부른다&lt;span&gt;. ksql id&lt;/span&gt;는&lt;span&gt; consumerId&lt;/span&gt;와 같은 동작을 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;클러스터 내의 각 인스턴스는&lt;span&gt; Streams&lt;/span&gt;처럼 파티션을 나눠 갖고&lt;span&gt;, &lt;/span&gt;실행된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.721%;&quot;&gt;&lt;span&gt;&lt;span&gt;SQL Engine&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.279%;&quot;&gt;&lt;span&gt;&lt;span&gt;SQL parser&lt;/span&gt;를 포함하고 있으며&lt;span&gt;, &lt;/span&gt;주요 기능은&lt;span&gt; SQL&lt;/span&gt;을 분석하고&lt;span&gt;, Streams &lt;/span&gt;토폴로지로 변환하는 것이다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.721%;&quot;&gt;&lt;span&gt;&lt;span&gt;REST Service (Interface)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.279%;&quot;&gt;&lt;span&gt;&lt;span&gt;SQL Engine&lt;/span&gt;의 인터페이스이다&lt;span&gt;. &lt;/span&gt;사용자는&lt;span&gt; REST&lt;/span&gt;를 통해&lt;span&gt; SQL Engine&lt;/span&gt;과 상호작용 할 수 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.721%;&quot;&gt;&lt;span&gt;&lt;span&gt;ksqlDB CLI&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.279%;&quot;&gt;&lt;span&gt;&lt;span&gt;cli&lt;/span&gt;를 통해 사용자에게&lt;span&gt; SQL Engine&lt;/span&gt;과 상호작용 할 수 있는&lt;span&gt; Applicaiton&lt;/span&gt;이다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.721%;&quot;&gt;&lt;span&gt;&lt;span&gt;ksqlDB UI&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 76.279%;&quot;&gt;&lt;span&gt;&lt;span&gt;Confluent&lt;/span&gt;에서 제공하는&lt;span&gt; Web UI&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;ksqlDB 모드&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Interactive&amp;nbsp;mode&amp;nbsp;(대화형)&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;사용자가 실시간으로 쿼리를 제출하며, ksqlDB Server가 해당 쿼리를 parsing 및 execute 한다.&lt;br /&gt;모든 쿼리는 command topic이라는 internal-topic으로 publish 되며, 이는 클러스터 내 한번 실행됨을 의미한다.&lt;br /&gt;정리하면, 모든 ksqlDB는 command topic을 subscribe 하고 있으며, 각각의 consumer설정과 partition설정을 통해 가져가서 SQL Engine에게 넘겨준다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Headless&amp;nbsp;mode&amp;nbsp;(사전정의형)&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;REST API 등 실시간 입력을 차단하고, 사전정의 된 Query만 실행하는 모드이다.&lt;br /&gt;기동 시 properties로 실행할 sql의 위치를 지정해줘야 한다.&lt;br /&gt;속성은 queries.file로 정의한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KSQL Language&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;keywords&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;참고: &lt;/span&gt;&lt;a href=&quot;https://docs.ksqldb.io/en/latest/reference/sql/appendix/#keywords&quot;&gt;https://docs.ksqldb.io/en/latest/reference/sql/appendix/#keywords&lt;/a&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Example&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;CREATE&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;create an object&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;CREATE STREAM rock_songs (artist VARCHAR, title VARCHAR) &amp;hellip;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;DROP&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;delete an object&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;DROP CONNECTOR &amp;lt;connector-name&amp;gt;;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;ADD&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;add column&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;ADD (COLUMN)? identifier type;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;SELECT FROM&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;query a stream or table&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;SELECT * FROM metrics EMIT CHANGES;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;WHERE&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;filter records by a condition&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;SELECT * FROM pageviews WHERE pageid &amp;lt; 'Page_20'&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;INSERT INTO&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;insert new records in a stream/table&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;INSERT INTO &amp;lt;stream-name&amp;gt; ...&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;DELETE&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;remove a Kafka topic&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;DROP TABLE &amp;lt;table-name&amp;gt; DELETE TOPIC;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;GROUP BY&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;group rows with the same values&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;SELECT regionid, COUNT(*) FROM pageviews GROUP BY regionid&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;HAVING&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;condition expression&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;GROUP BY card_number HAVING COUNT(*) &amp;gt; 3&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;JOIN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;match records in streams/tables&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;CREATE TABLE t AS SELECT * FROM l INNER JOIN r ON &lt;a href=&quot;http://l.ID&quot;&gt;l.ID&lt;/a&gt; = &lt;a href=&quot;http://r.ID&quot;&gt;r.ID&lt;/a&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;KEY&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;specify key column&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;CREATE TABLE users (userId INT PRIMARY KEY, &amp;hellip;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;EXPLAIN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;show execution plan&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;EXPLAIN &amp;lt;query-name&amp;gt;; or EXPLAIN &amp;lt;expression&amp;gt;;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;AS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;alias a column, expression, or type&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;CASE, WHEN, THEN, ELSE, END&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;select a condition from expressions&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;SELECT CASE WHEN condition THEN result [ WHEN &amp;hellip; THEN &amp;hellip; ] &amp;hellip; END&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;IF EXIST&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;test whether object exists&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#deebff&quot;&gt;&lt;span&gt;&lt;span&gt;DROP STREAM IF EXISTS &amp;lt;stream-name&amp;gt;;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;PRINT&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;output records in a topic&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;PRINT &amp;lt;topic-name&amp;gt; FROM BEGINNING;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;PROPERTIES&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;list all properties&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;LIST PROPERTIES; or SHOW PROPERTIES;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;QUERIES&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;list all queries&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;LIST QUERIES; or SHOW QUERIES;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;TOPIC&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;specify a Kafka topic&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;DROP TABLE &amp;lt;table-name&amp;gt; DELETE TOPIC;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;TOPICS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;list all topics&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;LIST TOPICS; or SHOW TOPICS;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;PARTITION BY&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;repartition a stream&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;PARTITION BY &amp;lt;key-field&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;PARTITIONS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;partitions to distribute keys over&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;CREATE STREAM users_rekeyed WITH (PARTITIONS=6) AS &amp;hellip;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;CONNECTOR&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;manage a connector&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;CREATE SOURCE CONNECTOR 'jdbc-connector' WITH( &amp;hellip;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;span&gt;&lt;span&gt;CONNECTORS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 31.6278%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;list all connectors&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 52.0931%;&quot; data-highlight-colour=&quot;#fffae6&quot;&gt;&lt;span&gt;&lt;span&gt;SHOW CONNECTORS;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;Data Types&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;참고: &lt;/span&gt;&lt;a href=&quot;https://docs.ksqldb.io/en/latest/reference/sql/data-types/&quot;&gt;https://docs.ksqldb.io/en/latest/reference/sql/data-types/&lt;/a&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 317px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 10.814%; height: 34px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.2326%; height: 34px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 34px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Storage size&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 34px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;range&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 34px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 34px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Backing Java Type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 10.814%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;Boolean&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.2326%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;value representing &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; or &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;java.lang.Boolean&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 10.814%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;Character&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.2326%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;varchar&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 34px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 34px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;variable-length string&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;java.lang.String&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 10.814%; height: 69px;&quot; rowspan=&quot;5&quot;&gt;&lt;span&gt;&lt;span&gt;Numeric&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.2326%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;bytes&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;variable-length byte array&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://docs.oracle.com/javase/8/docs/api/java/lang/Byte.html&quot;&gt;byte []&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 11px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 11px;&quot;&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 11px;&quot;&gt;&lt;span&gt;&lt;span&gt;4 bytes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 11px;&quot;&gt;&lt;span&gt;&lt;span&gt;-231 to 231-1&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 11px;&quot;&gt;&lt;span&gt;&lt;span&gt;typical choice for integer&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 11px;&quot;&gt;&lt;span&gt;&lt;span&gt;Integer&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;bigint&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;8 bytes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;-263 to 263-1&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;large-range integer&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;Long&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 21px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 21px;&quot;&gt;&lt;span&gt;&lt;span&gt;double&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 21px;&quot;&gt;&lt;span&gt;&lt;span&gt;8 bytes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 21px;&quot;&gt;&lt;span&gt;&lt;span&gt;2-1074&amp;dagger; to (2-2-52)&amp;middot;21023&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 21px;&quot;&gt;&lt;span&gt;&lt;span&gt;variable-precision, inexact&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 21px;&quot;&gt;&lt;span&gt;&lt;span&gt;Double&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;decimal&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;value dependent&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;n/a&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;user-specified precision, exact&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;BigDecimal&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 10.814%; height: 102px;&quot; rowspan=&quot;3&quot;&gt;&lt;span&gt;&lt;span&gt;Time&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.2326%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;time&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 34px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 34px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;value representing a time of day in millisecond precision.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;java.sql.Time&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;date&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 34px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 34px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;value representing a calendar date independent of time zone.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;java.sql.Date&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;timestamp&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 34px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 34px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;value representing a point in time in millisecond precision without timezone information&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;java.sql.Timestamp&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 10.814%; height: 61px;&quot; rowspan=&quot;3&quot;&gt;&lt;span&gt;&lt;span&gt;Compound&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.2326%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;array&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;sequence of values of a single type&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 17px;&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://docs.oracle.com/javase/specs/jls/se11/html/jls-10.html&quot;&gt;Java native array&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;struct&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 34px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 34px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;a strongly typed structured data type&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 34px;&quot;&gt;&lt;span&gt;&lt;span&gt;org.apache.kafka.connect.data.Struct&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 10.2326%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;map&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.3023%; height: 10px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 13.0232%; height: 10px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 37.9069%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;a mapping of keys to values&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.4884%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;java.util.map&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ksqlDB 활용&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;INSTALL&lt;/h3&gt;
&lt;pre id=&quot;code_1716784558857&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// ksqlDB-server 설치
docker run -d  -p 127.0.0.1:8088:8088  -e KSQL_BOOTSTRAP_SERVERS=xxx.xxx.xxx.xxx:9092  -e KSQL_LISTENERS=http://0.0.0.0:8088/  -e KSQL_KSQL_SERVICE_ID=ksql_service_x confluentinc/ksqldb-server:0.29.0

// ksqlCli 설치 및 실행
docker run -it --net host confluentinc/ksqldb-cli ksql http://127.0.0.1:8088&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ksqlDB는 Kafka의 consumer/producer와 같은 레이어이며 로컬에서 테스트 가능하다.&lt;br /&gt;도커로 간편하게 설치 가능하며 내부 포트, bootstrap server, serviceid 정도 세팅했다.&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;ksqlDB Stream Test&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. topic 확인&lt;/h4&gt;
&lt;pre id=&quot;code_1716784633015&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ksql&amp;gt; show topics;
 Kafka Topic                                            | Partitions | Partition Replicas 
 ...                
 quickstart-avro-config                                 | 1          | 1                  
 quickstart-avro-offsets                                | 25         | 1                  
 quickstart-avro-status                                 | 5          | 1                  
 schema-changes.mysql                                   | 1          | 1                  
 schema-changes.mysql.event_history                     | 1          | 1                  
 survey.debezium                                        | 1          | 1                  
 survey.debezium.survey.event_history                   | 1          | 1                  
 test.ksql.1                                            | 1          | 1&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;test.ksql.1 토픽으로 Stream을 생성할 것이다.&lt;br /&gt;해당 토픽은 사전 생성했다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Create Stream&lt;/h4&gt;
&lt;pre id=&quot;code_1716784659952&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ksql&amp;gt; CREATE STREAM TEST_STREAM_1 (id VARCHAR KEY, desc STRING) WITH (KAFKA_TOPIC='test.ksql.1', VALUE_FORMAT='json');
CREATE STREAM TEST_STREAM_1 (id VARCHAR KEY, desc STRING) WITH (KAFKA_TOPIC='test.ksql.1', VALUE_FORMAT='json')
 Message        
 Stream created&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;심플한 stream을 생성했다.&lt;br /&gt;stream 이름은 TEST_STREAM_1이며 schema는 id와 desc가 문자열이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. INSERT DATA&lt;/h4&gt;
&lt;pre id=&quot;code_1716784687817&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ksql&amp;gt; INSERT INTO TEST_STREAM_1 VALUES ('a', 'test a');
INSERT INTO TEST_STREAM_1 VALUES ('a', 'test a')
ksql&amp;gt; INSERT INTO TEST_STREAM_1 VALUES ('b', 'test b');
INSERT INTO TEST_STREAM_1 VALUES ('b', 'test b')
ksql&amp;gt; INSERT INTO TEST_STREAM_1 VALUES ('c', 'test c');
INSERT INTO TEST_STREAM_1 VALUES ('c', 'test c')
ksql&amp;gt; INSERT INTO TEST_STREAM_1 VALUES ('a', 'test a');
INSERT INTO TEST_STREAM_1 VALUES ('a', 'test a')
ksql&amp;gt; INSERT INTO TEST_STREAM_1 VALUES ('a', 'test a');
INSERT INTO TEST_STREAM_1 VALUES ('a', 'test a')&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;간단하게 Data를 insert 할 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. PUSH Query&lt;/h4&gt;
&lt;pre id=&quot;code_1716784714425&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# no data (cause by offset properties)
ksql&amp;gt; SELECT * FROM TEST_STREAM_1 EMIT CHANGES;
+----------------------------------------+----------------------------------------+
|ID                                      |DESC                                    |
+----------------------------------------+----------------------------------------+
# set offset reset
ksql&amp;gt; set 'auto.offset.reset' = 'earliest';
Successfully changed local property 'auto.offset.reset' to 'earliest'. Use the UNSET command to revert your change.
# SELECT data
ksql&amp;gt; SELECT * FROM TEST_STREAM_1 EMIT CHANGES;
+----------------------------------------+----------------------------------------+
|ID                                      |DESC                                    |
+----------------------------------------+----------------------------------------+
|a                                       |test a                                  |
|b                                       |test b                                  |
|c                                       |test c                                  |
|a                                       |test a                                  |
|a                                       |test a                                  |&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ksqlDB가 Streams를 사용하기 때문에, reset 정책도 당연하게 가져간다.&lt;br /&gt;offset 정책을 earliest로 해서 모든 데이터를 초기 offset부터 읽을 수 있도록 설정할 수 있다.&lt;br /&gt;결과 table이 기존 rdbms랑 다르게 밑줄이 없는 것은, data 가 streaming 되기 때문이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5.&amp;nbsp;Query&amp;nbsp;with&amp;nbsp;GROUP&amp;nbsp;BY&lt;/h4&gt;
&lt;pre id=&quot;code_1716784754877&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ksql&amp;gt; SELECT id, COUNT(*) AS CNT FROM TEST_STREAM_1 GROUP BY id EMIT CHANGES;
+-----------------------------------------+-----------------------------------------+
|ID                                       |CNT                                      |
+-----------------------------------------+-----------------------------------------+
|b                                        |1                                        |
|c                                        |1                                        |
|a                                        |3                                        |&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;STREAM에서 id를 기준으로 group by 하여 count 결과를 select 했다.&lt;br /&gt;이는 PUSH Query로 조회했으며, 실시간으로 CNT가 반영되는 것을 볼 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6.&amp;nbsp;drop&amp;nbsp;stream&lt;/h4&gt;
&lt;pre id=&quot;code_1716784783671&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ksql&amp;gt; drop stream TEST_STREAM_1;
 Message
----------------------------------------------------------
 Source `TEST_STREAM_1` (topic: test.ksql.1) was dropped.
----------------------------------------------------------
ksql&amp;gt; SHOW STREAMS;
 Stream Name | Kafka Topic | Key Format | Value Format | Windowed
------------------------------------------------------------------
------------------------------------------------------------------&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;간단하게 STREAM을 삭제할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ksqlDB&amp;nbsp;Table&amp;nbsp;Test&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.&amp;nbsp;CREATE&amp;nbsp;Table&lt;/h4&gt;
&lt;pre id=&quot;code_1716784878168&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ksql&amp;gt; CREATE TABLE TEST_TABLE_1 (id VARCHAR PRIMARY KEY, desc STRING) WITH (KAFKA_TOPI
C='test.ksql.1', VALUE_FORMAT='json');
 Message
---------------
 Table created
---------------
ksql&amp;gt; SHOW TABLES;
 Table Name   | Kafka Topic | Key Format | Value Format | Windowed
-------------------------------------------------------------------
 TEST_TABLE_1 | test.ksql.1 | KAFKA      | JSON         | false
-------------------------------------------------------------------&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Simple하게 테이블을 생성했다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.&amp;nbsp;SELECT&amp;nbsp;Data&lt;/h4&gt;
&lt;pre id=&quot;code_1716784906483&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ksql&amp;gt; set 'auto.offset.reset' = 'earliest';
Successfully changed local property 'auto.offset.reset' to 'earliest'. Use the UNSET command to revert your change.
ksql&amp;gt; SELECT * FROM TEST_TABLE_1 EMIT CHANGES;
+-----------------------------------------+-----------------------------------------+
|ID                                       |DESC                                     |
+-----------------------------------------+-----------------------------------------+
|b                                        |test b                                   |
|c                                        |test c                                   |
|a                                        |test a                                   |&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;STREAM과 다르게 pk로 데이터가 override 된 것을 확인할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Extend KSQL Guide&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SubQuery&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;아쉽게도 ksql은 서브쿼리를 지원하지 않으며, 현재 이에 관련된 개발 로드맵은 없다.&lt;br /&gt;ksqlDB는 subQuery를 통한 복잡도는 프로젝트 성격 상 맞지 않다고 판단했으며&lt;br /&gt;SubQuery가 아닌 여러 Query를 Kafka로 통합하는 방향을 지향하고 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Join&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;KSQL의 join은 기존 Join과 다른 양상을 보인다.&lt;br /&gt;이는 Source가 되는 Record의 structure가 Stream 또는 Table로 나누어지기 때문이다.&lt;br /&gt;ksqlDB 진영에서 JOIN의 의미는 UPSTREAM의 데이터를 특정 기준으로 합쳐서 DOWNSTREAM을 생성하는 것이다.&lt;br /&gt;따라서 PULL Query로는 JOIN 할 수 없다.&lt;br /&gt;// Pull queries don't support JOIN clauses.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;종류&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;JOIN은 3종류가 있다.&lt;br /&gt;INNER JOIN: 양쪽 Record의 키가 같을 때 사용할 수 있는 JOIN&lt;br /&gt;LEFT JOIN: LEFT Record를 기준으로 downStream을 형성한다. 즉 FROM 절의 record가 들어올 때 downStream record를 생성하며, join 할 대상이 없는 경우 null이 된다.&lt;br /&gt;FULL JOIN: 양쪽 모두의 Record Stream을 받아서 downStream을 형성한다. 또한 JOIN 할 대상이 없다면 null이 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;Full Flow Example 참고: &lt;/span&gt;&lt;a href=&quot;https://docs.ksqldb.io/en/latest/developer-guide/joins/join-streams-and-tables/&quot;&gt;https://docs.ksqldb.io/en/latest/developer-guide/joins/join-streams-and-tables/&lt;/a&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;제약사항&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.4651%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Support Combination&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.9069%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Support JOIN Function&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 56.5117%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Etc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.4651%;&quot;&gt;&lt;span&gt;&lt;span&gt;Stream - Stream&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.9069%;&quot;&gt;&lt;span&gt;&lt;span&gt;INNER JOIN&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;LEFT JOIN&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;FULL JOIN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 56.5117%;&quot;&gt;&lt;span&gt;&lt;span&gt;Stream&lt;/span&gt;간의&lt;span&gt; JOIN&lt;/span&gt;은&lt;span&gt; Window &lt;/span&gt;설정을 해야한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;이는&lt;span&gt; Stream&lt;/span&gt;은 무한하기 때문에&lt;span&gt;, JOIN &lt;/span&gt;시점에 전체 데이터를 알 수 있는 기준이 없기 때문&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.4651%;&quot;&gt;&lt;span&gt;&lt;span&gt;Stream - Table&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.9069%;&quot;&gt;&lt;span&gt;&lt;span&gt;INNER JOIN&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;LEFT JOIN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 56.5117%;&quot;&gt;&lt;span&gt;&lt;span&gt;Table&lt;/span&gt;은&lt;span&gt; PK &lt;/span&gt;컬럼만&lt;span&gt; JOIN&lt;/span&gt;에 사용할 수 있다&lt;span&gt;. &lt;/span&gt;이는&lt;span&gt; KTable&lt;/span&gt;이&lt;span&gt; repartition&lt;/span&gt;이 불가하기 때문이다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;또한 최근 지원되는&lt;span&gt; Multi-PK&lt;/span&gt;의 경우 그 중 하나만 선택해서&lt;span&gt; JOIN&lt;/span&gt;이 가능하다&lt;span&gt;. &lt;/span&gt;즉&lt;span&gt; JOIN &amp;hellip; ON &amp;hellip; AND&lt;/span&gt;는 불가하다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.4651%;&quot;&gt;&lt;span&gt;&lt;span&gt;Table - Table&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 22.9069%;&quot;&gt;&lt;span&gt;&lt;span&gt;INNER JOIN&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;LEFT JOIN&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;FULL JOIN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 56.5117%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;JOIN의 큰 조건은 두 대상의 파티션 수가 같아야 하며, 동일 파티션 어싸이너를 가져야 한다.&lt;br /&gt;이는 key를 통한 JOIN을 할 때 파티션 매칭을 위한 것이다.&lt;br /&gt;또한 JOIN KEY는 모두 동일 데이터 타입이어야 한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;N-Way Joins&lt;/h4&gt;
&lt;pre id=&quot;code_1716785078512&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CREATE STREAM joined AS 
  SELECT * 
  FROM A
    JOIN B ON A.id = B.product_id
    JOIN C ON A.id = C.purchased_id;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;최신 버전 ksqlDB는 여러 Stream / Table의 JOIN을 지원한다.&lt;br /&gt;제약사항으로 JOIN 구성 중 STREAM, TABLE 모두 있다면 FULL JOIN은 지원하지 못한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ksqlDB Java Client&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;참고: &lt;/span&gt;&lt;a href=&quot;https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-clients/java-client/&quot;&gt;https://docs.ksqldb.io/en/latest/developer-guide/ksqldb-clients/java-client/&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ksqlDB를 Java Applicaiton에서 사용할 수 있도록, Confluent에서 Java Client Library를 제공한다.&lt;/blockquote&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;Gradle&lt;/h3&gt;
&lt;pre id=&quot;code_1716785125668&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'io.confluent.ksql:ksqldb-api-client:7.5.3'&lt;/code&gt;&lt;/pre&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;Client init&lt;/h3&gt;
&lt;pre id=&quot;code_1716785138867&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ClientOptions options = ClientOptions.create()
    .setHost(KSQLDB_SERVER_HOST)
    .setPort(KSQLDB_SERVER_HOST_PORT);
Client client = Client.create(options);
// Send requests with the client by following the other examples
// Terminate any open connections and close the client
client.close();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Client는 host, port로 간단하게 생성할 수 있다.&lt;/blockquote&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;Client&lt;/h3&gt;
&lt;pre id=&quot;code_1716785163645&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Client {
  /**
   * Executes a query (push or pull) and returns the results one row at a time.
   * 
   * &amp;lt;p&amp;gt;This method may be used to issue both push and pull queries, but the usage 
   * pattern is better for push queries. For pull queries, consider using the 
   * {@link Client#executeQuery(String)} method instead.
   *
   * &amp;lt;p&amp;gt;If a non-200 response is received from the server, the {@code CompletableFuture} will be
   * failed.
   *
   * @param sql statement of query to execute
   * @return a future that completes once the server response is received, and contains the query
   *         result if successful
   */
  CompletableFuture&amp;lt;StreamedQueryResult&amp;gt; streamQuery(String sql);
  ...
  /**
   * Executes a query (push or pull) and returns all result rows in a single batch, once the query
   * has completed.
   * 
   * &amp;lt;p&amp;gt;This method is suitable for both pull queries and for terminating push queries,
   * for example, queries that have a {@code LIMIT} clause. For non-terminating push queries,
   * use the {@link Client#streamQuery(String)} method instead.
   *
   * @param sql statement of query to execute
   * @return query result
   */
  BatchedQueryResult executeQuery(String sql);
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;여러 method를 지원한다.&lt;br /&gt;그중 streamQuery()는 CompletableFuture로 실행결과에 대한 future를 받는 형태이며, PUSH Query에 권장되는 메서드이다.&lt;br /&gt;executeQuery는 PULL Query에 적합하다. 또한 PUSH Query 중 LIMIT 등으로 끝이 있는 쿼리에도 사용할 수 있다.&lt;br /&gt;그 외 INSERT 등 여러 메서드를 지원한다.&lt;br /&gt;// 아쉽게도 Spring 진영에서 integration을 지원하고 있진 않다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ksqlDB는 Kafka를 통한 Event-Driven System에서 고수준 인터페이스를 통해 Stream처리 및 source, sink를 구성할 수 있는 product이다.&lt;br /&gt;기본적으로 SQL을 확장하여 인터페이스를 구성했으며, 상호작용을 위한 대화형 CLI, REST API를 제공한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Confluent에서 관리되고 있으며, kafka update에 맞춰 적극적인 업데이트가 이루어지고 있다.&lt;/blockquote&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;h4 data-ke-size=&quot;size20&quot;&gt;스팟&amp;nbsp;성&amp;nbsp;요구사항&amp;nbsp;대응&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일시적으로 기존 비즈니스와 다른 요구사항이 발생했을 때 적은 비용으로 대응이 가능하다.&lt;br /&gt;이는 Kafka의 장점을 손쉽게 활용할 수 있다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Develop&amp;nbsp;환경에서&amp;nbsp;활용&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;개발 중 임시로 활용할 데이터 생성이나 기존 데이터를 복제하는 등 일시적인 stream 구성을 할 때 활용할 수 있다.&lt;br /&gt;이는 개발 생산성을 올릴 수 있는 방안으로 생각된다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;잦은&amp;nbsp;변경이&amp;nbsp;예상되는&amp;nbsp;비즈니스&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SQL 대화형 인터페이스 또는 UI로 Data Processing 로직 변경이 가능하기 때문에&lt;br /&gt;잦은 변경이 예상되는 부분을 ksqlDB로 구성하여, 즉각적인 반영이 가능하다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Data ETL&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;복잡한 비즈니스 없는 ETL 요구사항에 활용될 수 있다.&lt;br /&gt;ksqlDB는 단순한 비즈니스를 구성하는데 큰 생산성 향상을 가져오며, 유지관리에도 용이한 장점이 있다.&lt;br /&gt;이는 Kafka Connect + Schema Registry + Streams를 통합하여 제공하기 때문이며&lt;br /&gt;데이터의 source + processing + sink를 한 번에 관리할 수 있기 때문이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;18673&quot; data-ke-size=&quot;size16&quot;&gt;도서 - OREILLY의 Mastering Kafka Streams and ksqlDB&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;18723&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.ksqldb.io/en/latest/reference/sql/syntax/lexical-structure/&quot;&gt;https://docs.ksqldb.io/en/latest/reference/sql/syntax/lexical-structure/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;18797&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.ksqldb.io/en/latest/operate-and-deploy/installation/install-ksqldb-with-docker/&quot;&gt;https://docs.ksqldb.io/en/latest/operate-and-deploy/installation/install-ksqldb-with-docker/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>Kafka</category>
      <category>KSQL</category>
      <category>KsqlDB</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/88</guid>
      <comments>https://p-bear.tistory.com/88#entry88comment</comments>
      <pubDate>Mon, 27 May 2024 21:48:31 +0900</pubDate>
    </item>
    <item>
      <title>Java Reactive Streams Publisher / Subscriber 분석 (projectreactor)</title>
      <link>https://p-bear.tistory.com/87</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Iaisf/btsF4UZRIP3/UhXGdzCy8wuTHZLds5tlm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Iaisf/btsF4UZRIP3/UhXGdzCy8wuTHZLds5tlm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Iaisf/btsF4UZRIP3/UhXGdzCy8wuTHZLds5tlm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIaisf%2FbtsF4UZRIP3%2FUhXGdzCy8wuTHZLds5tlm1%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;272&quot; height=&quot;242&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;242&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ReactibeStreams의 사상 및 목적 조사 및 Publisher / Subscriber의 실제 동작 확인&lt;br /&gt;Java의 Reactive Programing의 구현 사항 및 동작 원리 분석&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reactive Programing 이란&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터 스트림의 구성 및 변화의 전파에 대한 선언적 프로그래밍 패러다임&lt;br /&gt;이 패러다임을 도입하면 배열 또는 이벤트 스트림을 쉽게 표현할 수 있다.&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;1. (비동기적인) 이벤트 처리를 표현 및 구현하기 위해&lt;br /&gt;2. 데이터 스트림을 표현 및 구현하기 위해&lt;br /&gt;3. 반응형 시스템을 구현하기 위해&lt;br /&gt;즉 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;실시간 데이터의 변화를 효율적으로 반영하여 제공하기 위해&lt;/b&gt;&lt;/span&gt; 탄생했다.&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;1. Responsive: 시스템은 요청/호출에 대해 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;즉각 응답&lt;/b&gt;&lt;/span&gt;할 수 있어야 한다.&lt;br /&gt;2. Resilient: 장애에 대한 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;탄력성&lt;/b&gt;&lt;/span&gt;이 요구됩니다. (복제, 봉쇄, 격리, 위임 등 기법 활용)&lt;br /&gt;3. Elastic: 변화에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;유연함&lt;/b&gt;&lt;/span&gt;이 요구 됩니다. 환경 및 작업량에 대응할 수 있어야 합니다.&lt;br /&gt;4. Message Driven: &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;메시지에 기반&lt;/b&gt;&lt;/span&gt;한 시스템을 구성해야 합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ReactiveStreams&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;official page: &lt;/span&gt;&lt;a href=&quot;https://www.reactive-streams.org/&quot;&gt;https://www.reactive-streams.org/&lt;/a&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;- Reactive programing의 stream을 구성하기 위한 API를 정의한 단체, java 외 javascript, swift 등 여러 언어에 대한 API 및 Rule을 정의했다.&lt;br /&gt;&amp;nbsp;- 기본적으로 Reactive Programing을 표현하기 위해서 데이터 제공자(Publisher), 데이터 소비자(Subscriber)를 분리해 정의했다.&lt;br /&gt;&amp;nbsp;- Java의 reactive code API는 java9의 concurrent.Flow를 이용하여 정의되었다.&lt;br /&gt;&amp;nbsp;- 총 4개의 interface를 정의했으며, 각 구현에 Rule을 정의했으며 각각 publisher, Subscriber, Subscription, Processor이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Core IDEA&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Data의 발행 주체와 소비 주체를 분리하는 것이 핵심 아이디어이다. Publisher는 Data 발행에 집중하고,. Subscriber는 데이터의 소비에 집중하는 구조를 지향했다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실시간&amp;nbsp;Data&amp;nbsp;전파&amp;nbsp;구성&amp;nbsp;방식&amp;nbsp;(reactiveStreams&amp;nbsp;이전)&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;- PUSH 방식: Publisher가 Data를 발행할 수 있을 때 Subscriber에게 제공(호출)한다.&lt;br /&gt;&amp;nbsp;- POLL 방식: Subscriber가 소비할 준비가 되었을 때 Data를 요청한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제점&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. PUSH 방식 문제: Subscriber의 상태와 관계없이 Data를 PUSH 하기 때문에, Subscriber의 문제를 대응하기 쉽지 않은 상황이 발생한다. 가령 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;subscriber의 지연이 발생했을 때 publisher는 이를 기다리거나, subscriber에게 buffer(or queue)를 두어&lt;/b&gt;&lt;/span&gt;서 해결해야 한다.&lt;br /&gt;2. POLL 방식 문제: &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Publisher의 Data를 지속적으로 체크하기 위한 요청 오버헤드가 발생&lt;/b&gt;&lt;/span&gt;한다. 이는 시스템이 복잡하고 커질수록 트래픽이 증가하는 결과를 가져오며 비용이 증가하고, 안정성이 떨어진다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ReactiveStreams에서는 Publisher는 Data 제공만 관심을 가지고, Subscriber는 소비만 관심을 가진다. 이외 Data 전달에 대해서는 중간 계층인 Subscription을 두고 해결한다.&lt;br /&gt;Publisher는 Data가 준비되면 각 Subscription에게 Data를 제공하고 Subscriber는 소비할 수 있을 만큼만 Subscription에게 제공받는다.&lt;br /&gt;여기서 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Backpressure&lt;/b&gt;&lt;/span&gt;라는 핵심 개념이 들어가는데, &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Subscriber는 소비할 수 있는 만큼 Subscription에게 request 하며, 데이터를 request 한 만큼 push 받는 개념&lt;/span&gt;&lt;/b&gt;이다. 이 개념을 통해서 Subscriber는 buffer 없이 Data를 안정적으로 제공받을 수 있다.&lt;/blockquote&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;1. Publisher&lt;/h3&gt;
&lt;pre id=&quot;code_1711429358087&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Publisher&amp;lt;T&amp;gt; {
    public void subscribe(Subscriber&amp;lt;? super T&amp;gt; s);
}&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 337px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;id&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Rule&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 40px;&quot;&gt;&lt;span&gt;&lt;span&gt;(Publisher-&amp;gt;Subscriber&lt;/span&gt;의&lt;span&gt; onNext).count &amp;lt;= Subscriber.(subscription.request).totalcount&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;onNext&lt;/span&gt;의 호출은&lt;span&gt; request&lt;/span&gt;된&lt;span&gt; element&lt;/span&gt;보다 작아야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;Publisher&lt;/span&gt;는&lt;span&gt; onNext&lt;/span&gt;도중&lt;span&gt; subscription&lt;/span&gt;을 종료하고&lt;span&gt; onComplete, onError signal&lt;/span&gt;로 변경할 수 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;publisher&lt;/span&gt;가 제공하는 모든&lt;span&gt; signal&lt;/span&gt;은&lt;span&gt; seial(sync)&lt;/span&gt;해야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;4&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;publisher&lt;/span&gt;의 오류 발생 시&lt;span&gt; onError signal&lt;/span&gt;을 주어야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;publisher&lt;/span&gt;의 정상 종료 시&lt;span&gt; onComplete signal&lt;/span&gt;을 주어야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;6&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;onError, onComplete signal &lt;/span&gt;발생 시&lt;span&gt; subscription&lt;/span&gt;은&lt;span&gt; cancel&lt;/span&gt;로 간주한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;7&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;terminate &lt;/span&gt;상태일 시 더 이상&lt;span&gt; signal&lt;/span&gt;을 발생 시키면 안된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;8&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;subscription&lt;/span&gt;이&lt;span&gt; cancel&lt;/span&gt;되면&lt;span&gt; subsciber&lt;/span&gt;에게&lt;span&gt; signal&lt;/span&gt;을 주면 안된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 100px;&quot;&gt;
&lt;td style=&quot;height: 100px;&quot;&gt;&lt;span&gt;&lt;span&gt;9&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 100px;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;publisher&lt;/b&gt;&lt;b&gt;의 subscribe 메서드는 아래 사항을 구현해야 한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;● Subscription&lt;/b&gt;&lt;b&gt;을 생성해야 한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;● subscriber.onSubscribe(Subscription)&lt;/b&gt;&lt;b&gt;를 호출해야 한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;● throw &lt;/b&gt;&lt;b&gt;없이 종료 되어야 한다. (parameter가 null인 경우 제외)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;● &lt;/b&gt;&lt;b&gt;에러 발생 시 onError를 호출 해야한다. (onSubscribe 호출 이후)&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;다른&lt;span&gt; Subscriber&lt;/span&gt;에 대해서 여러번 호출 될 수 있어야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;&lt;span&gt;11&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;span&gt;여러&lt;span&gt; subscriber&lt;/span&gt;를 지원하는 것이 권장된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Publisher는 Subscription 및 Data의 생성을 담당한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;Subscription은 subscribe 호출 시 생성하며, 호출 당 하나의 subscription을 만든다.&lt;br /&gt;Data는 자체적으로 생성하거나 상위 스트림에서 받을 수 있어야 한다.&lt;/blockquote&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;2. Subscriber&lt;/h3&gt;
&lt;pre id=&quot;code_1711429424840&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Subscriber&amp;lt;T&amp;gt; {
    public void onSubscribe(Subscription s);
    public void onNext(T t);
    public void onError(Throwable t);
    public void onComplete();
}&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;id&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Rule&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;onSubscribe(Subscription)에서 Subscription.request(long n)로 onNext signal을 받아야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Publisher&lt;/span&gt;의&lt;span&gt; responsivity&lt;/span&gt;를 저해할 수 있다면&lt;span&gt; async&lt;/span&gt;하게&lt;span&gt; signaling&lt;/span&gt;을 권장&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;onComplete() &lt;/span&gt;및&lt;span&gt; onError() &lt;/span&gt;메서드에서&lt;span&gt; subscription&lt;/span&gt;또는&lt;span&gt; publisher&lt;/span&gt;를 호출하면 안된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;4&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;onComplete &lt;/span&gt;및&lt;span&gt;onError&lt;/span&gt;는&lt;span&gt; subscription&lt;/span&gt;이&lt;span&gt; cancel&lt;/span&gt;되었다고 간주한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;subscription&lt;/span&gt;이 진행된 후&lt;span&gt; onSubscribe() &lt;/span&gt;요청의&lt;span&gt; subscription&lt;/span&gt;은&lt;span&gt; cancel()&lt;/span&gt;한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;6&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;subscription&lt;/span&gt;이 필요 없어지는 경우&lt;span&gt; cancel()&lt;/span&gt;해야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;7&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;publisher&lt;/span&gt;가 제공하는 모든&lt;span&gt; signal&lt;/span&gt;의&lt;span&gt; serial(sync) &lt;/span&gt;여부를 확인해야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;8&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;pending&lt;/span&gt;된&lt;span&gt; element&lt;/span&gt;가 있을 수 있으니&lt;span&gt; cancel() &lt;/span&gt;호출 이후&lt;span&gt; onNext()&lt;/span&gt;를 받을 수 있어야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;9&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Subscription.request() &lt;/span&gt;호출 전에&lt;span&gt; onComplete &lt;/span&gt;신호를 받을 수 있어야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Subscription.request() &lt;/span&gt;호출 전에&lt;span&gt; onError &lt;/span&gt;신호를 받을 수 있어야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;11&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;signal &lt;/span&gt;처리는 각각 수행되어야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;12&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;onSubscribe()&lt;/span&gt;는 최대 한번 호출 됩니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;13&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;모든 메서드는&lt;span&gt; thrown &lt;/span&gt;없이 정상 종료 되어야 합니다&lt;span&gt;. &lt;/span&gt;하지만&lt;span&gt; parameter&lt;/span&gt;가&lt;span&gt; null&lt;/span&gt;인 경우&lt;span&gt; nullPointException&lt;/span&gt;을 발생 시킵니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Subscriber는 Data 소비에 대한 기능을 담당한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;publisher에게 subscribe(Subscriber)로 전달되면, onSubscribe가 한번 무조건 호출되며, 이때 Subscription에게 element를 request(n) 해야 한다.&lt;br /&gt;이후 각 signal이 호출될 수 도 안 될 수 도 있다.&lt;/blockquote&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;3. Subscription&lt;/h3&gt;
&lt;pre id=&quot;code_1711429485063&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Subscription {
    public void request(long n);
    public void cancel();
}&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;id&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Rule&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;request()&lt;/span&gt;와&lt;span&gt; cancel()&lt;/span&gt;은&lt;span&gt; Subscriber&lt;/span&gt;만 호출된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;request()&lt;/span&gt;는&lt;span&gt; subscriber&lt;/span&gt;의&lt;span&gt; onNext, onSubscribe&lt;/span&gt;에서 호출된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;3&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;request() &lt;/span&gt;호출의 재귀 현상에 대한 제한을 두어야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;4&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;request()&lt;/span&gt;는 최대한 지연 없이 구현되어야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;cancel()&lt;/span&gt;은 최대한 지연 없이 구현되어야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;6&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;cancel() &lt;/span&gt;이후 호출되는&lt;span&gt; request()&lt;/span&gt;는 동작하면 안된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;7&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;cancel() &lt;/span&gt;이후 호출되는&lt;span&gt; cancel()&lt;/span&gt;은 동작하면 안된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;8&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;cancel &lt;/span&gt;되지 않은 상황에서&lt;span&gt; request()&lt;/span&gt;는 반복 호출 될 수 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;9&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;request(long n)&lt;/span&gt;에서&lt;span&gt; parameter &amp;lt;= 0 &lt;/span&gt;인 경우&lt;span&gt; onError&lt;/span&gt;를&lt;span&gt; IllegalArgumentException&lt;/span&gt;과 함께 발생 시킨다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;request()&lt;/b&gt;&lt;b&gt;는 subscriber의 onNext를 sync하게 호출 한다 (cancel 되지 않았다면)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;11&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;request()&lt;/b&gt;&lt;b&gt;는 subscriber의 onComplete, onError를 sync하게 호출 한다 (cancel 되지 않았다면)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;12&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;첫 cancel() 호출 시 publisher에게 signal 중단을 notify 해야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;13&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;첫&lt;span&gt; cancel() &lt;/span&gt;호출 시&lt;span&gt; publisher&lt;/span&gt;에게&lt;span&gt; subscriber&lt;/span&gt;에 대한&lt;span&gt; reference&lt;/span&gt;를 끊도록&lt;span&gt; notify &lt;/span&gt;해야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;14&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;cancel()&lt;/span&gt;이 호출되어 모든&lt;span&gt; subscription&lt;/span&gt;이 취소되면&lt;span&gt; publisher&lt;/span&gt;는&lt;span&gt; shutdown&lt;/span&gt;을 할 것이다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;15&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;cancel() &lt;/span&gt;호출은&lt;span&gt; throw &lt;/span&gt;없이 종료 되어야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;16&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;request() &lt;/span&gt;호출은&lt;span&gt; throw &lt;/span&gt;없이 종료 되어야 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;17&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;request(n)&lt;/span&gt;의&lt;span&gt; n &lt;/span&gt;개수는&lt;span&gt; Long.MAX_VALUE&lt;/span&gt;까지 지원해야 하며&lt;span&gt;, Long.MAX_VALUE&lt;/span&gt;는 무한으로 인식&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Subscription은 publisher와 Subscriber 사이에서 data flow(signaling)를 표현한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;Publisher에 의해 subscribe() 될 때마다 하나씩 생성되며, 하나의 Subscriber와 연관된다.&lt;/blockquote&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;4. Processor&lt;/h3&gt;
&lt;pre id=&quot;code_1711429536146&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Processor&amp;lt;T, R&amp;gt; extends Subscriber&amp;lt;T&amp;gt;, Publisher&amp;lt;R&amp;gt; {
}&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;id&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Rule&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Subscriber&lt;/span&gt;와&lt;span&gt; Publisher&lt;/span&gt;의&lt;span&gt; Rule &lt;/span&gt;모두 만족해야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;onError signal&lt;/span&gt;을 자체적으로&lt;span&gt; handling&lt;/span&gt;할 수 있지만&lt;span&gt;, subscription&lt;/span&gt;을&lt;span&gt; cancel &lt;/span&gt;해야 합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Publisher + Subscriber&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ProjectReactor에서 해당 인터페이스의 구현체들은 deprecated 되었다. (will be removed in 3.5)&lt;br /&gt;https://github.com/reactor/reactor-core/issues/2431&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ReactiveStreams&amp;nbsp;Example&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 Rule을 바탕으로 간단한 예시 코드를 작성했다.&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;예시를 통해 Pub &amp;rarr; Sub에 대한 전체적인 흐름을 파악하고, 각 요소들의 역할에 대해 설명하고자 한다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;(projectreactor가 구현한 Pub/Sub의 구현체인 Mono, Flux가 복잡하여 핵심 요약이 힘들다.)&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;구현 사항:&lt;br /&gt;&amp;nbsp;- publisher: random 한 int값을 max 개수만큼 생성한다.&lt;br /&gt;&amp;nbsp;- subscriber: n개 구독하여 element를 받을 때마다 더하는 구독자&lt;br /&gt;&amp;nbsp;- subscription: n개만큼 request 해서 내려주고, 많으면 cancel 하고 완료되면 complete, error는 error처리&lt;br /&gt;&amp;nbsp;- processor: 지정된 int 함수를 실행하면서 pub / sub 연결&lt;br /&gt;명확한 flow 파악을 위해 전체적으로 sync 하게 구현했다. &lt;br /&gt;Mono/Flux는 이를 비동기, 논블로킹, backpressure 등 개념을 적용해서 구현되어 있다.&lt;/blockquote&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;Sample Implementation&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SimplePublisher&lt;/h4&gt;
&lt;pre id=&quot;code_1711429704828&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SimplePublisher implements Publisher&amp;lt;Integer&amp;gt; {
  private final List&amp;lt;Integer&amp;gt; dataSources;
  private final Map&amp;lt;String, Subscription&amp;gt; subscriptions = new HashMap&amp;lt;&amp;gt;();

  private SimplePublisher(final List&amp;lt;Integer&amp;gt; dataSources) {
    this.dataSources = dataSources;
  }

  public static SimplePublisher create(final int maxCount) {
    return new SimplePublisher(
        IntStream.generate(() -&amp;gt; (int) (Math.random() * 100))
            .limit(maxCount)
            .boxed()
            .toList());
  }

  @Override
  public synchronized void subscribe(final Subscriber&amp;lt;? super Integer&amp;gt; subscriber) {
    System.out.println(&quot;[SimplePublisher] subscribe by: &quot; + subscriber.getClass().getSimpleName());
    String id = UUID.randomUUID().toString();
    SimpleSubscription subscription = new SimpleSubscription(id, subscriber, this.dataSources, this);
    this.subscriptions.put(id, subscription);
    subscriber.onSubscribe(subscription);
  }

  public void cancelSignal(final String subscriptionId) {
    System.out.println(&quot;[SimplePublisher] cancelSignal&quot;);
    this.subscriptions.get(subscriptionId).cancel();
    this.deleteSubscription(subscriptionId);
  }

  public void deleteSubscription(final String subscriptionId) {
    System.out.println(&quot;[SimplePublisher] subscription removed&quot;);
    this.subscriptions.remove(subscriptionId);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Publisher의 메인 역할&lt;br /&gt;1. 구독자에 따른 Subscription 관리 (create, remove)&lt;br /&gt;2. provide element&lt;br /&gt;예시는 subscribe시마다 subscription을 생성하여 map으로 관리한다.&lt;br /&gt;Publisher의 dataSource는 생성 시 결정 (like Mono.just())&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SimpleSubscriber&lt;/h4&gt;
&lt;pre id=&quot;code_1711429749047&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class SimpleSubscriber implements Subscriber&amp;lt;Integer&amp;gt; {
  private final int subscribeCount;
  private int current = 0;

  @Override
  public void onSubscribe(final Subscription subscription) {
    System.out.println(&quot;[SimpleSubscriber] onSubscribe, subscription: &quot; + subscription.getClass().getSimpleName());
    subscription.request(subscribeCount);
  }

  @Override
  public void onNext(final Integer t) {
    current += t;
    System.out.println(&quot;[SimpleSubscriber] next: &quot; + t + &quot;, current: &quot; + this.current);
  }

  @Override
  public void onError(final Throwable throwable) {
    System.out.println(&quot;[SimpleSubscriber] error: &quot; + throwable.getMessage());
  }

  @Override
  public void onComplete() {
    System.out.println(&quot;[SimpleSubscriber] complete, final: &quot; + this.current);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Subscriber의 역할&lt;br /&gt;1. subscribe시 subscription을 통해 publisher로 element를 요청 (with count)&lt;br /&gt;2. element가 올 때마다 처리&lt;br /&gt;3. error 처리&lt;br /&gt;4. complete 처리&lt;br /&gt;예시는 생성자로 받은 maxCount만큼 subscription에게 요청한다.&lt;br /&gt;element가 올 때 마다 값을 더하여 유지한다.&lt;br /&gt;완료 시 지금까지 더한 값을 표시한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SimpleSubscription&lt;/h4&gt;
&lt;pre id=&quot;code_1711429796662&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class SimpleSubscription implements Subscription {
  private final String subscriptionId;
  private final Subscriber&amp;lt;? super Integer&amp;gt; subscriber;
  private final List&amp;lt;Integer&amp;gt; source;
  private final SimplePublisher parent;

  @Override
  public void request(final long n) {
    System.out.println(&quot;[SimpleSubscription] request n: &quot; + n);
    try {
      this.source.stream().limit(n).forEach(subscriber::onNext);
      if (source.size() &amp;gt; n) {
        this.parent.cancelSignal(this.subscriptionId);
        return;
      }
      this.subscriber.onComplete();
      this.parent.deleteSubscription(this.subscriptionId);
    } catch (Exception e) {
      this.subscriber.onError(e);
    }
  }

  @Override
  public void cancel() {
    System.out.println(&quot;[SimpleSubscription] cancel&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Subscription의 역할&lt;br /&gt;1. request 받은 개수만큼 subscriber에게 제공 (onNext)&lt;br /&gt;2. subscriber에게 complete signal 생성&lt;br /&gt;3. subscriber에게 error signal 생성&lt;br /&gt;4. publisher에게 cancel signal 생성&lt;br /&gt;5. cancel처리&lt;br /&gt;예시에서는 request시 publisher에게 제공받은 source를 subscriber에게 순차적으로 제공한다.&lt;br /&gt;subscriber의 request에 따라서 자동으로 cancel을 publisher로 호출한다. (requestCount &amp;lt; elementCount)&lt;br /&gt;완료 및 에러 발생 시 subscriber에게 signal 생성(호출)&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SimpleProcessor&lt;/h4&gt;
&lt;pre id=&quot;code_1711429836685&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class SimpleProcessor implements Processor&amp;lt;Integer, Integer&amp;gt; {
  private final Function&amp;lt;Integer, Integer&amp;gt; addOne = i -&amp;gt; i + 1;
  private final Function&amp;lt;Integer, Integer&amp;gt; subOne = i -&amp;gt; i - 1;

  private final Publisher&amp;lt;Integer&amp;gt; delegatePublisher;
  private Subscriber&amp;lt;? super Integer&amp;gt; delegateSubscriber;
  private final String functionName;

  public static SimpleProcessor from(final Publisher&amp;lt;Integer&amp;gt; from, final String functionName) {
    return new SimpleProcessor(from, functionName);
  }

  @Override
  public void subscribe(final Subscriber&amp;lt;? super Integer&amp;gt; subscriber) {
    System.out.println(&quot;[SimpleProcessor] subscribe by: &quot; + subscriber.getClass().getSimpleName());
    this.delegateSubscriber = subscriber;
    this.delegatePublisher.subscribe(this);
  }

  @Override
  public void onSubscribe(final Subscription subscription) {
    System.out.println(&quot;[SimpleProcessor] onSubscribe, subscription: &quot; + subscription.getClass().getSimpleName());
    this.delegateSubscriber.onSubscribe(subscription);
  }

  @Override
  public void onNext(final Integer t) {
    System.out.println(&quot;[SimpleProcessor] next: &quot; + t);
    this.delegateSubscriber.onNext(apply(t));
  }

  @Override
  public void onError(final Throwable throwable) {
    System.out.println(&quot;[SimpleProcessor] error: &quot; + throwable.getMessage());
    this.delegateSubscriber.onError(throwable);
  }

  @Override
  public void onComplete() {
    System.out.println(&quot;[SimpleProcessor] complete&quot;);
    this.delegateSubscriber.onComplete();
  }

  private Integer apply(final int origin) {
    return switch (this.functionName) {
      case &quot;addOne&quot; -&amp;gt; this.addOne.apply(origin);
      case &quot;subOne&quot; -&amp;gt; this.subOne.apply(origin);
      default -&amp;gt; origin;
    };
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Processor는 publisher와 subscriber의 책임을 모두 가진다.&lt;br /&gt;예시에서는 pub/sub을 delegation 하며, onNext 시 지정된 함수를 적용하여 내려주고 있다.&lt;br /&gt;// Subscription 생성은 생략했다.&lt;/blockquote&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;Sample Flow 분석&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.&amp;nbsp;Publish&amp;nbsp;&amp;rarr;&amp;nbsp;Subscribe&amp;nbsp;Flow&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;1005&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NbhIW/btsF7HLMHRQ/DwPvqNkJTCftZgT6fdRyZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NbhIW/btsF7HLMHRQ/DwPvqNkJTCftZgT6fdRyZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NbhIW/btsF7HLMHRQ/DwPvqNkJTCftZgT6fdRyZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNbhIW%2FbtsF7HLMHRQ%2FDwPvqNkJTCftZgT6fdRyZk%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;624&quot; height=&quot;1005&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;1005&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1711429923858&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// init Pub / Sub
SimplePublisher simplePublisher = SimplePublisher.create(5);
SimpleSubscriber simpleSubscriber = new SimpleSubscriber(5);

// use Pub / Sub
simplePublisher.subscribe(simpleSubscriber);&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1711429932039&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// log
[SimplePublisher] subscribe by: SimpleSubscriber
[SimpleSubscriber] onSubscribe, subscription: SimpleSubscription
[SimpleSubscription] request n: 5
[SimpleSubscriber] next: 11, current: 11
[SimpleSubscriber] next: 35, current: 46
[SimpleSubscriber] next: 32, current: 78
[SimpleSubscriber] next: 29, current: 107
[SimpleSubscriber] next: 94, current: 201
[SimpleSubscriber] complete, final: 201
[SimplePublisher] subscription removed&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;생성 및 구독 단계&lt;/b&gt;&lt;br /&gt;&amp;nbsp;- 시작 전 Element 5개를 제공하는 Publisher와 5개를 구독하여 처리할 Subscruber 인스턴스를 생성했다.&lt;br /&gt;&amp;nbsp;- publisher는 subscribe(Subscriber) 호출을 통해 전체적인 Flow를 관장할 Subscription을 생성한다.&lt;br /&gt;&amp;nbsp;- publisher는 Subscription 생성 후 subscriber에게 onSubscribe를 생성하며, 아직 데이터를 전달하진 않았다.&lt;br /&gt;&amp;nbsp;- subscriber는 onSubscribe() 호출받았으며 Subscription에게 onRequest()로 Element(Data) 발행을 요청한다.&lt;br /&gt;&amp;nbsp;- Subscription은 Subscriber의 각 signal handler를 호출한다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;Data processing 단계&lt;/b&gt;&lt;br /&gt;&amp;nbsp;- Subscription은 상황에 맞게 Subscriber의 onNext(), onError(), onComplete() 호출한다.&lt;br /&gt;&amp;nbsp;- Subscriber는 각 signal에 대한 처리를 한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.&amp;nbsp;with&amp;nbsp;Cancel&amp;nbsp;Flow&lt;/h4&gt;
&lt;pre id=&quot;code_1711429998895&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SimplePublisher simplePublisher = SimplePublisher.create(5);
simplePublisher.subscribe(new SimpleSubscriber(3));&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1711430004476&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[SimplePublisher] subscribe by: SimpleSubscriber
[SimpleSubscriber] onSubscribe, subscription: SimpleSubscription
[SimpleSubscription] request n: 3
[SimpleSubscriber] next: 44, current: 44
[SimpleSubscriber] next: 70, current: 114
[SimpleSubscriber] next: 60, current: 174
[SimplePublisher] cancelSignal
[SimpleSubscription] cancel
[SimplePublisher] subscription removed&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Request보다 Element가 많을 시 cancel 요청을 보내서 subscription을 cancel 처리하는 예시이다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3.&amp;nbsp;Publish&amp;nbsp;&amp;rarr;&amp;nbsp;Processor&amp;nbsp;&amp;rarr;&amp;nbsp;Subscriber&amp;nbsp;Flow&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;1075&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KUrew/btsF6JXytvs/RtkLlbhkU0fgFKZYmDt7tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KUrew/btsF6JXytvs/RtkLlbhkU0fgFKZYmDt7tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KUrew/btsF6JXytvs/RtkLlbhkU0fgFKZYmDt7tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKUrew%2FbtsF6JXytvs%2FRtkLlbhkU0fgFKZYmDt7tk%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;624&quot; height=&quot;1075&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;1075&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;예시 프로세스:&lt;br /&gt;1. 사용자는 processor객체를 parent publisher를 가지고 생성한다.&lt;br /&gt;2. processor를 통해 subscribe()하면, processor는 자신을 publisher에게 subscriber로 넘긴다.&lt;br /&gt;3. publisher는 processor를 subscriber로 간주하고 Subscription을 생성한다.&lt;br /&gt;4. onSubscribe는 publisher &amp;rarr; processor &amp;rarr; subscriber 순으로 진행&lt;br /&gt;5. subscriber는 subscription에게 request 한다.&lt;br /&gt;6. onNext 등 signal은 Subscription &amp;rarr; processor &amp;rarr; subscriber로 진행&lt;br /&gt;Processor는 Pub + Sub이며, 중간 비즈니스를 담당한다.&lt;br /&gt;예시 Processor는 delegate 할 pub / sub을 받아서 onNext에만 개입하여 function을 execute 하는 역할을 가진다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sample Conclusion&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;전반적인 reactibestreams의 rule에 따른 구현은 위와 같이 구성할 수 있다.&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;이 구조는 Publisher나 Subscriber가 유연하게 확장될 수 있으며, backpressure 개념이 들어가 있다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;기존 push / poll 구조의 단점인 subscriber buffer 또는 periodic call 문제가 없이, data stream을 구성할 수 있다.&lt;br /&gt;위 구성에 비동기적 동작 및 디테일을 붙이면 실시간 data처리가 가능한 backpressure 기반 stream이 구현된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ProjectReactor&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;reactivestreams의 구현한 프로젝트이다.&lt;br /&gt;주요 구현으로는 Publisher, Subscriber, Subscription, processor(will be removed in 3.5)가 있다.&lt;br /&gt;구현체 몇 가지를 살펴보며 설명을 진행하고자 한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;projectreactor는 추가적으로 비동기처리를 지원하기 위한 scheduler를 정의하고 구현했다.&lt;br /&gt;(scheduler 참고: https://p-bear.tistory.com/68)&lt;/blockquote&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;MonoJust&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Create형 Publisher&lt;/blockquote&gt;
&lt;pre id=&quot;code_1711430279234&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class MonoJust&amp;lt;T&amp;gt; extends Mono&amp;lt;T&amp;gt; implements Fuseable.ScalarCallable&amp;lt;T&amp;gt;, Fuseable, SourceProducer&amp;lt;T&amp;gt;  {
	final T value;

	MonoJust(T value) {
		this.value = Objects.requireNonNull(value, &quot;value&quot;);
	}
	...

	@Override
	public void subscribe(CoreSubscriber&amp;lt;? super T&amp;gt; actual) {
		actual.onSubscribe(Operators.scalarSubscription(actual, value));
	}
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Publisher를 &amp;ldquo;create&amp;rdquo; 할 수 있는 대표적인 구현체이다. (Mono.just(&amp;hellip;))&lt;br /&gt;생성자에서 단일 값을 받아 유지하며, Subscribe 시 해당 값으로 subscription을 생성하여 onSubscribe를 호출하는 것을 볼 수 있다.&lt;/blockquote&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;MonoDefer&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Deleagete형 Publisher&lt;/blockquote&gt;
&lt;pre id=&quot;code_1711430311341&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class MonoDefer&amp;lt;T&amp;gt; extends Mono&amp;lt;T&amp;gt; implements SourceProducer&amp;lt;T&amp;gt; {
	final Supplier&amp;lt;? extends Mono&amp;lt;? extends T&amp;gt;&amp;gt; supplier;

	MonoDefer(Supplier&amp;lt;? extends Mono&amp;lt;? extends T&amp;gt;&amp;gt; supplier) {
		this.supplier = Objects.requireNonNull(supplier, &quot;supplier&quot;);
	}

	@SuppressWarnings(&quot;unchecked&quot;)
	@Override
	public void subscribe(CoreSubscriber&amp;lt;? super T&amp;gt; actual) {
		Mono&amp;lt;? extends T&amp;gt; p;

		try {
			p = Objects.requireNonNull(supplier.get(), &quot;The Mono returned by the supplier is null&quot;);
		}
		catch (Throwable e) {
			Operators.error(actual, Operators.onOperatorError(e, actual.currentContext()));
			return;
		}

		fromDirect(p).subscribe(actual);
	}
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;상위 Publisher(Mono)를 생성자로 받아서 유지하며, subscribe 시 해당 publisher에게 subscribe를 위임한다. (위임 앞뒤로 추가 로직들을 수행한다.)&lt;br /&gt;just와 달리 onSubscribe에 관여하지 않으며, 상위 스트림에 subscribe()를 연결하는 역할만 한다.&lt;/blockquote&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;LamdaSubscriber&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Execute UserFunction Subscriber&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1371&quot; data-origin-height=&quot;277&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ijh4Q/btsF5RO9360/zSsVBc2xa1xi3XscNiX2Lk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ijh4Q/btsF5RO9360/zSsVBc2xa1xi3XscNiX2Lk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ijh4Q/btsF5RO9360/zSsVBc2xa1xi3XscNiX2Lk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIjh4Q%2FbtsF5RO9360%2FzSsVBc2xa1xi3XscNiX2Lk%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;1371&quot; height=&quot;277&quot; data-origin-width=&quot;1371&quot; data-origin-height=&quot;277&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1711430364899&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class LambdaSubscriber&amp;lt;T&amp;gt; implements InnerConsumer&amp;lt;T&amp;gt;, Disposable {
	final Consumer&amp;lt;? super T&amp;gt;            consumer;
	final Consumer&amp;lt;? super Throwable&amp;gt;    errorConsumer;
	final Runnable                       completeConsumer;
	final Consumer&amp;lt;? super Subscription&amp;gt; subscriptionConsumer;
	final Context                        initialContext;
	volatile Subscription subscription;

    LambdaSubscriber(
			@Nullable Consumer&amp;lt;? super T&amp;gt; consumer,
			@Nullable Consumer&amp;lt;? super Throwable&amp;gt; errorConsumer,
			@Nullable Runnable completeConsumer,
			@Nullable Consumer&amp;lt;? super Subscription&amp;gt; subscriptionConsumer,
			@Nullable Context initialContext) {
		this.consumer = consumer;
		this.errorConsumer = errorConsumer;
		this.completeConsumer = completeConsumer;
		this.subscriptionConsumer = subscriptionConsumer;
		this.initialContext = initialContext == null ? Context.empty() : initialContext;
	}
    ...

	@Override
	public final void onSubscribe(Subscription s) {
		if (Operators.validate(subscription, s)) {
			this.subscription = s;
			if (subscriptionConsumer != null) {
				try {
					subscriptionConsumer.accept(s);
				}
				catch (Throwable t) {
					Exceptions.throwIfFatal(t);
					s.cancel();
					onError(t);
				}
			}
			else {
				s.request(Long.MAX_VALUE);
			}
		}
	}

	@Override
	public final void onComplete() {
		Subscription s = S.getAndSet(this, Operators.cancelledSubscription());
		if (s == Operators.cancelledSubscription()) {
			return;
		}
		if (completeConsumer != null) {
			try {
				completeConsumer.run();
			}
			catch (Throwable t) {
				Exceptions.throwIfFatal(t);
				onError(t);
			}
		}
	}

	@Override
	public final void onError(Throwable t) {
		Subscription s = S.getAndSet(this, Operators.cancelledSubscription());
		if (s == Operators.cancelledSubscription()) {
			Operators.onErrorDropped(t, this.initialContext);
			return;
		}
		if (errorConsumer != null) {
			errorConsumer.accept(t);
		}
		else {
			Operators.onErrorDropped(Exceptions.errorCallbackNotImplemented(t), this.initialContext);
		}
	}

	@Override
	public final void onNext(T x) {
		try {
			if (consumer != null) {
				consumer.accept(x);
			}
		}
		catch (Throwable t) {
			Exceptions.throwIfFatal(t);
			this.subscription.cancel();
			onError(t);
		}
	}
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적으로 Mono&amp;hellip;subscribe(&amp;hellip;) 로 method chain을 구성하면 생성되는 subscriber 구현체이다.&lt;br /&gt;생성자로 각 signal 처리를 담당하는 consumer 또는 Runnable 받는다.&lt;br /&gt;각 signal에 대응하는 로직이 있으면 실행 및 실행 결과에 대한 에러처리&lt;/blockquote&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;ScalarSubscription&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;publish single element once syncronously&lt;/blockquote&gt;
&lt;pre id=&quot;code_1711430412800&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static final class ScalarSubscription&amp;lt;T&amp;gt; implements Fuseable.SynchronousSubscription&amp;lt;T&amp;gt;, InnerProducer&amp;lt;T&amp;gt; {
	final CoreSubscriber&amp;lt;? super T&amp;gt; actual;
	final T value;
	@Nullable
	final String stepName;
	volatile int once;

	ScalarSubscription(CoreSubscriber&amp;lt;? super T&amp;gt; actual, T value, String stepName) {
		this.value = Objects.requireNonNull(value, &quot;value&quot;);
		this.actual = Objects.requireNonNull(actual, &quot;actual&quot;);
		this.stepName = stepName;
	}

	@Override
	public void cancel() {
		if (once == 0) {
			Operators.onDiscard(value, actual.currentContext());
		}
		ONCE.lazySet(this, 2);
	}
	...

	@Override
	public void request(long n) {
		if (validate(n)) {
			if (ONCE.compareAndSet(this, 0, 1)) {
				Subscriber&amp;lt;? super T&amp;gt; a = actual;
				a.onNext(value);
				if(once != 2) {
					a.onComplete();
				}
			}
		}
	}
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Mono.just 등으로 생성되는 Subscription 중 하나이다.&lt;br /&gt;하나의 element를 subscriber에게 전달하는 구독으로, 생성 시점에 element하나와 subscriber를 받는다.&lt;br /&gt;request 호출 시 가지고 있는 element를 onNext로 내려주고, complete signal을 보낸다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ReactiveStreams&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기존 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;PUSH / POLL 방식의 한계인 subscriber buffer 또는 perodic call 이슈를 Backpressure를 통해 해결&lt;/b&gt;&lt;/span&gt;할 수 있도록 정의한 Interface 및 Rule&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;- Publisher: &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Data 발행에 관심&lt;/b&gt;&lt;/span&gt;을 가지고 있으며, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Subscription을 생성 및 관리&lt;/b&gt;&lt;/span&gt;한다.&lt;br /&gt;&amp;nbsp;- Subscriber: &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Data 소비에 관심&lt;/b&gt;&lt;/span&gt;을 가지고 있으며, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;BackPressure 개념을 도입&lt;/b&gt;&lt;/span&gt;했다. 소비할 element 개수를 Subscription에게 request 하고, onNext, onComplete 등 signal handling function을 Subscription이 호출한다.&lt;br /&gt;&amp;nbsp;- Subscription: &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Pub / Sub의 Flow를 control&lt;/b&gt;&lt;/span&gt; 하는 역할을 가지고 있으며, publisher에게 data를 받을 수 있고, subscriber에게 request 개수대로 onNext 호출을 하는 등 작업을 한다.&lt;br /&gt;&amp;nbsp;- Processor: Publisher + Subscriber&lt;/blockquote&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;ProjectReactor&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ReactiveStreams의 Interface를 구현한 프로젝트&lt;br /&gt;Method Chain으로 Data Pipeline(Stream)을 구성할 수 있도록 구현했다. 실제 동작은 ReactiveStreams의 사상을 따르며, Function들은 비동기(ExecutorService) 위에서 동작한다.&lt;br /&gt;Webflux, RSocket 등 Data Pipeline을 비동기로 구성하는 lib/framework에서 Base로 사용된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;20985&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/reactive-streams/reactive-streams-jvm&quot;&gt;https://github.com/reactive-streams/reactive-streams-jvm&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;21043&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.reactive-streams.org/&quot;&gt;https://www.reactive-streams.org/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;21078&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/reactor/reactor-core&quot;&gt;https://github.com/reactor/reactor-core&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>java</category>
      <category>projectreactor</category>
      <category>publisher</category>
      <category>reactivestreams</category>
      <category>Subscriber</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/87</guid>
      <comments>https://p-bear.tistory.com/87#entry87comment</comments>
      <pubDate>Tue, 26 Mar 2024 20:24:14 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux with EventListener</title>
      <link>https://p-bear.tistory.com/86</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b911gK/btsFPHm1Q8a/gsWRu4GvDKZKyEQDzjAboK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b911gK/btsFPHm1Q8a/gsWRu4GvDKZKyEQDzjAboK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b911gK/btsFPHm1Q8a/gsWRu4GvDKZKyEQDzjAboK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb911gK%2FbtsFPHm1Q8a%2FgsWRu4GvDKZKyEQDzjAboK%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;441&quot; height=&quot;114&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;114&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring framework에서 제공하는 eventListener 기능 설명 (4.2 이상 기준)&lt;br /&gt;Webflux 환경에서 EventListener에 대한 Sample Code 제시&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작원리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring&amp;nbsp;@EventListener&lt;/h3&gt;
&lt;pre id=&quot;code_1710727816440&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Reflective
public @interface EventListener {
  @AliasFor(&quot;classes&quot;)
  Class&amp;lt;?&amp;gt;[] value() default {};

  @AliasFor(&quot;value&quot;)
  Class&amp;lt;?&amp;gt;[] classes() default {};

  String condition() default &quot;&quot;;

  String id() default &quot;&quot;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Method를 target을 하는 Spring Annotation이다.&lt;br /&gt;classes: handle Class 지정&lt;br /&gt;condition: SqEL로 표현되는 조건 String&lt;br /&gt;id: id부여&lt;br /&gt;value == classes&lt;/blockquote&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;ApplicationEventPublisher&lt;/h3&gt;
&lt;pre id=&quot;code_1710727857218&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface ApplicationEventPublisher {
  default void publishEvent(ApplicationEvent event) {
    this.publishEvent((Object)event);
  }

  void publishEvent(Object event);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Functional Interface로 event 발행을 위한 기능을 정의했다.&lt;br /&gt;Spring은 해당 interface의 구현체를 Bean으로 관리하며, 내부 spring event publish 용으로 사용한다.&lt;br /&gt;내부적으로 ApplicationEvent 객체를 통해 전파한다.&lt;/blockquote&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;ApplicationContext&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;482&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ocUNF/btsFPpfiSxk/E4smcHND0AYWyty9jVydaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ocUNF/btsFPpfiSxk/E4smcHND0AYWyty9jVydaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ocUNF/btsFPpfiSxk/E4smcHND0AYWyty9jVydaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FocUNF%2FbtsFPpfiSxk%2FE4smcHND0AYWyty9jVydaK%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;708&quot; height=&quot;482&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;482&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Application이 기동 할 때 생성되는 Context는 기본적으로 ApplicationEventPublisher를 상속받는다.&lt;br /&gt;그 외 EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ResourcePatternResolver 등 interface도 상속받는다.&lt;/blockquote&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;AbstractApplicationContext&lt;/h3&gt;
&lt;pre id=&quot;code_1710727969045&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public void publishEvent(ApplicationEvent event) {
	publishEvent(event, null);
}

protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
	Assert.notNull(event, &quot;Event must not be null&quot;);

	// Decorate event as an ApplicationEvent if necessary
	ApplicationEvent applicationEvent;
	if (event instanceof ApplicationEvent) {
		applicationEvent = (ApplicationEvent) event;
	}
	else {
		applicationEvent = new PayloadApplicationEvent&amp;lt;&amp;gt;(this, event);
		if (eventType == null) {
			eventType = ((PayloadApplicationEvent&amp;lt;?&amp;gt;) applicationEvent).getResolvableType();
		}
	}

	// Multicast right now if possible - or lazily once the multicaster is initialized
	if (this.earlyApplicationEvents != null) {
		this.earlyApplicationEvents.add(applicationEvent);
	}
	else {
		getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
	}

	// Publish event via parent context as well...
	if (this.parent != null) {
		if (this.parent instanceof AbstractApplicationContext) {
			((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
		}
		else {
			this.parent.publishEvent(event);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;EventPublisher의 publishEvent(&amp;hellip;)를 구현한 abstract class이다.&lt;br /&gt;4.2부터 ApplicationEvent를 상속받지 않아도 event를 publish 할 수 있다.&lt;br /&gt;이는 object를 PayloadApplicationEvent로 event(Object)를 한번 감싸기 때문이다.&lt;br /&gt;사용자가 publish한 이벤트는 보통 PayloadApplicationEvent로 multicater의 multicastEvent(&amp;hellip;)에 호출된다.&lt;/blockquote&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;ApplicationEventMulticaster&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tgVi8/btsFQO6K69p/o4eVZzWYvMkf0kUkMsINJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tgVi8/btsFQO6K69p/o4eVZzWYvMkf0kUkMsINJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tgVi8/btsFQO6K69p/o4eVZzWYvMkf0kUkMsINJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtgVi8%2FbtsFQO6K69p%2Fo4eVZzWYvMkf0kUkMsINJK%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;621&quot; height=&quot;309&quot; data-origin-width=&quot;621&quot; data-origin-height=&quot;309&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Application에서 구현된 Listener들을 execute 하는 Interface&lt;br /&gt;일반적으로 SimpleApplicationEventMulticaster 구현체가 사용된다.&lt;/blockquote&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;SimpleApplicationEventMulticaster&lt;/h3&gt;
&lt;pre id=&quot;code_1710728520474&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
	ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
	Executor executor = getTaskExecutor();
	for (ApplicationListener&amp;lt;?&amp;gt; listener : getApplicationListeners(event, type)) {
		if (executor != null) {
			executor.execute(() -&amp;gt; invokeListener(listener, event));
		}
		else {
			invokeListener(listener, event);
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;multicastEvent는 event에 맞는 ApplicationListener를 모두 순회하며 invoke 시킨다.&lt;br /&gt;executor가 있으면 executor를 통해, 없으면 sync하게 invoke 된다.&lt;br /&gt;executor는 AsyncConfigurer + @EnableAsync로 생성할 수 있다.&lt;br /&gt;Listener는 Event class 및 resolver에 따라서 retrive 및 caching 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Webflux&amp;nbsp;Event&amp;nbsp;Sample&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제상황&lt;/h3&gt;
&lt;pre id=&quot;code_1710728782773&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;userService
  .getUser(req)
  .flatMap(userService::changePassword)
  .doOnNext(user -&amp;gt; 
    produceManager
      .send(user)
      .subscribeOn(Schedulers.boundedElastic())
      .subscribe()
  )
  ...&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적으로 Webflux(Projectreactor)에서 별도의 Scheduler로 비동기처리를 위해서는&lt;br /&gt;위와 같이 subscribeOn으로 scheduler를 지정하고 subscribe 별도로 호출한다.&lt;br /&gt;하지만 business logic에 subscribe()를 호출하는 코드는 문제점을 가진다.&lt;br /&gt;1. 가독성도 떨어진다.&lt;br /&gt;2. 비동기로 실행되어, 의도되지 않은 코드에 대해 인지를 못할 수 있다.&lt;br /&gt;3. 코드가 비대 해질수록 subscribe가 증가한다. 그에 따라 debug point가 증가하게 된다.&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Context의 EventListener를 Async 하게 사용한다.&lt;br /&gt;이는 Event에 대한 Subscriber, Subscribe logic, Scheduler 등 비즈니스와 분리할 수 있다.&lt;br /&gt;결론적으로 Event처리 비즈니스의 응집도를 높일 수 있다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Event할당&amp;nbsp;처리를&amp;nbsp;위한&amp;nbsp;Async&amp;nbsp;Executor&amp;nbsp;설정&lt;/h4&gt;
&lt;pre id=&quot;code_1710729014677&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableAsync
public class Config implements AsyncConfigurer {
  @Override
  public Executor getAsyncExecutor() {
    return Executors.newFixedThreadPool(
        Math.max(1, Runtime.getRuntime().availableProcessors()),
        new DefaultThreadFactory(&quot;async-event&quot;));
  }

  @Override
  public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
    return new SimpleAsyncUncaughtExceptionHandler();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적인 processor 기반 fixed thread pool을 만든다.&lt;br /&gt;해당 Executor는 listener를 통한 event의 invoke를 담당한다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Event&amp;nbsp;Handler&amp;nbsp;작성&lt;/h4&gt;
&lt;pre id=&quot;code_1710729040721&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class EventHandler {
  private final WebClient webClient = WebClient.builder().build();

  @Async
  @EventListener
  public void handleEvent(final Event event) throws InterruptedException {
    // 비동기 처리 확인을 위한 임시 sleep
    Thread.sleep(1000);
    Mono.just(event.getPath())
        .doOnNext(path -&amp;gt; log.info(&quot;publish path: {}&quot;, path))
        .flatMap(path -&amp;gt; webClient
            .get()
            .uri(&quot;http://localhost:12345&quot; + path)
            .retrieve()
            .bodyToMono(HashMap.class))
        .subscribe();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Bean에 @Async, @EventListener를 통해 handler method를 작성한다.&lt;br /&gt;Annotation 기반이기 때문에, 상대적으로 자연스럽게 handler method를 관리할 수 있다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Business&amp;nbsp;Example&lt;/h4&gt;
&lt;pre id=&quot;code_1710729064450&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
@Slf4j
public class BusinessService {
  private final ApplicationEventPublisher applicationEventPublisher;

  public void doSomething() {
    Mono.just(new Event(&quot;/1&quot;))
        .doOnNext(applicationEventPublisher::publishEvent)
        .map(ev -&amp;gt; new Event(&quot;/2&quot;))
        .doOnNext(applicationEventPublisher::publishEvent)
        .map(ev -&amp;gt; new Event(&quot;/3&quot;))
        .doOnNext(applicationEventPublisher::publishEvent)
        .map(ev -&amp;gt; new Event(&quot;/4&quot;))
        .doOnNext(applicationEventPublisher::publishEvent)
        .doOnNext(ev -&amp;gt; log.info(&quot;doSomething end&quot;))
        .subscribe();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Log&lt;/h4&gt;
&lt;pre id=&quot;code_1710729079943&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[           main] o.e.eventwebflux.send.BusinessService    : doSomething end
[async-event-3-3] o.e.eventwebflux.send.EventHandler       : publish path: /3
[async-event-3-1] o.e.eventwebflux.send.EventHandler       : publish path: /1
[async-event-3-2] o.e.eventwebflux.send.EventHandler       : publish path: /2
[async-event-3-4] o.e.eventwebflux.send.EventHandler       : publish path: /4&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;상대적으로 깔끔하게 비동기 Event 처리를 구현할 수 있다.&lt;br /&gt;async executor에서 처리됨을 확인할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/21025&quot;&gt;https://github.com/spring-projects/spring-framework/issues/21025&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/21831&quot;&gt;https://github.com/spring-projects/spring-framework/issues/21831&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>async</category>
      <category>EventListener</category>
      <category>eventpublisher</category>
      <category>spring</category>
      <category>webflux</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/86</guid>
      <comments>https://p-bear.tistory.com/86#entry86comment</comments>
      <pubDate>Wed, 20 Mar 2024 20:33:10 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Parallel Consumer</title>
      <link>https://p-bear.tistory.com/85</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R9yhE/btsFQfwMCZT/VYNfAGZdEB4BwSMieYiwFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R9yhE/btsFQfwMCZT/VYNfAGZdEB4BwSMieYiwFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R9yhE/btsFQfwMCZT/VYNfAGZdEB4BwSMieYiwFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR9yhE%2FbtsFQfwMCZT%2FVYNfAGZdEB4BwSMieYiwFk%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;500&quot; height=&quot;250&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;250&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Parallel&amp;nbsp;Consumer란?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Confulent.inc에서 개발한 Open Source로 consumer의 consume이 parallel 하게 동작하도록 구현했다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;주요 목적은 분산 Consume을 위해 partition이 과도하게 늘아나는 비합리성을 해결하기 위함이다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Before&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1075&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0RTzd/btsFP4CjXdB/NnT02a3abWSTZrQiBYAwRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0RTzd/btsFP4CjXdB/NnT02a3abWSTZrQiBYAwRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0RTzd/btsFP4CjXdB/NnT02a3abWSTZrQiBYAwRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0RTzd%2FbtsFP4CjXdB%2FNnT02a3abWSTZrQiBYAwRk%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;1075&quot; height=&quot;594&quot; data-origin-width=&quot;1075&quot; data-origin-height=&quot;594&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기존 Consumer의 구조는 Consume의 주체는 Single Thread로 동작한다.&lt;br /&gt;1. Consumer는 설정된 size 만큼 message를 읽는다(consume).&lt;br /&gt;2. 설정된 offset 전략에 따라서 모든 message 처리가 완료될 때 까지 blocking 한다.&lt;br /&gt;위 그림은 consume 프로세스 중 delay 될 수 있는 상황을 도식화했다.&lt;br /&gt;4번 message가 web request 이슈로 delay 되면 전체 consume 퍼포먼스가 낮아진다.&lt;/blockquote&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;After&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8G9CG/btsFRC5BhMc/yN0KVghjPgakcsX0MVPtWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8G9CG/btsFRC5BhMc/yN0KVghjPgakcsX0MVPtWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8G9CG/btsFRC5BhMc/yN0KVghjPgakcsX0MVPtWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8G9CG%2FbtsFRC5BhMc%2FyN0KVghjPgakcsX0MVPtWk%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;862&quot; height=&quot;518&quot; data-origin-width=&quot;862&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Parallel Consumer는 위 상황을 해소하기 위해 개발되었다.&lt;br /&gt;목표는 consume을 parallel 하게 동작하여 하나의 파티션에 여러 consumer가 동작하는 효과를 보는 것이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Overview&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nv8YW/btsFQv7aKXw/qQA49r1kHZbrnuKEKJTDL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nv8YW/btsFQv7aKXw/qQA49r1kHZbrnuKEKJTDL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nv8YW/btsFQv7aKXw/qQA49r1kHZbrnuKEKJTDL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnv8YW%2FbtsFQv7aKXw%2FqQA49r1kHZbrnuKEKJTDL0%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;1028&quot; height=&quot;480&quot; data-origin-width=&quot;1028&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;- consumer가 consume을 병렬로 처리할 수 있도록 Thread Pool을 관리한다.&lt;br /&gt;&amp;nbsp;- offset 관리를 위해 Offset Storage에 완료된 offset이 update 된다.&lt;br /&gt;&amp;nbsp;- Offset Commiter가 비동기로 완료된 offset을 선정하여 commit 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ParallelConsumerOptions&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Order&lt;/h3&gt;
&lt;pre id=&quot;code_1710726893310&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum ProcessingOrder {
    UNORDERED,
    PARTITION,
    KEY
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ParallelConsumer는 순서보장 관련하여 3가지 옵션을 지원한다.&lt;br /&gt;&amp;nbsp;- UNORDERED: 순서보장 없이 모든 Key에 대해서 shard를 생성&lt;br /&gt;&amp;nbsp;- PARTITION: 파티션 단위로 shard를 생성, 파티션단위로 순서보장&lt;br /&gt;&amp;nbsp;- KEY: KEY 단위로 shard를 생성하여 key별 순서를 보장&lt;br /&gt;// default order: KEY&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;Shard란?&lt;/b&gt;&lt;br /&gt;Local에서 생성되는 Queue로 worker에게 할당할 work를 큐잉 한다.&lt;br /&gt;자세한 사항은 후술&lt;/blockquote&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;CommitMode&lt;/h3&gt;
&lt;pre id=&quot;code_1710726953107&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum CommitMode {
    PERIODIC_TRANSACTIONAL_PRODUCER,
    PERIODIC_CONSUMER_SYNC,
    PERIODIC_CONSUMER_ASYNCHRONOUS
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CommitManager가 수행할 commit 전략에 대한 설정&lt;br /&gt;&amp;nbsp;- PERIODIC_TRANSACTIONAL_PRODUCER: Streams 등 consume-&amp;gt;produce에 transaction을 지원하는 commit 전략&lt;br /&gt;&amp;nbsp;- PERIODIC_CONSUMER_SYNC: 주기적으로 sync 하게 commit을 수행&lt;br /&gt;&amp;nbsp;- PERIODIC_CONSUMER_ASYNCHRONOUS: async 하게 commit 요청을 보냄, 속도가 제일 빠름&lt;br /&gt;// default commitMode: PERIODIC_CONSUMER_ASYNCHRONOUS&lt;br /&gt;// default auto commit interval ms: 5000ms&lt;/blockquote&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;InvalidOffsetMetadataHandlingPolicy&lt;/h3&gt;
&lt;pre id=&quot;code_1710726993355&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum InvalidOffsetMetadataHandlingPolicy {
    FAIL,
    IGNORE
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;offset handling 도중 에러가 발생한 상황의 정책 설정&lt;br /&gt;&amp;nbsp;- FAIL: 발생 시 shutdown&lt;br /&gt;&amp;nbsp;- IGNORE: 발생 시 무시&lt;br /&gt;// default: FAIL&lt;/blockquote&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;pre id=&quot;code_1710727020043&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final Duration sendTimeout = Duration.ofSeconds(10);
private final Duration offsetCommitTimeout = Duration.ofSeconds(10);
private final Integer batchSize = 1;
private final Duration thresholdForTimeSpendInQueueWarning = Duration.ofSeconds(10);
private final int maxFailureHistory = 10;
public final Duration shutdownTimeout = Duration.ofSeconds(10);
public final Duration drainTimeout = Duration.ofSeconds(30);
public final int messageBufferSize;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기타 적용 가능한 설정들&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작원리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;전체적인 동작과정은&lt;br /&gt;1. Poll Task가 executor로 pooling 되어 broker로부터 record를 poll 함&lt;br /&gt;2. poll 된 record를 mailbox(BlockingQueue)로 add&lt;br /&gt;3. control Task가 executor로 pooling 되어 commit 트리거 및 userFunction submit을 담당&lt;br /&gt;4. OffsetCommitter가 offset처리를 담당&lt;br /&gt;5. userFunction이 recordContext와 함께 처리됨 (workerThreadPool)&lt;/blockquote&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;ParallelConsumer&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYRMoW/btsFQvlKuhC/TxDwG2S0gKUzkBxNl7fDqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYRMoW/btsFQvlKuhC/TxDwG2S0gKUzkBxNl7fDqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYRMoW/btsFQvlKuhC/TxDwG2S0gKUzkBxNl7fDqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYRMoW%2FbtsFQvlKuhC%2FTxDwG2S0gKUzkBxNl7fDqK%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;1008&quot; height=&quot;655&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;라이브러리의 핵심이 되는 interface이다.&lt;br /&gt;일반적으로 사용자는 ParallelStreamProcessor의 poll 메서드를 통해 interact 한다.&lt;/blockquote&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;Setup&amp;nbsp;Parallel&amp;nbsp;Consumer&lt;/h3&gt;
&lt;pre id=&quot;code_1710727131096&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ParallelStreamProcessor&amp;lt;String, String&amp;gt; setupParallelConsumer() {
    Consumer&amp;lt;String, String&amp;gt; kafkaConsumer = getKafkaConsumer();
    Producer&amp;lt;String, String&amp;gt; kafkaProducer = getKafkaProducer();

    var options = ParallelConsumerOptions.&amp;lt;String, String&amp;gt;builder()
            .ordering(KEY)
            .maxConcurrency(1000)
            .consumer(kafkaConsumer)
            .producer(kafkaProducer)
            .build();

    ParallelStreamProcessor&amp;lt;String, String&amp;gt; eosStreamProcessor =
            ParallelStreamProcessor.createEosStreamProcessor(options);

    eosStreamProcessor.subscribe(of(inputTopic));

    return eosStreamProcessor;
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;공식 parallelConsumer Setup example이다.&lt;br /&gt;일반적으로 ParallelEoSStreamProcessor.class를 사용하며, Reactor 환경에서는 ReactorProcessor.class를 생성하여 사용한다.&lt;br /&gt;producer는 생략 가능하며, 순수 consumer로 사용할 수 있다.&lt;/blockquote&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;ParallelEoSStreamProcessor.class&amp;nbsp;&amp;rarr;&amp;nbsp;poll(&amp;hellip;)&lt;/h3&gt;
&lt;pre id=&quot;code_1710727164293&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public void poll(Consumer&amp;lt;PollContext&amp;lt;K, V&amp;gt;&amp;gt; usersVoidConsumptionFunction) {
    Function&amp;lt;PollContextInternal&amp;lt;K, V&amp;gt;, List&amp;lt;Object&amp;gt;&amp;gt; wrappedUserFunc = (context) -&amp;gt; {
        log.trace(&quot;asyncPoll - Consumed a consumerRecord ({}), executing void function...&quot;, context);

        carefullyRun(usersVoidConsumptionFunction, context.getPollContext());

        log.trace(&quot;asyncPoll - user function finished ok.&quot;);
        return UniLists.of(); // user function returns no produce records, so we satisfy our api
    };
    Consumer&amp;lt;Object&amp;gt; voidCallBack = ignore -&amp;gt; log.trace(&quot;Void callback applied.&quot;);
    supervisorLoop(wrappedUserFunc, voidCallBack);
}

public static &amp;lt;PARAM&amp;gt; void carefullyRun(Consumer&amp;lt;PARAM&amp;gt; wrappedFunction, PARAM userFuncParam) {
    try {
        wrappedFunction.accept(userFuncParam);
    } catch (Throwable e) {
        throw new ExceptionInUserFunctionException(MSG, e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본 구현체인 ParallelEoSStreamProcessor의 poll 메서드의 코드이다.&lt;br /&gt;PollContext의 consumer를 parameter로 받고 wrapping 하여 supervisorLoop() 함수로 execute 시킨다.&lt;/blockquote&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;PollContext&amp;lt;K,&amp;nbsp;V&amp;gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1710727191208&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PollContext&amp;lt;K, V&amp;gt; implements Iterable&amp;lt;RecordContext&amp;lt;K, V&amp;gt;&amp;gt; {
  protected Map&amp;lt;TopicPartition, Set&amp;lt;RecordContextInternal&amp;lt;K, V&amp;gt;&amp;gt;&amp;gt; records = new HashMap&amp;lt;&amp;gt;();
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;PollContext는 한 번에 poll 한 record를 context와 함께 가진 클래스이다.&lt;br /&gt;사용자는 poll 정책에 따라서 record 1개 또는 여러 개 받아서 처리할 수 있다.&lt;/blockquote&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;AbstractParallelEoSStreamProcessor.class&amp;nbsp;&amp;rarr;&amp;nbsp;supervisorLoop(&amp;hellip;)&lt;/h3&gt;
&lt;pre id=&quot;code_1710727212952&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protected &amp;lt;R&amp;gt; void supervisorLoop(Function&amp;lt;PollContextInternal&amp;lt;K, V&amp;gt;, List&amp;lt;R&amp;gt;&amp;gt; userFunctionWrapped, Consumer&amp;lt;R&amp;gt; callback) {
    ...
    // broker poll subsystem
    brokerPollSubsystem.start(options.getManagedExecutorService());

    ExecutorService executorService;
    try {
        executorService = InitialContext.doLookup(options.getManagedExecutorService());
    } catch (NamingException e) {
        ...
    }


    // run main pool loop in thread
    Callable&amp;lt;Boolean&amp;gt; controlTask = () -&amp;gt; {
        ...
        while (state != CLOSED) {
            log.debug(&quot;Control loop start&quot;);
            try {
                controlLoop(userFunctionWrapped, callback);
            } catch (InterruptedException e) {
                ...
            }
        }
        log.info(&quot;Control loop ending clean (state:{})...&quot;, state);
        return true;
    };
    Future&amp;lt;Boolean&amp;gt; controlTaskFutureResult = executorService.submit(controlTask);
    this.controlThreadFuture = Optional.of(controlTaskFutureResult);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 함수는 poll task와 handle with userFunction task를 execute 하는 기능을 가진다.&lt;br /&gt;이는&lt;br /&gt;1. start poll task&lt;br /&gt;2. start control(userFunction) task&lt;br /&gt;&amp;nbsp; - executorService를 lookup 하거나 생성한다. (정책에 따라서)&lt;br /&gt;&amp;nbsp; - controlLoop() 함수를 호출하는 task(callable)을 만든다.&lt;br /&gt;&amp;nbsp; - executorService로 submit 한다.&lt;br /&gt;로 요약할 수 있다.&lt;/blockquote&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;Poll&amp;nbsp;Task&amp;nbsp;관련&lt;/h3&gt;
&lt;pre id=&quot;code_1710727278656&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;BrokerPollSystem.class
public void start(String managedExecutorService) {
    ...
    Future&amp;lt;Boolean&amp;gt; submit = executorService.submit(this::controlLoop);
    ...
}

private boolean controlLoop() throws TimeoutException, InterruptedException {
    ...
    try {
        while (runState != CLOSED) {
            handlePoll();
            ...
        }
        ...
}

private void handlePoll() {
    ...
    if (runState == RUNNING || runState == DRAINING) { // if draining - subs will be paused, so use this to just sleep
        var polledRecords = pollBrokerForRecords();
        ...

        if (count &amp;gt; 0) {
            ...
            pc.registerWork(polledRecords);
        }
    }
}

private EpochAndRecordsMap&amp;lt;K, V&amp;gt; pollBrokerForRecords() {
    ...
    ConsumerRecords&amp;lt;K, V&amp;gt; poll = consumerManager.poll(thisLongPollTimeout);

    return new EpochAndRecordsMap&amp;lt;&amp;gt;(poll, wm.getPm());
}

// private final BlockingQueue&amp;lt;ControllerEventMessage&amp;lt;K, V&amp;gt;&amp;gt; workMailBox = new LinkedBlockingQueue&amp;lt;&amp;gt;();
public void registerWork(EpochAndRecordsMap&amp;lt;K, V&amp;gt; polledRecords) {
    ...
    workMailBox.add(ControllerEventMessage.of(polledRecords));
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;poll task는 task별로 mailbox라는 Queue에 poll 한 결과(recordContext)를 add 하는 작업이다.&lt;br /&gt;mailbox는 Threadsafe 한 blocking queue이며, control task에서 꺼내서 userFunction에서 사용된다.&lt;/blockquote&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;AbstractParallelEoSStreamProcessor.class&amp;nbsp;&amp;rarr;&amp;nbsp;controlLoop(...)&lt;/h3&gt;
&lt;pre id=&quot;code_1710727308654&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protected &amp;lt;R&amp;gt; void controlLoop(Function&amp;lt;PollContextInternal&amp;lt;K, V&amp;gt;, List&amp;lt;R&amp;gt;&amp;gt; userFunction,
                                   Consumer&amp;lt;R&amp;gt; callback) throws TimeoutException, ExecutionException, InterruptedException {
    ...
    if (shouldTryCommitNow) {
        // offsets will be committed when the consumer has its partitions revoked
        commitOffsetsThatAreReady();
    }

    // distribute more work
    retrieveAndDistributeNewWork(userFunction, callback);

    // run call back
    log.trace(&quot;Loop: Running {} loop end plugin(s)&quot;, controlLoopHooks.size());
    this.controlLoopHooks.forEach(Runnable::run);

    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;control task는 크게 3가지 역할을 가진다.&lt;br /&gt;1. offset처리가 필요한지 체크해서 OffsetCommitter(BrokerPollSystem)에게 commit 트리거&lt;br /&gt;2. userFunction 실행&lt;br /&gt;3. callback 처리&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;성능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;performance&amp;nbsp;measurement&amp;nbsp;by&amp;nbsp;Confluent&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;- A random processing time between 0 and 5ms &lt;br /&gt;&amp;nbsp;- 10,000 messages to process &lt;br /&gt;&amp;nbsp;- A single partition (simplifies comparison - a topic with 5 partitions is the same as 1 partition with a keyspace of 5) &lt;br /&gt;&amp;nbsp;- Default ParallelConsumerOptions &lt;br /&gt;&amp;nbsp; &amp;nbsp;- maxConcurrency = 100 &lt;br /&gt;&amp;nbsp; &amp;nbsp;- numberOfThreads = 16&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Ordering&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Number of keys&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Duration&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Note&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Partition&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;20 (not relevant)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;22.221s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&lt;span&gt;&lt;span&gt;This is the same as a single partition with a single normal serial consumer, as we can see: 2.5ms avg processing time * 10,000 msg / 1000ms = ~25s.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;1&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;26.743s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&lt;span&gt;&lt;span&gt;Same as above&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;2&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;13.576s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;5.916s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;10&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;3.310s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;20&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;2.242s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;2.204s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;100&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;2.178s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;1,000&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;2.056s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Key&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;10,000&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;2.128s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&lt;span&gt;&lt;span&gt;As key space is t he same as the number of messages, this is similar (but restricted by max concurrency settings) as having a &lt;b&gt;&lt;span&gt;single consumer&lt;/span&gt;&lt;/b&gt; instance and &lt;b&gt;&lt;span&gt;partition&lt;/span&gt;&lt;/b&gt; &lt;span&gt;per key&lt;/span&gt;. 10,000 msgs * avg processing time 2.5ms = ~2.5s.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.3488%;&quot;&gt;&lt;span&gt;&lt;span&gt;Unordered&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;span&gt;&lt;span&gt;20 (not relevant)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.76744%;&quot;&gt;&lt;span&gt;&lt;span&gt;2.829s&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.4651%;&quot;&gt;&lt;span&gt;&lt;span&gt;As there is no order restriction, this is similar (but restricted by max concurrency settings) as having a &lt;b&gt;&lt;span&gt;single consumer&lt;/span&gt;&lt;/b&gt; instance and &lt;b&gt;&lt;span&gt;partition&lt;/span&gt;&lt;/b&gt; &lt;span&gt;per key&lt;/span&gt;. 10,000 msgs * avg processing time 2.5ms = ~2.5s.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Key 1개 + order == Single Thread이다.&lt;br /&gt;key의 개수가 Thread Count(16)까지 유의미한 처리 속도를 보이며, 그 이후는 큰 차이를 보이지 않는다.&lt;br /&gt;사실상 Key &amp;gt;= Thread Count 인 경우와 unordered는 성능 향상을 보이며, 동일한 퍼포먼스를 보인다고 볼 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;performance measurement by Naver&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KSVmC/btsFOYI1RJk/MAZMRWehtMePjGeYBSCDl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KSVmC/btsFOYI1RJk/MAZMRWehtMePjGeYBSCDl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KSVmC/btsFOYI1RJk/MAZMRWehtMePjGeYBSCDl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKSVmC%2FbtsFOYI1RJk%2FMAZMRWehtMePjGeYBSCDl0%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;861&quot; height=&quot;232&quot; data-origin-width=&quot;861&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;성능 측정 시 2 consumer(parallel), 2 partition이 8 consumer(일반), 8 partition과 유사한 성능을 보였다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Parallel Consumer는 기존 Consumer에 병렬 처리를 더한 구현체이다.&lt;br /&gt;유의미한 성능 향상을 보이지만, 관리포인트(config)가 늘어난다.&lt;br /&gt;기본적으로 partition의 key에 대한 예측 및 관리가 필요하다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;정리하자면&lt;br /&gt;&amp;nbsp;- 도입 시 key 예측 및 추가적인 config 등 관리포인트가 늘어난다.&lt;br /&gt;&amp;nbsp;- 고부하 상황이 아니라면 유의미한 성능 향상을 기대할 수 없다. &lt;br /&gt;&amp;nbsp;- Conumser 교체, 추가적인 key 관리 (estimation)만 하면 되기 때문에 추후 도입이 용이하다.&lt;br /&gt;&amp;nbsp; &amp;nbsp;// 메인 비즈니스의 변경 없이 도입 가능&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;따라서 partition을 늘리기 부담스러운 고부하 상황에서 도입하는 것이 타당할 것으로 판단된다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/confluentinc/parallel-consumer&quot;&gt;https://github.com/confluentinc/parallel-consumer&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/7181840&quot;&gt;https://d2.naver.com/helloworld/7181840&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;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;</description>
      <category>개발 일지</category>
      <category>consumer</category>
      <category>Kafka</category>
      <category>parallel consumer</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/85</guid>
      <comments>https://p-bear.tistory.com/85#entry85comment</comments>
      <pubDate>Tue, 19 Mar 2024 18:06:34 +0900</pubDate>
    </item>
    <item>
      <title>TURN Protocol (+ STUN Message)</title>
      <link>https://p-bear.tistory.com/84</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;346&quot; data-origin-height=&quot;322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YQxMS/btsFQrKojME/HR8u8lO61jlKaD0qBeLn2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YQxMS/btsFQrKojME/HR8u8lO61jlKaD0qBeLn2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YQxMS/btsFQrKojME/HR8u8lO61jlKaD0qBeLn2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYQxMS%2FbtsFQrKojME%2FHR8u8lO61jlKaD0qBeLn2k%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;346&quot; height=&quot;322&quot; data-origin-width=&quot;346&quot; data-origin-height=&quot;322&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TURN 이란?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Traversal Using Relays around NAT의 약자이다.&lt;br /&gt;TURN protocol은 STUN 프로토콜의 확장으로 Direct Communication이 불가 한 상황에서, relay를 통해 두 연결을 제공해주는 역할을 가진다. &lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;NAT 뒤의 HOST로 hole punching을 통해 통신이 가능하다. 하지만, 두 host 모두 NAT 뒤에 있으며, 적절한 설정이 없으면 이 또한 불가 하다.&lt;br /&gt;또는 엔터프라이즈 환경에서 UDP 통신 자체가 불가 할 수 도 있다. &lt;br /&gt;이를 해결하기 위해, TURN 서버에서 두 client의 통신을 relay 해주며, 통신이 가능하게 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Overview&lt;/h3&gt;
&lt;pre id=&quot;code_1710725982605&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;                                    Peer A
                                    Server-Reflexive    +---------+
                                    Transport Address   |         |
                                    192.0.2.150:32102   |         |
                                        |              /|         |
                      TURN              |            / ^|  Peer A |
   Client's           Server            |           /  ||         |
   Host Transport     Transport         |         //   ||         |
   Address            Address           |       //     |+---------+
198.51.100.2:49721  192.0.2.15:3478     |+-+  //     Peer A
           |            |               ||N| /       Host Transport
           |   +-+      |               ||A|/        Address
           |   | |      |               v|T|     203.0.113.2:49582
           |   | |      |               /+-+
+---------+|   | |      |+---------+   /              +---------+
|         ||   |N|      ||         | //               |         |
| TURN    |v   | |      v| TURN    |/                 |         |
| Client  |----|A|-------| Server  |------------------|  Peer B |
|         |    | |^      |         |^                ^|         |
|         |    |T||      |         ||                ||         |
+---------+    | ||      +---------+|                |+---------+
               | ||                 |                |
               | ||                 |                |
               +-+|                 |                |
                  |                 |                |
                  |                 |                |
         Client's                   |             Peer B
         Server-Reflexive     Relayed             Transport
         Transport Address    Transport Address   Address
         192.0.2.1:7000       192.0.2.15:50000    192.0.2.210:49191&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 그림은 TURN Server를 통한 Client들 간의 communication을 그린 것 이다.&lt;br /&gt;TURN Client는 Peer A 또는 Peer B와 통신이 하고 싶은 상황이다.&lt;br /&gt;TURN Server는 Public 망에 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;STUN Message&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;STUN Message는 STUN Protocol에서 사용되는 message이다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;TURN은 STUN Message를 확장한 프로토콜&lt;/b&gt;&lt;/span&gt;이기에, 기본적으로 STUN Message 구조를 따른다.&lt;/blockquote&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;STUN Header&lt;/h3&gt;
&lt;pre id=&quot;code_1710726046483&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0|     STUN Message Type     |         Message Length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Magic Cookie                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                     Transaction ID (96 bits)                  |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;STUN Message의 Header는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;20 Byte&lt;/b&gt;&lt;/span&gt;로 구성된다.&lt;br /&gt;STUN Message Type: message의 class 정보이며 request, success response, failuser response, indication 등 있으며, TURN 등 확장 프로토콜에서 추가되기도 한다.&lt;br /&gt;Message Length: Header를 제외한 Message의 size&lt;br /&gt;Transaction ID: STUN은 request &amp;harr;︎ response간 transaction을 지원한다. 해당 ID를 올릴 수 있는 부분&lt;/blockquote&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;STUN Message Type&lt;/h3&gt;
&lt;pre id=&quot;code_1710726077396&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; 0                 1
 2  3  4 5 6 7 8 9 0 1 2 3 4 5
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+
|M |M |M|M|M|C|M|M|M|C|M|M|M|M|
|11|10|9|8|7|1|6|5|4|0|3|2|1|0|
+--+--+-+-+-+-+-+-+-+-+-+-+-+-+&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Stun의 MessageType은 14bit로 표현된다.&lt;br /&gt;앞 두 bit는 무조건 00이며(prefix, not message Type), Class + Method로 구성되어 있다.&lt;br /&gt;Class는 8, 12 번째 bit이며, 나머지는 Method이다.&lt;br /&gt;Class: 0b00=request, 0b01=indication, 0b10=successResponse, 0b11=errorResponse&lt;/blockquote&gt;
&lt;pre id=&quot;code_1710726103898&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;EX) 
TURN Protocol Create Permission
Class: request(0b00) or successResponse(0b10) or errorResponse(0b11)
Method: 0x008

request: 0000 0000 0000 1000 = 0x0008
sucessResponse: 0000 0001 0000 1000 = 0x0108
errorResponse: 0000 0001 0001 1000 = 0x0118&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Protocol&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Method&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Bytes&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Support Class&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;(Reserved)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;0x000&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Binding&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;0x001&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;(Reserved; was SharedSecret)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;0x002&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Allocate&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;0x003&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;req / res&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Refresh&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;0x004&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;req / res&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Send&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;0x006&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;indication&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Data&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;0x007&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;indication&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;CreatePermission&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;0x008&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;req / res&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;ChannelBind&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;0x009&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;req / res&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;STUN Attributes&lt;/h3&gt;
&lt;pre id=&quot;code_1710726194767&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Type                  |            Length             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Value (variable)                ....
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Type: Attribute의 구분으로 16bit이다.&lt;br /&gt;Length: value의 length in bytes로 16bit 이다.&lt;br /&gt;value: Attribute의 값&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style6&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Protocol&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Bytes&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;MAPPED-ADDRESS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0001&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;reflexive transport address of the client&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;USERNAME&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0006&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;identifies the &lt;b&gt;&lt;span&gt;username and password&lt;/span&gt;&lt;/b&gt; &lt;b&gt;&lt;span&gt;combination&lt;/span&gt;&lt;/b&gt; used in the message-integrity&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;MESSAGE-INTEGRITY&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0008&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;contains an &lt;b&gt;&lt;span&gt;HMAC-SHA1&lt;/span&gt;&lt;/b&gt; [RFC2104] of the STUN message&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;ERROR-CODE&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0009&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;errorCode: 300 ~ 699 + &amp;alpha;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;300: Try Alternate&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;400: Bad Request&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;401: Unauthorized&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;420: Unknown Attribute&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;438: Stale Nonce&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;500: Server Error&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;UNKNOWN-ATTRIBUTES&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x000A&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;errorResponse&lt;/span&gt;에서만 사용되는&lt;span&gt; type&lt;/span&gt;으로&lt;span&gt;, &lt;/span&gt;서버가 이해하지 못한&lt;span&gt; attribute&lt;/span&gt;를 뜻함&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;REALM&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0014&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;Attribute&lt;/span&gt;가&lt;span&gt; request&lt;/span&gt;에 있는 경우&lt;span&gt; long-term credential&lt;/span&gt;이 사용되고 있음을 나타내며&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;error response&lt;/span&gt;에 있다면&lt;span&gt; client&lt;/span&gt;에게&lt;span&gt; long-term credential&lt;/span&gt;이 필요함을 나타냄&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;NONCE&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0015&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;128 character&lt;/span&gt;보다 적은 임의의 값&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;XOR-MAPPED-ADDRESS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0020&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;MAPPED-ADDRESS&lt;/span&gt;와 동일하며&lt;span&gt;, &lt;/span&gt;난독화 됨&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;SOFTWARE&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x8022&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;textual description of the software&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;ALTERNATE-SERVER&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x8023&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;alternate transport address identifying a different STUN server that the STUN client should try&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;FINGERPRINT&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x8028&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;use for identifier&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;CHANNEL-NUMBER&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x000C&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;channel&lt;/span&gt;을 지정하기 위한 값&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;LIFETIME&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x000D&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;duration of allocation&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;XOR-PEER-ADDRESS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0012&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;난독화된 상대의&lt;span&gt; address&lt;/span&gt;를 의미&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;DATA&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0013&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;data&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;XOR-RELAYED-ADDRESS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0016&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;Allocate Response&lt;/span&gt;로 전달되는&lt;span&gt; allocated&lt;/span&gt;된&lt;span&gt; address&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;REQUESTED-ADDRESS-FAMILY&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0017&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;IPv4: 0x01&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;IPv6: 0x02&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;EVEN-PORT&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0018&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;relay port&lt;/span&gt;를&lt;span&gt; even&lt;/span&gt;으로 요청&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;REQUESTED-TRANSPORT&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0019&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;protocol&lt;/span&gt;을 지정하는&lt;span&gt; attribute&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;(&lt;a href=&quot;https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml&quot;&gt;https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml&lt;/a&gt;)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span data-colorid=&quot;ngagiaxmza&quot;&gt;&lt;span&gt;17 (User Datagram Protocol)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;로 고정&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;DONT-FRAGMENT&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x001A&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;Allocate&lt;/span&gt;시 보내는&lt;span&gt; attribute&lt;/span&gt;로&lt;span&gt; peer&lt;/span&gt;로 데이터 전달 시&lt;span&gt; fragment&lt;/span&gt;를 하지 말라는 의미&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;value&lt;/span&gt;는 없음&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;RESERVATION-TOKEN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0022&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;Server&lt;/span&gt;에서 예비로 남긴&lt;span&gt; relay address&lt;/span&gt;를 식별하는 토큰&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;allocation&lt;/span&gt;에서 사용될 수 있음&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;ADDITIONAL-ADDRESS-FAMILY&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x8000&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;REQUESTED-ADDRESS-FAMILY&lt;/span&gt;의 확장&lt;span&gt;, allocate&lt;/span&gt;에서 사용될 수 있음&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;ADDRESS-ERROR-CODE&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x8001&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;address&lt;/span&gt;관련&lt;span&gt; error code&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;ICMP&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x8004&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;UDP Packets&lt;/span&gt;이 사라진 이유를 서버에서 알리기 위한&lt;span&gt; Attribute&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;(Reserved)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0000&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;-&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;(Reserved; was RESPONSE-ADDRESS)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0002&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;for legacy protocol&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;(Reserved; was CHANGE-ADDRESS)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0003&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;for legacy protocol&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;(Reserved; was SOURCE-ADDRESS)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0004&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;for legacy protocol&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;(Reserved; was CHANGED-ADDRESS)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0005&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;for legacy protocol&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;(Reserved; was PASSWORD)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0007&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;for legacy protocol&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;STUN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;(Reserved; was REFLECTED-FROM)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x000B&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;for legacy protocol&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;Reserved (was BANDWIDTH)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0010&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;for legacy protocol&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 9.53488%;&quot;&gt;&lt;span&gt;&lt;span&gt;TURN&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 34.5349%;&quot;&gt;&lt;span&gt;&lt;span&gt;Reserved (was TIMER-VAL)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 8.60465%;&quot;&gt;&lt;span&gt;&lt;span&gt;0x0021&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.093%;&quot;&gt;&lt;span&gt;&lt;span&gt;for legacy protocol&lt;/span&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sample&amp;nbsp;TURN&amp;nbsp;Sequence&amp;nbsp;Diagram&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;1500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFNu74/btsFTY7V6r4/CnxKbV1eT8vCyKkKagwihk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFNu74/btsFTY7V6r4/CnxKbV1eT8vCyKkKagwihk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFNu74/btsFTY7V6r4/CnxKbV1eT8vCyKkKagwihk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFNu74%2FbtsFTY7V6r4%2FCnxKbV1eT8vCyKkKagwihk%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;810&quot; height=&quot;1500&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;1500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Client는 TURN Server로 Allocate를 진행하는 참여자이며 Peer는 TURN 서버에서 메시지 전송이 가능한 참여자이다.&lt;br /&gt;따라서 Peer는 public 망에 있거나, 이전에 Allocate 된 상태로 보면 된다.&lt;br /&gt;allocate를 포함하여 대부분의 메시지에 적용되는 인증은 같은 방식으로 진행한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적인 Flow는 Allocate &amp;rarr; (Permission) &amp;rarr; Send data 이며 Send data는 indicate와 channel 방식이 있다.&lt;br /&gt;Client는 Allocate를 통해 자신에게 udp 전송이 가능하도록 할당을 받는다.&lt;br /&gt;이후 peer등 다른 참여자는 allocate된 해당 address로 data를 전송하면 TURN Server에서 allocate 된 정보를 바탕으로 Client에게 메시지를 전송한다.&lt;br /&gt;indicate는 메시지마다 peer에 대한 정보를 같이 보내는 방식&lt;br /&gt;channel은 사전에 channel number와 peer를 mapping하여, channel number로만 data를 전송하는 방식이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Appendix:&amp;nbsp;UDP&amp;nbsp;Hole&amp;nbsp;Punching&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Hole Pubching은 NAT 뒤의 client들의 peer to peer가 가능하도록 하는 방법 중 하나이다.&lt;br /&gt;이는 NAT 뒤의 client끼리 p2p 통신을 시작할 수 없기 때문에 필요하다.&lt;br /&gt;Hole Pubching 를 위해서는 NAT 방식이 EIM(Endpoint-Independent Mapping)일 때 이상적으로 동작한다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Under EIM-NAT&lt;/h3&gt;
&lt;pre id=&quot;code_1710726414849&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;                                 Server S
                             192.0.2.128:20001
                                     |
        +----------------------------+----------------------------+
        | ^ Registry Session(A-S) ^     ^ Registry Session(B-S) ^ |
        | | 192.0.2.128:20001     |     |  192.0.2.128:20001    | |
        | | 192.0.2.1:62000       |     |  192.0.2.254:31000    | |
        |                                                         |
        | ^ P2P Session (A-B)     ^     ^  P2P Session (B-A)    ^ |
        | | 192.0.2.254:31000     |     |  192.0.2.1:62000      | |
        | | 192.0.2.1:62000       |     |  192.0.2.254:31000    | |
        |                                                         |
      +--------------+                                 +--------------+
      | 192.0.2.1    |                                 | 192.0.2.254  |
      |              |                                 |              |
      | EIM-NAT A    |                                 | EIM-NAT B    |
      +--------------+                                 +--------------+
        |                                                         |
        | ^ Registry Session(A-S) ^     ^ Registry Session(B-S) ^ |
        | |  192.0.2.128:20001    |     |  192.0.2.128:20001    | |
        | |     10.0.0.1:1234     |     |     10.1.1.3:1234     | |
        |                                                         |
        | ^ P2P Session (A-B)     ^     ^  P2P Session (B-A)    ^ |
        | |  192.0.2.254:31000    |     |  192.0.2.1:62000      | |
        | |     10.0.0.1:1234     |     |     10.1.1.3:1234     | |
        |                                                         |
     Client A                                                 Client B
     10.0.0.1:1234                                        10.1.1.3:1234&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;두 Client가 서로 다른 EIM-NAT 뒤에 있다.&lt;br /&gt;ClientA &amp;harr;︎ ClientB는 직접통신은 불가하며, NAT를 거쳐 통신을 하고자 한다.&lt;br /&gt;1. ClientA, B 모두 Server로 Session을 등록하는 과정을 갖는다.&lt;br /&gt;2. EIM-NAT는 Endpoint와 상관없이 각각 192.0.2.1:62000, 192.0.2.254:31000을 ClientA, B와 매핑한다.&lt;br /&gt;3. ClientA, B 모두 Server에게 상대의 NAT public IP를 쿼리한다.&lt;br /&gt;4. ClientA, B 서로 Server를 통해 만든 NAT Table의 포트로 UDP 통신을 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;11358&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8656&quot;&gt;https://datatracker.ietf.org/doc/html/rfc8656&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;11405&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc5389&quot;&gt;https://datatracker.ietf.org/doc/html/rfc5389&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;11452&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc5128#section-3.3&quot;&gt;https://datatracker.ietf.org/doc/html/rfc5128#section-3.3&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;11511&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.netmanias.com/ko/post/blog/5833/nat-network-protocol/nat-behavior-requirements-for-unicast-udp-rfc-4787-part-1-mapping-behavior&quot;&gt;https://www.netmanias.com/ko/post/blog/5833/nat-network-protocol/nat-behavior-requirements-for-unicast-udp-rfc-4787-part-1-mapping-behavior&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>hole punching</category>
      <category>NAT</category>
      <category>relay server</category>
      <category>stun</category>
      <category>Turn</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/84</guid>
      <comments>https://p-bear.tistory.com/84#entry84comment</comments>
      <pubDate>Mon, 18 Mar 2024 19:48:32 +0900</pubDate>
    </item>
    <item>
      <title>API Gateway with Passport</title>
      <link>https://p-bear.tistory.com/83</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sclhD/btsFD5OHCVP/0ctWjK10oYBVKmkfE6ELA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sclhD/btsFD5OHCVP/0ctWjK10oYBVKmkfE6ELA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sclhD/btsFD5OHCVP/0ctWjK10oYBVKmkfE6ELA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsclhD%2FbtsFD5OHCVP%2F0ctWjK10oYBVKmkfE6ELA0%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;200&quot; height=&quot;200&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이전에 토스 및 네이버(라인?) 아티클에서 API Gateway를 통해서 MSA 구조에서 API로 접근하는 인증 / 인가를 수행하고 있음을 읽었었다.&lt;br /&gt;이는 각 API 서버에서 필요한 기능을 한 곳에서 수행하여 생산성을 높여 효율화를 한 작업으로 감탄했었다. 현 개인서버의 요구사항으로는 최대한 중복 개발을 줄여 생산성을 극대화하며, 귀찮음을 덜고자하는 것이 부합한다.&lt;br /&gt;위 방식을 응용하여 전체적인 아키텍쳐를 설계하고자 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버 아키텍쳐 설계&lt;/h2&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;483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eiyYMd/btsFFkZb1j1/EQz5RbanEyQUYSTqfiuik1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eiyYMd/btsFFkZb1j1/EQz5RbanEyQUYSTqfiuik1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eiyYMd/btsFFkZb1j1/EQz5RbanEyQUYSTqfiuik1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeiyYMd%2FbtsFFkZb1j1%2FEQz5RbanEyQUYSTqfiuik1%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;483&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;483&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;현 개인서버의 전체적인 아키텍쳐이다.&lt;br /&gt;- Nginx에서 SSL을 수행하고 reverse proxy를 통해 내부 통신을 연결한다.&lt;br /&gt;- Gateway Server는 Nginx로 부터 모든 요청을 받아서 L7 switching의 역할을 할 생각이다.&lt;br /&gt;&amp;nbsp; &amp;nbsp;기본적으로 Gateway와 Zookeeper를 연동하여 ServiceDiscovery를 통해 auto routing을 수행할 예정이다.&lt;br /&gt;- OAuth Server는 토큰 발급 및 Passport 정보를 생성하는 주체가 될 예정이다.&lt;br /&gt;- User Server는 기본적인 회원관리를 담당할 예정이다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;기본적으로 모든 Server들은 zookeeper에 자신의 정보를 올려서 discovery할 수 있도록 common library를 구성했었다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;User Server&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;유저는 기본적으로 main 계정이 존재하며, Social Login 등 다른 기타 유저정보가 1:N으로 구성될 수 있는 방식을 취할 예정이다.&lt;br /&gt;이는 URI Path를 통해서 Restful하게 표현할 예정이다.&lt;/blockquote&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;Package 설계&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;315&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UgP6R/btsFGpZtXpq/MFaHiBr8kYSncHPKlpp9L0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UgP6R/btsFGpZtXpq/MFaHiBr8kYSncHPKlpp9L0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UgP6R/btsFGpZtXpq/MFaHiBr8kYSncHPKlpp9L0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUgP6R%2FbtsFGpZtXpq%2FMFaHiBr8kYSncHPKlpp9L0%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;315&quot; height=&quot;459&quot; data-origin-width=&quot;315&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;User Server는 기본적으로 Rest 및 Kafka Produce/consume을 하게 될 예정이다.&lt;br /&gt;따라서 core한 서비스 및 기능을 구현하고&lt;br /&gt;inbound / outbound 처리는 별도의 페키지에서 할 수 있도록 구성했다.&lt;/blockquote&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; URI Path&lt;/h3&gt;
&lt;pre id=&quot;code_1709992650768&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
  public RouterFunction&amp;lt;ServerResponse&amp;gt; userMainRoute(final UserMainHandler userMainHandler) {
    return RouterFunctions.route()
        .GET(&quot;/main/{id}&quot;, userMainHandler::handleGetUserMainId)
        .GET(&quot;/main&quot;, userMainHandler::handleGetUserMain)
        .POST(&quot;/main&quot;, userMainHandler::handlePostUserMain)
        .POST(&quot;/main/password&quot;, userMainHandler::handlePostUserMainPassword)
        .build();
  }&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;/main, /main/{id} 는 설계에 큰 무리가 없었다.&lt;br /&gt;&lt;br /&gt;기본적으로 Password의 CUD는 user server가 수행하기 때문에, password encode/decoder 또한 user server가 관리한다.&lt;br /&gt;따라서 OAuth Server에서 사용자 id/password를 체크하기 위해서는 password 체크가 필요하다.&lt;br /&gt;하지만, password 매칭에 대한 path 설계가 조금 어색하긴 하다...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OAuth Server&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본적으로 Spring Authorization Server 라이브러리로 구성했다.&lt;br /&gt;필요한 부분만 구현했으며, 나머지는 기본 설정에 따라서 동작한다.&lt;/blockquote&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;Authorization&lt;/h3&gt;
&lt;pre id=&quot;code_1709993190754&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public InMemoryOAuth2AuthorizationService authorizationService() {
  return new InMemoryOAuth2AuthorizationService();
}

@Bean
public AuthorizationServerSettings authorizationServerSettings() {
  return AuthorizationServerSettings.builder().build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일단 scale-out에 대한 예정이 없어서 inMemory 방식 기본 모듈을 사용했다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1709993417602&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
@Slf4j
public class PbearAuthenticationProvider implements AuthenticationProvider {
  ...

  @Override
  public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
    ...

    if (!this.userService.checkPassword(
        String.valueOf(targetAuth.getPrincipal()),
        String.valueOf(targetAuth.getCredentials()))) {
      throw new BadCredentialsException(&quot;not match password, id: &quot; + authentication.getPrincipal().toString());
    }

    return new UsernamePasswordAuthenticationToken(
        this.createAuthenticatedPrincipal(String.valueOf(targetAuth.getPrincipal())),
        null,
        targetAuth.getAuthorities()
    );
  }

  ...

  private String createAuthenticatedPrincipal(final String mainId) {
    UserInfo userInfo = this.userService.getUserInfo(mainId);
    try {
      return this.objectMapper.writeValueAsString(Map.of(
          &quot;id&quot;, userInfo.id(),
          &quot;mainId&quot;, userInfo.mainId()));
    } catch (JsonProcessingException e) {
      return mainId;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Authentication은 Custom하게 구현했다.&lt;br /&gt;1. user server에 /main/password API를 호출하여 password 체크를 수행한다.&lt;br /&gt;2. user server로 부터 user정보를 GET하여 principal을 jsonString으로 생성한다.&lt;/blockquote&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;Client 관리&lt;/h3&gt;
&lt;pre id=&quot;code_1709993284507&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
  public InMemoryRegisteredClientRepository registeredClientRepository() {
    RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
        .clientId(&quot;common-client&quot;)
        ...
        .build();

    return new InMemoryRegisteredClientRepository(registeredClient);
  }&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Client도 일단 아직 관리할 필요성을 느끼지 못해서 하드코딩된 inMemory 방식 모듈을 사용했다.&lt;/blockquote&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; Token 관련&lt;/h3&gt;
&lt;pre id=&quot;code_1709993801943&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public JWKSource&amp;lt;SecurityContext&amp;gt; jwkSource(final KeyProvider keyProvider) {
  KeyPair keyPair = keyProvider.getKeyPair();
  RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
  RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
  RSAKey rsaKey = new RSAKey.Builder(publicKey)
    .privateKey(privateKey)
    .keyID(DEFAULT_OAUTH_KEY_ID)
    .build();
  JWKSet jwkSet = new JWKSet(rsaKey);
  return (jwkSelector, securityContext) -&amp;gt; jwkSelector.select(jwkSet);
}

@Bean
public OAuth2TokenCustomizer&amp;lt;JwtEncodingContext&amp;gt; oAuth2TokenCustomizer() {
  return new TokenCustomizer();
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본적으로 classpath에서 private key를 load하여 jwk를 구성하고 이를 전체 jwt에 적용하도록 구성했다.&lt;br /&gt;privage key는 기본적으로 github에 공유할 수 없기 때문에, CI 과정에서 특정 디렉토리에서 cp하도록 했다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;토큰 customizer관련 코드: https://github.com/p-bear/pbear-root/blob/master/pbear-spring/pbear-app-oauth/src/main/java/com/pbear/oauth/core/TokenCustomizer.java&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Login Page 관련&lt;/h3&gt;
&lt;pre id=&quot;code_1709993975334&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public SecurityFilterChain defaultSecurityFilterChain(final HttpSecurity http,
                                                      final RedirectService redirectService,
                                                      final PBearLoginSuccessHandler pBearLoginSuccessHandler) throws Exception {
  http
    .authorizeHttpRequests(authorize -&amp;gt;
        authorize
            .requestMatchers(&quot;/**.css&quot;).permitAll()
            .requestMatchers(request -&amp;gt; request.getRequestURI().endsWith(&quot;login-page.html&quot;)).permitAll()
            .requestMatchers(request -&amp;gt; request.getRequestURI().endsWith(&quot;login&quot;)).permitAll()
            .requestMatchers(request -&amp;gt; request.getRequestURI().endsWith(&quot;error&quot;)).permitAll()
            .anyRequest().authenticated()
    )
    .formLogin(formLogin -&amp;gt; formLogin
        .loginPage(redirectService.createLoginPageRedirectUrl())
        .loginProcessingUrl(&quot;/login&quot;)
        .successHandler(pBearLoginSuccessHandler)
    )
    ...

  return http.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본적으로 static resource에서 html을 제공하도록 구성했다.&lt;br /&gt;&lt;br /&gt;하지만, 문제는 nginx -&amp;gt; gateway -&amp;gt; oauth로 오는 구성에서 발생헀다.&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;일단 운영상황에서는 request path에 /gateway/{serverName}이 prefix로 붙어야 각 서비스로 routing 되지만, 로컬 개발시에는 direct로 붙게된다.&lt;br /&gt;그리고 nginx를 통과한 request는 기본적으로 request host가 변경 되어  servletRequest에서 원 host를 찾을 수 없었다. (내부IP로 읽어짐)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;reuqest header의 &quot;origin&quot;을 읽을 수 도 있지만, 이는 모든 상황에서 통용될 수 없다 판단했다.&lt;br /&gt;&lt;br /&gt;이를 해결하기 위해, 운영서버를 판단하는 기준인 spring profile active로 redirect  URL를 다시 작성하는 방법을 취했다. 기본적으로 url을 customize할 수 있는 옵션들은, 코드를 보았을 때 full url이면 해당 url로 대체되도록 구현되어있어, 이를 활용했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gateway Server&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zookeeper 연동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1709994603083&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

dependencies {
    ...
    implementation 'org.springframework.cloud:spring-cloud-starter-zookeeper-discovery'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1709994500934&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  ...
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 옵션으로 간단하게 zookeeper와 연동할 수 있다.&lt;br /&gt;이는 zookeeper-discovery 라이브러에서 DiscoveryClient를 구현해 제공해주기 때문이다.&lt;br /&gt;기본적으로 discovery에 등록된 serviceName으로 routing을 해준다.&lt;/blockquote&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;Passport Global Filter&lt;/h3&gt;
&lt;pre id=&quot;code_1709994718819&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 PassportGlobalFilter implements GlobalFilter {
  private static final String PASSPORT_HEADER_PREFIX = &quot;X-PP-&quot;;
  private final PassportService passportService;

  @Override
  public Mono&amp;lt;Void&amp;gt; filter(final ServerWebExchange exchange, final GatewayFilterChain chain) {
    return Mono
        .zip(this.handleExchangeWithPassport(exchange), Mono.just(chain))
        .flatMap(tuple -&amp;gt; tuple.getT2().filter(tuple.getT1()));
  }

  private Mono&amp;lt;ServerWebExchange&amp;gt; handleExchangeWithPassport(final ServerWebExchange serverWebExchange) {
    HttpHeaders httpHeaders = serverWebExchange.getRequest().getHeaders();
    if (!httpHeaders.containsKey(HttpHeaders.AUTHORIZATION)) {
      return Mono.just(serverWebExchange);
    }
    return this.passportService.getPassportData(httpHeaders.getFirst(HttpHeaders.AUTHORIZATION))
        .map(passportData -&amp;gt; {
          ServerHttpRequest.Builder builder = serverWebExchange.getRequest().mutate();
          passportData.forEach((headerName, headerValue) -&amp;gt;
              builder.headers(header -&amp;gt; header.add(PASSPORT_HEADER_PREFIX + headerName, headerValue)));
          return serverWebExchange.mutate()
              .request(builder.build())
              .build();
        });
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 기본적으로 모든 request에서 Authorization이 header에 있는지 체크한다.&lt;br /&gt;2. 있다면 해당 header value를 넣어서 oauth server의 resource API인 /passport로 요청한다.&lt;br /&gt;3. 응답 결과를 받아서 passport data를 request header에 X-PP-를 prefix해서 추가한다.&lt;br /&gt;// 현 구현으로는 X-PP-id, X-PP-mainId 2가지가 있다.&lt;br /&gt;&lt;br /&gt;추후 인가가 필요한 서비스는 X-PP- 헤더를 읽어서 해결할 수 있을 것이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;여기까지가 기본적인 서비스 개발을 위한 준비가 완료되었다고 판단된다.&lt;br /&gt;이후 개발하는 서비스들은 여기까지 구현된 사항으로 보다 쾌척하게 작업할 수 있을 것으로 기대된다.&lt;br /&gt;생산성의 향상으로 신규 개발의 저항감을 어느정도 해소 할 수 있기를 빌어본다...&lt;br /&gt;&lt;br /&gt;현 계획된 서비스는 2가지가 있다.&lt;br /&gt;1. 개인 자산 통합관리&lt;br /&gt;2. 지하철 데이터를 활용한 hot place 시각화 서비스&lt;br /&gt;올해안으로 완성할 수 있기를 기도해본다..&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Authorization Server&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6749&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://datatracker.ietf.org/doc/html/rfc6749&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://docs.spring.io/spring-authorization-server/reference/protocol-endpoints.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-authorization-server/reference/protocol-endpoints.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://github.com/spring-projects/spring-authorization-server/tree/main/samples/demo-authorizationserver/src/main/java/sample&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/spring-projects/spring-authorization-server/tree/main/samples/demo-authorizationserver/src/main/java/sample&lt;/a&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API Gateway Server&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/global-filters.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/global-filters.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://github.com/spring-cloud-samples/spring-cloud-gateway-sample/blob/main/src/main/java/com/example/demogateway/DemogatewayApplication.java&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/spring-cloud-samples/spring-cloud-gateway-sample/blob/main/src/main/java/com/example/demogateway/DemogatewayApplication.java&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://toss.tech/article/slash23-server&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://toss.tech/article/slash23-server&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>[시즌2] 개인서버 개발/시즌2 설계(완)</category>
      <category>spring</category>
      <category>spring gateway</category>
      <category>spring oauth</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/83</guid>
      <comments>https://p-bear.tistory.com/83#entry83comment</comments>
      <pubDate>Sat, 9 Mar 2024 23:40:19 +0900</pubDate>
    </item>
    <item>
      <title>ProjectReactor Case Study</title>
      <link>https://p-bear.tistory.com/82</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mcbvR/btsFbvNFxlX/zGiLksIxmg4tkTmQQt1zcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mcbvR/btsFbvNFxlX/zGiLksIxmg4tkTmQQt1zcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mcbvR/btsFbvNFxlX/zGiLksIxmg4tkTmQQt1zcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmcbvR%2FbtsFbvNFxlX%2FzGiLksIxmg4tkTmQQt1zcK%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;280&quot; height=&quot;280&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;280&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Reactive coding에서는 적절한 operation 사용이 필수 불가결이다.&lt;br /&gt;이는 가독성을 높이고, Sequence를 효과적으로 구성할 수 있다.&lt;br /&gt;하지만, operator는 종류가 많고, 복잡한 것들이 많기 때문에, 선택에 어려움이 있다.&lt;br /&gt;이번 글은 상황 별 operator 가이드와, 예시를 제공하고자 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Case 별 Operator 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sequence&amp;nbsp;생성&lt;/h3&gt;
&lt;table style=&quot;background-color: #ffffff; color: #172b4d; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-table-width=&quot;976&quot; data-number-column=&quot;false&quot; data-testid=&quot;renderer-table&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;&lt;b&gt;Operator&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt; &lt;b&gt;상황&lt;/b&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;just, justOrEmpty&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;특정 Element 또는 Element들로 시작하는 Sequence 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;fromArray, fromIterable, range, fromStream&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;iteration 할 수 있는 Element들로 Sequence(Flux) 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;empty&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;empty한 Sequence 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;defer&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;subscribe 시점에 실행&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;error&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;error 생성&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Sequence를 생성하는 상황은 기본적으로 특정 Object 또는 여러 Object 를 기반으로 시작한다.&lt;br /&gt;일반적으로 Object의 reference를 통해 생성하거나, Functional Interface를 람다로 구현한다.&lt;/blockquote&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;Sequence에서&amp;nbsp;Transform&lt;/h3&gt;
&lt;table style=&quot;background-color: #ffffff; color: #172b4d; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-table-width=&quot;978&quot; data-number-column=&quot;false&quot; data-testid=&quot;renderer-table&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;Operator&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt; &lt;b&gt;상황&lt;/b&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;map&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;Sequence에 변동 없이 Object만 바꾸고 싶을 경우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;cast&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;Sequence에 변동 없이 Object Type만 바꾸고 싶을 경우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;flatMap&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;다른 Sequence를 넣고 싶은 경우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;startWith, concatWithValues&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;element를 앞,뒤로 추가하고 싶은 경우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;collectList, collectSortedList, collectMap, collectMultiMap,&lt;/span&gt;&lt;br /&gt;&lt;span&gt;collect, count, all, any&amp;hellip;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;flux를 aggregation하고 싶은 경우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;concatWith, zip, zipWith, zipWhen&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;다른 Sequence와 합치고 싶은 경우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;defaultIfEmpty, switchIfEmpty&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;empty sequence를 처리하고 싶은 경우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;then, thenEmpty, thenMany&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;sequence와 상관없이 다음 sequence를 만들고 싶은 경우&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Sequence를 변경할 수 있는 방법들이다. 일반적으로, element 변경, 다른 sequence를 태우거나 합치는 방법들&lt;br /&gt;위 Operator들은 기존 sequence의 변화를 준다.&lt;/blockquote&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;Peeking&lt;/h3&gt;
&lt;table style=&quot;background-color: #ffffff; color: #172b4d; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-table-width=&quot;982&quot; data-number-column=&quot;false&quot; data-testid=&quot;renderer-table&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt; &lt;b&gt;Operator&lt;/b&gt; &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt; &lt;b&gt;상황&lt;/b&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;doOnNext&lt;/span&gt;&lt;br /&gt;&lt;span&gt;doOnComplete, doOnSuccess, doOnError, doOnCancel&lt;/span&gt;&lt;br /&gt;&lt;span&gt;doFirst&lt;/span&gt;&lt;br /&gt;&lt;span&gt;doOnSubscribe, doOnRequest, doOnTerminate&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;sideEffect를 통한 기존 Sequence에 영향을 미치지 않는 방식&lt;/span&gt;&lt;br /&gt;&lt;span&gt;각각의 실행 시점을 choose할 수 있다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;doOnEach, materialize&lt;/span&gt;&lt;br /&gt;&lt;span&gt;log&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;모든 signal에 대한 handling을 하고 싶을 때 사용하는 operator들&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;peeking은 기존 sequence에 영향을 미치지 않는 operator들로 여겨진다.&lt;br /&gt;이 의미는 doOnNext를 skip하고 해석해도 기존 flow를 이해할 수 있어야 한다는 말과 같다고 볼 수 있다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;가령 doOnNext는 sideEffect를 통해 기존 sequence에 영향을 미치지 않는 로직이 들어간다고 생각할 수 있다.&lt;br /&gt;따라서 doOnNext에서 객체의 속성을 변경하거나 하는 로직은 혼란을 야기할 수 있다.&lt;br /&gt;자세한 사항은 후술&lt;/blockquote&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;Filtering&lt;/h3&gt;
&lt;table style=&quot;background-color: #ffffff; color: #172b4d; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-table-width=&quot;982&quot; data-number-column=&quot;false&quot; data-testid=&quot;renderer-table&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt; &lt;b&gt;Operator&lt;/b&gt; &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;fliter, filterWhen&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;특정 로직을 통한 필터링&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;ignoreElements, distinct, ofType&lt;/span&gt;&lt;br /&gt;&lt;span&gt;take, takeLast, next&lt;/span&gt;&lt;br /&gt;&lt;span&gt;elementAt, skip, single&lt;/span&gt;&lt;br /&gt;&lt;span&gt;sample&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;hard한 filtering으로 분류 된다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;sequence의 element를 선택/filtering하는 방법&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;element를 필터링 하는 Operator들이다.&lt;br /&gt;custom로직, 갯수, 시간, element 위치 등으로 filtering할 수 있도록 지원한다.&lt;/blockquote&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;Error&amp;nbsp;Handling&lt;/h3&gt;
&lt;table style=&quot;background-color: #ffffff; color: #172b4d; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-table-width=&quot;976&quot; data-number-column=&quot;false&quot; data-testid=&quot;renderer-table&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt; &lt;b&gt;Operator&lt;/b&gt; &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt; &lt;b&gt;상황&lt;/b&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;error&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;error signal을 발생 시킬 수 있는 operator&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;onErrorReturn, onErrorComplete, onErrorResume, onErrorMap&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;error handling&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;doFinally, using&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;error consume&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;retry, retryWhen&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt;retry when error&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;에러처리를 지원하는 operator이다.&lt;br /&gt;권장사항은 에러처리를 try catch가 아닌 reactive하게 처리하는 것 이다.&lt;/blockquote&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;Flux&amp;nbsp;Split&lt;/h3&gt;
&lt;table style=&quot;background-color: #ffffff; color: #172b4d; text-align: start; border-collapse: collapse; width: 100%; height: 51px;&quot; border=&quot;1&quot; data-table-width=&quot;988&quot; data-number-column=&quot;false&quot; data-testid=&quot;renderer-table&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;span&gt; &lt;b&gt;Operator&lt;/b&gt; &lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left;&quot;&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 34px;&quot;&gt;&lt;span&gt;window, windowUntil&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left; height: 34px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;352&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmag6F/btsFckdR1Hv/xEdcfKB5mbrW06M1FPbCGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmag6F/btsFckdR1Hv/xEdcfKB5mbrW06M1FPbCGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmag6F/btsFckdR1Hv/xEdcfKB5mbrW06M1FPbCGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbmag6F%2FbtsFckdR1Hv%2FxEdcfKB5mbrW06M1FPbCGK%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;458&quot; height=&quot;352&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;352&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;div data-node-type=&quot;mediaSingle&quot; data-width-type=&quot;pixel&quot; data-width=&quot;477&quot; data-layout=&quot;center&quot;&gt;
&lt;div data-alt=&quot;image-20240129-015501.png&quot; data-file-mime-type=&quot;&quot; data-file-size=&quot;1&quot; data-file-name=&quot;file&quot; data-collection=&quot;contentId-363168522&quot; data-id=&quot;ee721bb5-a780-4c89-bdba-eed76eb8b206&quot; data-height=&quot;450&quot; data-width=&quot;607&quot; data-node-type=&quot;media&quot; data-type=&quot;file&quot; data-context-id=&quot;363168522&quot;&gt;
&lt;div id=&quot;newFileExperienceWrapper&quot; data-testid=&quot;media-card-view&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;text-align: left; height: 17px;&quot;&gt;&lt;span&gt;buffer&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgq6Gn/btsE970USf2/19OiLu7C4odhsM5RKf01K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgq6Gn/btsE970USf2/19OiLu7C4odhsM5RKf01K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgq6Gn/btsE970USf2/19OiLu7C4odhsM5RKf01K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcgq6Gn%2FbtsE970USf2%2F19OiLu7C4odhsM5RKf01K0%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;473&quot; height=&quot;275&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;275&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;flux를 size 또는 duration으로 쪼개는 Operator들 이다.&lt;/blockquote&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;Going&amp;nbsp;Back&amp;nbsp;to&amp;nbsp;the&amp;nbsp;Synchronous&amp;nbsp;World&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-pm-slice=&quot;1 1 []&quot; data-table-width=&quot;986&quot; data-table-local-id=&quot;&quot; data-autosize=&quot;false&quot; data-layout=&quot;default&quot; data-number-column=&quot;false&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt; &lt;b&gt;Operator&lt;/b&gt; &lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt; &lt;b&gt;상황&lt;/b&gt; &lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;block, blockFirst, blockLast&lt;/span&gt;&lt;br /&gt;&lt;span&gt;toIterable, toStream&lt;/span&gt;&lt;br /&gt;&lt;span&gt;toFuture&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;reactive에서 sync로 넘어가는 방법&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Sync하게 Sequence 바깥으로 탈출하는 Operator들이다.&lt;br /&gt;당연하게도 Sync로 넘어갈 수 없는 Scheduler에서는 사용할 수 없는 Operator들 이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reactive&amp;nbsp;Code&amp;nbsp;Best&amp;nbsp;Practice&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Operator&amp;nbsp;단일&amp;nbsp;책임&amp;nbsp;원칙&amp;nbsp;(Depth&amp;nbsp;최소화)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-pm-slice=&quot;1 1 [&amp;quot;panel&amp;quot;,{&amp;quot;panelType&amp;quot;:&amp;quot;info&amp;quot;,&amp;quot;panelIcon&amp;quot;:null,&amp;quot;panelIconId&amp;quot;:null,&amp;quot;panelIconText&amp;quot;:null,&amp;quot;panelColor&amp;quot;:null}]&quot; data-ke-size=&quot;size16&quot;&gt;하나의 Operator는 한가지 일을 해야한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;주의:&lt;br /&gt;Reactive code를 작성하다 보면, 뜻하지 않게 하나의 operator안에서 여러가지 비즈니스를 처리하게 된다.이는 co-worker 또는 시간이 지남에 따라, 읽기 힘들어지며, 유지보수성이 떨어지게 된다.operator의 depth가 높아지는 상황은 하나의 문장 안에 많은 내용을 집어넣는 것과 같다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;추천:&lt;br /&gt;이를 해결하기 위해서 적절한 Operation 사용을 통한 로직의 분리, 라이브러리를 통한 분리하는 방법이 있다.이는 비즈니스의 문장들을 연결할 접속사를 다양하고 적절하게 사용해서, 가독성을 높이는 효과를 보이는 것이라 생각한다.아래 예시들은 depth가 늘어나는 상황을 적절한 operator를 통해 해소하는 방법들에 대한 것이다.&lt;/blockquote&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;map(),&amp;nbsp;doOnNext(),&amp;nbsp;filter(),&amp;nbsp;switchIfEmpty()&amp;nbsp;등&amp;nbsp;Example&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;흔한 비즈니스를 예제로 했다.&lt;br /&gt;1. input 로깅&lt;br /&gt;2. 특정 값으로 db 조회&lt;br /&gt;3. 조회값 로깅&lt;br /&gt;4. db 조회 된 값을 기준으로 REST 요청&lt;br /&gt;5. REST 요청의 response code 200 검증&lt;br /&gt;&amp;nbsp;- 200이 아닐 시 custom error 생성&lt;br /&gt;6. Res의 Z 값 리턴&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708592734814&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Before

public Mono&amp;lt;?&amp;gt; worstCase(final String id) {
  return Mono.just(id)
      .flatMap(xId -&amp;gt; {
        log(xId);
        return getXFromDB(xId)
            .flatMap(x -&amp;gt; {
              log(x);
              Req req = new Req(x.getYId());
              return getYFromRest(req)
                  .flatMap(y -&amp;gt; {
                    if (y.getResCode().equals(&quot;200&quot;)) {
                      return Mono.just(y.getZId());
                    } else {
                      throw new RuntimeException(&quot;rest error&quot;);
                    }
                  });
            });
      });
}

// After
public Mono&amp;lt;?&amp;gt; betterCase(final String id) {
  return Mono.just(id)
      .doOnNext(this::log)
      .flatMap(this::getXFromDB)
      .doOnNext(this::log)
      .map(x -&amp;gt; new Req(x.getYId()))
      .flatMap(this::getYFromRest)
      .filter(y -&amp;gt; y.getResCode().equals(&quot;200&quot;))
      .switchIfEmpty(Mono.defer(() -&amp;gt; Mono.error(new RuntimeException(&quot;rest error&quot;))))
      .map(Y::getZId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;element에 초점을 두고, 변화를 operator를 통해 표현하면 depth가 낮아지는 효과를 볼 수 있다.&lt;br /&gt;비즈니스 로직을 순차적으로 호출하는 것이 아닌, element가 변화하는 방식으로 위 비즈니스를 해석하자면&lt;br /&gt;1. xId를 기반으로 Sequence 생성&lt;br /&gt;&amp;nbsp;- (sideEffect) xId 로깅&lt;br /&gt;2. DB 조회 로직을 통해 X Element로 변환&lt;br /&gt;&amp;nbsp;- X 로깅&lt;br /&gt;3. X를 Req로 변환&lt;br /&gt;4. REST 로직을 통해 Y (Res)로 변환&lt;br /&gt;5. 200인 응답을 필터링&lt;br /&gt;&amp;nbsp;- 200이 아닌 경우 error signal 생성&lt;br /&gt;6. Z 값으로 변환&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;depth를 낮추고 element 위주의 설계는 가독성 외 2가지 장점을 가진다.&lt;br /&gt;1. 메서드를 분리하는 기준이 된다.&lt;br /&gt;2. 로직의 추가/제거가 상대적으로 용이하다.&lt;/blockquote&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;filterWhen()&amp;nbsp;Example&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;453&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8EvLT/btsFeTGizJD/6NkfgV2aeD7DebirxMaXF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8EvLT/btsFeTGizJD/6NkfgV2aeD7DebirxMaXF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8EvLT/btsFeTGizJD/6NkfgV2aeD7DebirxMaXF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8EvLT%2FbtsFeTGizJD%2F6NkfgV2aeD7DebirxMaXF1%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;747&quot; height=&quot;453&quot; data-origin-width=&quot;747&quot; data-origin-height=&quot;453&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;cache에서 특정 id가 로그인 되어있는지 확인하는 필터를 추가하는 비즈니스가 추가 되는 상황을 가정했다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708592897505&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;Boolean&amp;gt; checkLoginFromCache(final String yId) {
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1708592914747&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Before
public Flux&amp;lt;?&amp;gt; filterWhenWorst() {
  return this.getXListFromDB()
      .flatMap(x -&amp;gt; checkLoginFromCache(x.getYId())
          .map(isLogin -&amp;gt; {
            if (isLogin) {
              return Mono.just(x);
            }
            return Mono.empty();
          }));
}

// After
public Flux&amp;lt;?&amp;gt; filterWhenGood() {
  return this.getXListFromDB()
      .filterWhen(x -&amp;gt; checkLoginFromCache(x.getYId()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;한정된 Operator로 인하여 depth가 늘어나게 되는 대표적인 예시이다.&lt;br /&gt;filterWhen Operator를 모른다면, 하위 Sequence로 X를 필터링해서 보내기 위해 flatMap으로 다른 Sequence의 결과를 기반으로 X Element를 다시 Sequence로 만들어야 한다.&lt;br /&gt;filterWhen을 사용하면, 다른 Sequence(Flow)의 결과를 기반으로 현제 Sequence를 필터링 할 수 있다.&lt;/blockquote&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;Zip(),&amp;nbsp;ZipWhen(),&amp;nbsp;ZipWith()&amp;nbsp;Example&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYiQN8/btsFckdSxgj/9Acq9s3m0KNNTmOib0K9zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYiQN8/btsFckdSxgj/9Acq9s3m0KNNTmOib0K9zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYiQN8/btsFckdSxgj/9Acq9s3m0KNNTmOib0K9zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYiQN8%2FbtsFckdSxgj%2F9Acq9s3m0KNNTmOib0K9zk%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;620&quot; height=&quot;393&quot; data-origin-width=&quot;620&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;X Table에서 data를 조회하고, 그 값에 기반하여 Y Table을 조회하여 새로운 객체를 만드는 상황 예시이다.&lt;br /&gt;이 예시는 Rest로 조회한 값과, DB 값을 합칠 때 같이 비즈니스 구성 시 많이 발생하는 상황이다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708593010893&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Worst
public Mono&amp;lt;?&amp;gt; zipWorst(final String id) {
  return getXFromDB(id)
      .flatMap(x -&amp;gt; getYFromDB(x.getYId())
          .map(y -&amp;gt; new Req(x, y)))
      .flatMap(this::getZFromRest);
}

// Good
public Mono&amp;lt;?&amp;gt; zipGood(final String id) {
  return getXFromDB(id)
      .zipWhen(x -&amp;gt; getYFromDB(x.getYId()))
      .map(tuple -&amp;gt; new Req(tuple.getT1(), tuple.getT2()))
      .flatMap(this::getZFromRest);
}

// Best
public Mono&amp;lt;?&amp;gt; zipBest(final String id) {
  return getXFromDB(id)
    .zipWhen(x -&amp;gt; getYFromDB(x.getYId()), Req::new)
    .flatMap(this::getZFromRest);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;두 Sequence를 합치는 zip 관련 operator를 사용하지 않는다면, depth가 하나 늘어날 수 밖에 없다.&lt;br /&gt;zip 을 통하여 명확하게 두 sequence를 합치는 것을 알리면서, depth를 줄일 수 있다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Flux&amp;nbsp;Zip&amp;nbsp;with&amp;nbsp;Default&lt;/h4&gt;
&lt;pre id=&quot;code_1708593038109&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void zipWarning() {
  Flux&amp;lt;String&amp;gt; strFlux = Flux.just(&quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;4&quot;);
  Flux&amp;lt;Integer&amp;gt; intFlux = Flux.just(1, 2);

  strFlux
      .zipWith(intFlux)
      .doOnNext(TupleUtils.consumer((s, i) -&amp;gt; System.out.println(s + &quot; / &quot; + i)))
      .subscribe();
}

&amp;gt;&amp;gt; 1 / 1
&amp;gt;&amp;gt; 2 / 2&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Flux의 Zip 사용 시 주의 점으로&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;zip 은 두 sequence 중 하나라도 complete signal이 오면 zip operation이 종료되며, 나머지 sequence로 cancel signal을 보낸다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;위 예제처럼 intFlux의 element가 2개이기 때문에, strFlux의 &amp;ldquo;3&amp;rdquo;, &amp;ldquo;4&amp;rdquo;는 cancel 되었다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;595&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yjtPF/btsFeR2MuVx/Grj7jKamaOyLSGvzQ37q30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yjtPF/btsFeR2MuVx/Grj7jKamaOyLSGvzQ37q30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yjtPF/btsFeR2MuVx/Grj7jKamaOyLSGvzQ37q30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyjtPF%2FbtsFeR2MuVx%2FGrj7jKamaOyLSGvzQ37q30%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;595&quot; height=&quot;355&quot; data-origin-width=&quot;595&quot; data-origin-height=&quot;355&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1708593075039&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Flux&amp;lt;String&amp;gt; strFlux = Flux.just(&quot;1&quot;, &quot;2&quot;, &quot;3&quot;, &quot;4&quot;);
Flux&amp;lt;Integer&amp;gt; intFlux = Flux.just(1, 2);

strFlux
    .zipWith(intFlux.concatWith(Flux.just(-1).repeat()))
    .doOnNext(TupleUtils.consumer((s, i) -&amp;gt; System.out.println(s + &quot; / &quot; + i)))
    .subscribe();
    
&amp;gt;&amp;gt; 1 / 1
&amp;gt;&amp;gt; 2 / 2
&amp;gt;&amp;gt; 3 / -1
&amp;gt;&amp;gt; 4 / -1&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;해결 방안으로 concat을 활용할 수 있다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;concat은 complete signal로 다른 publisher에 연결된다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;이를 활용하여, repeat()하는 element create publisher로 default element처럼 zip해주면&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;default element를 생성하는 zip operation이 가능하다.&lt;/span&gt;&lt;/b&gt;&lt;/blockquote&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;delayUntil()&amp;nbsp;Example&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PVBJ1/btsFfdYUbpc/6K3DGbAR0VE4JPflkY2LRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PVBJ1/btsFfdYUbpc/6K3DGbAR0VE4JPflkY2LRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PVBJ1/btsFfdYUbpc/6K3DGbAR0VE4JPflkY2LRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPVBJ1%2FbtsFfdYUbpc%2F6K3DGbAR0VE4JPflkY2LRk%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;652&quot; height=&quot;400&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Z를 result로 내보내야 하지만, 중간에 내부 속성인 x와 y를 DB에 update해야하는 비즈니스가 있다고 가정했다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708593141540&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Worst
public Mono&amp;lt;Z&amp;gt; delayUntilWorst(final Req req) {
  return getZFromRest(req)
      .flatMap(z -&amp;gt; updateXToDB(z.getX())
          .flatMap(x -&amp;gt; updateYToDB(z.getY()))
          .map(y -&amp;gt; z));
}

// Good
public Mono&amp;lt;Z&amp;gt; delayUntilGood(final Req req) {
  return getZFromRest(req)
      .delayUntil(z -&amp;gt; updateXToDB(z.getX()))
      .delayUntil(z -&amp;gt; updateYToDB(z.getY()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;sequence의 element를 유지하면서 다른 Flow를 처리하는 방법이다.&lt;br /&gt;flatMap을 사용한다면, 부모 element를 밑으로 다시 넘기기 위해 depth가 늘어날 수 밖에 없다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;주의:&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;delayUntil은 내부적으로 생성되는 sequence가 flat하다. &lt;br /&gt;따라서, Flux의 경우 element가 순차적으로 생성되는 Sequence 실행되기 때문에 성능 상 손해가 발생할 수 있다.&lt;/blockquote&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;Tuple&amp;nbsp;Library&amp;nbsp;활용&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Tuple의 경우 Reactive Coding에서 활용성이 좋은 DTO지만, 가독성이 문제가 될 수 있다.&lt;br /&gt;이는 t1, t2 등 내부 값이 IDE의 가이드 없이 확인하기 어렵기 때문이다.&lt;br /&gt;이를 도와주는 Projectreactor의 addon Library가 있다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Gradle&lt;/h4&gt;
&lt;pre id=&quot;code_1708593207290&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'io.projectreactor.addons:reactor-extra'&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Zip with Library&lt;/h4&gt;
&lt;pre id=&quot;code_1708593242754&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Before
public Mono&amp;lt;?&amp;gt; zipGood(final String id) {
  return getXFromDB(id)
      .zipWhen(x -&amp;gt; getYFromDB(x.getYId()))
      .map(tuple -&amp;gt; new Req(tuple.getT1(), tuple.getT2()))
      .flatMap(this::getZFromRest);
}

// After
public Mono&amp;lt;?&amp;gt; zipWithLib(final String id) {
  return getXFromDB(id)
      .zipWhen(x -&amp;gt; getYFromDB(x.getYId()))
      .map(TupleUtils.function((x, y) -&amp;gt; new Req(x, y)))
      .flatMap(this::getZFromRest);
}

public Mono&amp;lt;?&amp;gt; zipWithLibGood(final String id) {
  return getXFromDB(id)
      .zipWhen(x -&amp;gt; getYFromDB(x.getYId()))
      .map(TupleUtils.function(Req::new))
      .flatMap(this::getZFromRest);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 예제 중 zip 관련 예제이다.&lt;br /&gt;코드로 보았을 때 map안의 tuple이 정확하게 어떤 객체로 이루어져 있는지 확인할 수 없다.&lt;br /&gt;이를 function으로 풀어서 작성자가 보다 명확하게 표현할 수 있도록 도와둔다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;참고:&lt;br /&gt;Kotlin의 경우 문법적으로 확장 함수, 구조 분해 등을 통해 가독성을 지원한다. ㅎㅎ&lt;/blockquote&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;Use&amp;nbsp;error()&amp;nbsp;Operator&lt;/h3&gt;
&lt;pre id=&quot;code_1708593324445&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. Success to handle Exception (Throw)
public Mono&amp;lt;?&amp;gt; handleErrorExample() {
  return Mono.just(&quot;ex&quot;)
      .flatMap(this::customError)
      .onErrorReturn(&quot;errorHandled&quot;);
}
public Mono&amp;lt;String&amp;gt; customError(final String x) {
  throw new RuntimeException();
}

&amp;gt;&amp;gt; onErrorReturn 동작


// 2. Fail to handle Exception (Throw)
public Mono&amp;lt;?&amp;gt; handleErrorExample() {
  return Mono.just(&quot;ex&quot;)
      .flatMap(s -&amp;gt; this.customError(s)
          .onErrorReturn(&quot;errorHandled&quot;));
}
public Mono&amp;lt;String&amp;gt; customError(final String x) {
  throw new RuntimeException();
}

&amp;gt;&amp;gt; onErrorReturn 미동작


// 3. handle Exception with error() operator
public Mono&amp;lt;?&amp;gt; handleErrorExample() {
  return Mono.just(&quot;ex&quot;)
      .flatMap(s -&amp;gt; this.customError(s)
          .onErrorReturn(&quot;errorHandled&quot;));
}
public Mono&amp;lt;String&amp;gt; customError(final String x) {
  return Mono.error(new RuntimeException());
}

&amp;gt;&amp;gt; onErrorReturn 동작&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1번의 경우 flatMap에서 exception을 감지하고, error signal을 만들어 하위 flow로 넘기기 때문에 onErrorReturn이 처리된다.&lt;br /&gt;하지만 2번의 경우 throw 된 Exception을 error signal로 변환하지 못한 상황에서 onErrorReturn이 호출되어서 error handling이 skip 되는 문제가 있다.&lt;br /&gt;3번처럼 Exception을 error signal과 함께 생성하는 방식이, 문제 상황을 차단할 수 있다.&lt;/blockquote&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;Conditional&amp;nbsp;Flow&amp;nbsp;Example&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;비즈니스를 구성할 때 자주 발생하는 상황으로 condition에 따른 로직 분기가 있다.&lt;br /&gt;예로&lt;br /&gt;1. a일 때 A 프로세스, b일 때 B 프로세스&lt;br /&gt;2. c일 때 C 프로세스 추가&lt;br /&gt;등이 있다.&lt;br /&gt;일반적으로 분기를 통한 비즈니스 수행은 비즈니스 구성에 핵심적인 내용을 담은 경우가 많다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1.&amp;nbsp;Simple&amp;nbsp;Conditional&amp;nbsp;Flow&lt;/h4&gt;
&lt;pre id=&quot;code_1708593514649&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;String&amp;gt; conditionalFlow1(final String s) {
  return this.doSomeThing(s)
      .flatMap(condition -&amp;gt; {
        if (&quot;a&quot;.equals(condition)) {
          return processA();
        }
        return process();
      });
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;심플하게 표현한 flow 설계 코드이다.&lt;br /&gt;Depth가 많이 들어가서 안 좋은 코드로 보이지만, 위 방법이 잘못되지 않았다는 의견도 많다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708593529439&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;String&amp;gt; conditionalFlow1A(final String s) {
  return this.doSomeThing(s)
      .flatMap(condition -&amp;gt; &quot;a&quot;.equals(condition) ? this.processA() : this.process());
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;삼항연산자를 통해 위 if else를 한줄로 변경&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708593545953&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;String&amp;gt; conditionalFlow1B(final String s) {
  return this.doSomeThing(s)
      .flatMap(this::processWithCondition);
}

public Mono&amp;lt;String&amp;gt; processWithCondition(final String condition) {
  if (&quot;a&quot;.equals(condition)) {
    return processA();
  }
  return process();
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 Depth를 줄이기 위해, method를 분리했다.&lt;br /&gt;분기 로직을 메서드로 분리하는 방식은 아래 장단점을 가진다.&lt;br /&gt;&lt;b&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;장점: 비즈니스의 핵심인 분기 처리를 한 곳에서 관리할 수 있다.&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;단점: 비즈니스가 복잡해지면, 파라미터가 과도하게 많아지거나, 상위에서 defer를 통한 lazy 처리가 필요할 수 있다.&lt;/b&gt;&lt;/span&gt;&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;2.&amp;nbsp;conditional&amp;nbsp;with&amp;nbsp;switchIfEmpty&lt;/h4&gt;
&lt;pre id=&quot;code_1708593610208&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;String&amp;gt; conditionalFlow2(final String s) {
  return this.doSomeThing(s)
      .filter(&quot;a&quot;::equals)
      .flatMap(conditionA -&amp;gt; processA())
      .switchIfEmpty(Mono.defer(this::process));
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;switchIfEmpty Operator를 활용하여 분기를 처리하는 방법이다.&lt;br /&gt;이 방식의 한계점은 switchIfEmpty 내부 로직이 상위 element를 받을 수 없다는 점이다.&lt;/blockquote&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;ConnectableFlux&amp;nbsp;Example&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: &lt;a href=&quot;https://p-bear.tistory.com/80&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://p-bear.tistory.com/80&lt;/a&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Manually&lt;/h4&gt;
&lt;pre id=&quot;code_1708593684717&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ConnectableFlux&amp;lt;String&amp;gt; source = sample2.sourceFromKafka()
        .delayUntil(sample2::doSomeBusiness)
        .map(sample2::mapRecordToObject)
        .publish();

source
    .publishOn(Schedulers.boundedElastic())
    .doOnNext(s -&amp;gt; printWithThread(&quot; before A Server func&quot;))
    .flatMap(sample2::restToAServer)
    .subscribe();
source
    .publishOn(Schedulers.boundedElastic())
    .doOnNext(s -&amp;gt; printWithThread(&quot;before B Server func&quot;))
    .flatMap(sample2::restToBServer)
    .subscribe();

source.connect();

[boundedElastic-2] before B Server func
[boundedElastic-1]  before A Server func
[boundedElastic-2]  req B Server -&amp;gt; res B
[boundedElastic-1]  req B Server -&amp;gt; 1
[boundedElastic-2] before B Server func
[boundedElastic-1]  before A Server func
[boundedElastic-2]  req B Server -&amp;gt; res B
[boundedElastic-1]  req B Server -&amp;gt; 2&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;예시 비즈니스는 Kafka에서 받은 Record를 처리 후 여러 서버로 전파하는 로직이다.&lt;br /&gt;ConnectableFlux를 활용하여 여러 Sequence에게 Element를 Hot하게 보내도록 구현했다.&lt;br /&gt;위 예시는 특정 비즈니스는 한 stream에서 처리하고 이후 다른 구독자들에게 다른 scheduler를 적용한다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;with share()&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;Returns a new Flux that multicasts (shares) the original Flux. As long as there is at least one Subscriber this Flux will be subscribed and emitting data. When all subscribers have cancelled it will cancel the source Flux. This is an alias for publish().ConnectableFlux.refCount(). Returns: a Flux that upon first subscribe causes the source Flux to subscribe once, late subscribers might therefore miss items.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708593728748&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Sample2 sample2 = new Sample2();

Flux&amp;lt;String&amp;gt; sharedSource = sample2.sourceFromKafka()
    .map(sample2::mapRecordToObject)
    .share();

sharedSource
    .publishOn(Schedulers.boundedElastic())
    .doOnNext(s -&amp;gt; printWithThread(&quot; before A Server func&quot;))
    .flatMap(sample2::restToAServer)
    .subscribe();

sharedSource
    .publishOn(Schedulers.boundedElastic())
    .doOnNext(s -&amp;gt; printWithThread(&quot;before B Server func&quot;))
    .flatMap(sample2::restToBServer)
    .subscribe();

System.out.println(&quot;next...&quot;);

sharedSource
    .subscribe(s -&amp;gt; System.out.println(&quot;main sub: &quot; + s));
    
next...
main sub: 1
main sub: 2
[boundedElastic-2] before B Server func
[boundedElastic-1]  before A Server func
[boundedElastic-2]  req B Server -&amp;gt; res B
[boundedElastic-1]  req B Server -&amp;gt; 1
[boundedElastic-2] before B Server func
[boundedElastic-1]  before A Server func
[boundedElastic-2]  req B Server -&amp;gt; res B
[boundedElastic-1]  req B Server -&amp;gt; 2&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;깔끔하게 여러 subscriber를 Publisher에 적용하는 방법이다.&lt;br /&gt;share() == publish().refCount()&lt;br /&gt;이며, refCount()는 refCount(1)이며 최소 한 개의 subscriber가 있을 때 데이터를 제공하고, subscriber가 모두 cancel되면 상위flux(sharedSource)를 cancel 시킨다.&lt;/blockquote&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;Cache&amp;nbsp;Example&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n6999/btsFdZfWgnj/Q4FZUjvEpw6AYnPLY0Vtok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n6999/btsFdZfWgnj/Q4FZUjvEpw6AYnPLY0Vtok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n6999/btsFdZfWgnj/Q4FZUjvEpw6AYnPLY0Vtok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn6999%2FbtsFdZfWgnj%2FQ4FZUjvEpw6AYnPLY0Vtok%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;599&quot; height=&quot;516&quot; data-origin-width=&quot;599&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일부 비즈니스는 성능상의 이유로 값을 캐싱이 필요할 때가 있다.&lt;br /&gt;가령 client_credential로 받은 하루짜리 토큰을 하루동안 사용해야할 때 등이 있다.&lt;br /&gt;이때 cache()를 통한 mono, flux 캐싱이 유용하다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708593782029&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class TokenService {
  private final WebClient webClient;
  private final Mono&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; tokenCache = this.cacheToken();

  public Mono&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; getCachedToken() {
    return this.tokenCache;
  }

  public Mono&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; cacheToken() {
    return this.getToken()
        .cache(
            tokenResponse -&amp;gt; Duration.ofSeconds(((Integer) tokenResponse.get(&quot;expires_in&quot;)).longValue()),
            throwable -&amp;gt; Duration.ZERO,
            () -&amp;gt; Duration.ZERO);
  }

  public Mono&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; getToken() {
    return this.getTokenURIMono()
        .flatMap(this::requestToken);
  }

  private Mono&amp;lt;Map&amp;lt;String, Object&amp;gt;&amp;gt; requestToken(final URI uri) {
    return this.webClient
        .post()
        ...
        .retrieve()
        .bodyToMono(new ParameterizedTypeReference&amp;lt;&amp;gt;() {});
  }
  
  ...
}

// cache가 없을 때 token 요청
2024-02-02 14:06:38 INFO [reactor-http-nio-3] [65bc785efcfc05e5b31ab7066d150506,b31ab7066d150506] com.pbear.TokenService - create uri: https://...
2024-02-02 14:06:38 INFO [reactor-http-nio-3] [65bc785efcfc05e5b31ab7066d150506,b31ab7066d150506] com.pbear.WebClientConfig - Req 3rd &amp;gt; POST, URI=...
2024-02-02 14:06:39 INFO [reactor-http-nio-3] [65bc785efcfc05e5b31ab7066d150506,b31ab7066d150506] com.pbear.WebClientConfig - Res 3rd &amp;gt; 200 OK
2024-02-02 14:06:39 INFO [reactor-http-nio-3] [65bc785efcfc05e5b31ab7066d150506,b31ab7066d150506] com.pbear.TokenService - get Token: ce25ef3a-3ab5-4641-b9a5-faa91d9c603c
// 2,3번째 토큰은 cache로 부터
2024-02-02 14:06:51 INFO [reactor-http-nio-3] [65bc786bd274d9a444d2222fb7e82b8d,44d2222fb7e82b8d] com.pbear.TokenService - get Token: ce25ef3a-3ab5-4641-b9a5-faa91d9c603c
2024-02-02 14:06:53 INFO [reactor-http-nio-3] [65bc786d823d266fe3c0b70ec8d1cea6,e3c0b70ec8d1cea6] com.pbear.TokenService - get Token: ce25ef3a-3ab5-4641-b9a5-faa91d9c603c&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 구성은 최초 token 발급으로부터 expires_in 초 이후 cache가 삭제되는 로직이다.&lt;br /&gt;cache()가 유지되는 동안 토큰발급 rest 요청을 하지 않는다.&lt;br /&gt;이를 활용하면, 다른 library 도움 없이 기본적인 cache 로직을 구현할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;java에서 Reactive 코드를 손과 머리가 닿는 방식으로 작성하면 차후에 수습불가능한 코드 덩어리가 생성된다.&lt;br /&gt;최대한 다양한 Operator를 적절하게 사용하여 가독성을 높이는 작업은 생산성과 직결하는 사항이다.&lt;br /&gt;&lt;br /&gt;특히 depth를 줄이는 것이 중요하다. 하지만, 과하게 줄이는 것도 문제가 있다.&lt;br /&gt;때로는 코드의 줄이 늘어나는 것 보다는 깊이가 늘어나는게 좋을 때 도 있다.&lt;br /&gt;&lt;br /&gt;최대한 많은 case를 살펴보고 정리해야&lt;br /&gt;코드의 품질이 올라갈 수 있는 유일하고 정석적인 방법으로 생각된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;25219&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/reference/index.html#which-operator&quot;&gt;https://projectreactor.io/docs/core/release/reference/index.html#which-operator&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;25300&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/2771091&quot;&gt;https://d2.naver.com/helloworld/2771091&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;25341&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://climbtheladder.com/10-project-reactor-best-practices/&quot;&gt;https://climbtheladder.com/10-project-reactor-best-practices/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;25404&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tech.kakao.com/2018/05/29/reactor-programming/&quot;&gt;https://tech.kakao.com/2018/05/29/reactor-programming/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>flux</category>
      <category>mono</category>
      <category>projectreactor</category>
      <category>reactive</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/82</guid>
      <comments>https://p-bear.tistory.com/82#entry82comment</comments>
      <pubDate>Thu, 22 Feb 2024 18:28:05 +0900</pubDate>
    </item>
    <item>
      <title>Global Hooks And Context Propagation (Projectreactor Advanced Features)</title>
      <link>https://p-bear.tistory.com/81</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rWKnM/btsE1CshzSq/LRKnhMKCKNbqZweJ0sKcM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rWKnM/btsE1CshzSq/LRKnhMKCKNbqZweJ0sKcM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rWKnM/btsE1CshzSq/LRKnhMKCKNbqZweJ0sKcM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrWKnM%2FbtsE1CshzSq%2FLRKnhMKCKNbqZweJ0sKcM1%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;280&quot; height=&quot;280&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;280&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Global&amp;nbsp;Hooks란&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;리액터는 연산자의 콜백을 전역으로 설정할 수 잇다. 이는 Operator에 element가 흐르는 로직에 관여하는 것과 비슷하다.&lt;br /&gt;Hooks 클래스로 제공되는 기능으로 3가지 종류가 있다.&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;1. Dropping Hooks&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;2. Internal Error Hook&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;3. Assembly Hooks&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DroppingHooks&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적으로 onError가 모든 에러를 잡을 수 있을 것으로 기대하지만, 그렇지 못한 상황이 발생한다.&lt;br /&gt;가령 onComplete()를 호출 한 후 onNext()등 다른 signal를 연결했을 때 또는 handle되지 못한 에러가 있다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dropped&amp;nbsp;By&amp;nbsp;Previous&amp;nbsp;Complete&lt;/h3&gt;
&lt;pre id=&quot;code_1708332536669&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// onNextDropped Example
Hooks.onNextDropped(o -&amp;gt; {
  System.out.println(&quot;onNextDropped called&quot;);
  System.out.println(o);
});

Mono
    .from(subscriber -&amp;gt; {
      subscriber.onComplete();
      subscriber.onNext(&quot;&quot;);
    })
    .doOnError(throwable -&amp;gt; System.out.println(&quot;doOnError Call!&quot;))
    .subscribe();
    
&amp;gt;&amp;gt; onNextDropped called
&amp;gt;&amp;gt; inner

// onErrorDropped Example
Hooks.onErrorDropped(o -&amp;gt; {
  System.out.println(&quot;onErrorDropped called&quot;);
  System.out.println(o.getMessage());
});

Mono
    .from(subscriber -&amp;gt; {
      subscriber.onComplete();
      subscriber.onError(new RuntimeException(&quot;inner&quot;));
    }).doOnError(throwable -&amp;gt; System.out.println(&quot;doOnError Call!&quot;))
    .subscribe();
    
&amp;gt;&amp;gt; onErrorDropped called
&amp;gt;&amp;gt; inner&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Publisher에서 onComplete() 이후 onNext(), onError()를 호출하면, 잘못된 reactive flow로 에러가 나고doOnError()에 잡힐 꺼 같지만 &lt;br /&gt;error signal은 발생하지 않으며, onNext(), onError()는 drop처리된다.&lt;br /&gt;이 부분을 Hooks로 잡아내서 처리할 수 있다.&lt;/blockquote&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;Dropped&amp;nbsp;Error&amp;nbsp;by&amp;nbsp;not&amp;nbsp;handled&lt;/h3&gt;
&lt;pre id=&quot;code_1708332561839&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hooks.onErrorDropped(throwable -&amp;gt; System.out.println(&quot;error dropped: &quot; + throwable.getMessage()));

Flux.just(1, 2, 3, 4, 0)
        .doOnNext(i -&amp;gt; System.out.println(&quot;element: &quot; + i))
        .map(i -&amp;gt; 10 / i)
        .subscribe();

// hooks에 onErrorDropped를 설정하지 않는다면, default onErrorDropped로 처리된다.
&amp;gt;&amp;gt; [ERROR] (main) Operator called default onErrorDropped - reactor.core.Exceptions$ErrorCallbackNotImplemented: ...&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;처리되지 못한 Error Signal들은 결국 drop 처리된다.&lt;br /&gt;이를 Hook으로 drop되기전에 처리할 수 있다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708332575163&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// onErrorDropped에 등록되는 function은 append 된다.
Hooks.onErrorDropped(o -&amp;gt; System.out.println(&quot;onErrorDropped called&quot;));
Hooks.onErrorDropped(o -&amp;gt; System.out.println(&quot;onErrorDropped2 called&quot;));
Hooks.onErrorDropped(o -&amp;gt; System.out.println(&quot;onErrorDropped3 called&quot;));

&amp;gt;&amp;gt; onErrorDropped called
&amp;gt;&amp;gt; onErrorDropped2 called
&amp;gt;&amp;gt; onErrorDropped3 called

// 1,2번은 초기화로 인하여 제거
Hooks.onErrorDropped(o -&amp;gt; System.out.println(&quot;onErrorDropped called&quot;));
Hooks.onErrorDropped(o -&amp;gt; System.out.println(&quot;onErrorDropped2 called&quot;));
Hooks.resetOnErrorDropped();
Hooks.onErrorDropped(o -&amp;gt; System.out.println(&quot;onErrorDropped3 called&quot;));
    
&amp;gt;&amp;gt; onErrorDropped3 called&lt;/code&gt;&lt;/pre&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;pre id=&quot;code_1708332591621&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Operators.class
public static &amp;lt;T&amp;gt; void onNextDropped(T t, Context context) {
    ...
	Consumer&amp;lt;Object&amp;gt; hook = context.getOrDefault(Hooks.KEY_ON_NEXT_DROPPED, null);
	if (hook == null) {
		hook = Hooks.onNextDroppedHook;
	}
	if (hook != null) {
		hook.accept(t);
	}
	else if (log.isDebugEnabled()) {
		log.debug(&quot;onNextDropped: &quot; + t);
	}
}

// MonoFlatMap
@Override
public void onNext(T t) {
	if (done) {
		Operators.onNextDropped(t, actual.currentContext());
		return;
	}
	...
}

// FluxMap.class
@Override
public void onNext(T t) {
	if (done) {
		Operators.onNextDropped(t, actual.currentContext());
		return;
	}
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Operator를 사용하면, 각 operator에 맞는 구현체들이 호출되는데 이는 Publisher &amp;rarr; Mono or Flux를 상속 받은 클래스들이다.&lt;br /&gt;이들은 onNext() 시 done이라는 boolean flag를 확인하며, done 되었을 때 Operators의 onNextDropped()를 호출한다.&lt;br /&gt;Operators의 onNextDropped() 메서드는 onNextDroppedHook이 있으면, function을 accept한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Internal&amp;nbsp;Error&amp;nbsp;Hook&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;onOperatorError()는 예기치 못한 에러를 hook할 수 있는 방법이다.&lt;br /&gt;operator에서 error가 발생하면 콜 되며, exception에 따른 처리 및 변경이 가능하다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708332670539&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hooks.onOperatorError((throwable, o) -&amp;gt; {
  if (((Integer) 0).equals(o)) {
    System.out.println(&quot;[onOperatorError] called&quot;);
    return new RuntimeException(&quot;[onOperatorError] changed&quot;);
  }
  return throwable;
});

Flux.just(1, 2, 3, 4, 0)
    .doOnNext(i -&amp;gt; System.out.println(&quot;element: &quot; + i))
    .map(i -&amp;gt; 10 / i)
    .onErrorComplete()
    .subscribe();
    
&amp;gt;&amp;gt; element: 1
&amp;gt;&amp;gt; element: 2
&amp;gt;&amp;gt; element: 3
&amp;gt;&amp;gt; element: 4
&amp;gt;&amp;gt; element: 0
&amp;gt;&amp;gt; [onOperatorError] called&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;onOperatorError는 operator에 error signal이 전달 될 때 마다 hooking된다.&lt;br /&gt;이는 모든 operator에 doOnError + onErrorMap을 거는 것과 유사하다.&lt;/blockquote&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;pre id=&quot;code_1708332699477&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Operators.class
public static Throwable onOperatorError(@Nullable Subscription subscription, Throwable error, @Nullable Object dataSignal, Context context) {
	...
	BiFunction&amp;lt;? super Throwable, Object, ? extends Throwable&amp;gt; hook = context.getOrDefault(Hooks.KEY_ON_OPERATOR_ERROR, null);
	if (hook == null) {
		hook = Hooks.onOperatorErrorHook;
	}
	if (hook == null) {
		...
		return t;
	}
	return hook.apply(error, dataSignal);
}

// FluxFlatMap.class
@Override
public void onNext(T t) {
	...
	try {
		...
	}
	catch (Throwable ex) {
		actual.onError(Operators.onOperatorError(s, ex, t, actual.currentContext()));
		return;
	}

	if (m instanceof Callable) {
		...
		try {
			...
		}
		catch (Throwable ex) {
			actual.onError(Operators.onOperatorError(s, ex, t, actual.currentContext()));
			return;
		}
		...
	}

	try {
		...
	}
	catch (Throwable e) {
		actual.onError(Operators.onOperatorError(this, e, t, actual.currentContext()));
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Operators에는 onOperatorError() 메서드를 통해 hooks에 세팅된 hook function을 불러와서 apply해준다&lt;br /&gt;이는 Publisher의 구현체들에서 호출되는데, 대표적으로 onNext() 함수들의 catch문 안에서 호출 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Assembly&amp;nbsp;Hooks&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Assembly Hooks는 Operator chain이 조립(초기화) 때 마다 호출된다.&lt;br /&gt;대표적으로 onEachOperator가 있으며, chain이 추가될 때 마다 hooking된다.&lt;br /&gt;일반적으로 reactor.core.publisher.Operators 클래스와 함께 사용 및 구현된다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708332736028&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hooks.onEachOperator(publisher -&amp;gt; {
  System.out.println(&quot;onEachOperator called&quot;);
  return publisher;
});

Flux.just(1, 2, 3, 4)
    .doOnNext(i -&amp;gt; System.out.println(&quot;doOnNext 1: &quot; + i))
    .doOnNext(i -&amp;gt; System.out.println(&quot;doOnNext 2: &quot; + i))
    .subscribe();
    
&amp;gt;&amp;gt; onEachOperator called
&amp;gt;&amp;gt; onEachOperator called
&amp;gt;&amp;gt; onEachOperator called
&amp;gt;&amp;gt; doOnNext 1: 1
&amp;gt;&amp;gt; doOnNext 2: 1
&amp;gt;&amp;gt; doOnNext 1: 2
&amp;gt;&amp;gt; doOnNext 2: 2
&amp;gt;&amp;gt; doOnNext 1: 3
&amp;gt;&amp;gt; doOnNext 2: 3
&amp;gt;&amp;gt; doOnNext 1: 4
&amp;gt;&amp;gt; doOnNext 2: 4&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본적인 onEachOperator의 실행 시점을 찍었다.&lt;br /&gt;doOnNext(), doOnNext(), just(), 가 각각 호출(역순, chain, 조립) 될 때 onEachOperator가 실행되었다.&lt;/blockquote&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;pre id=&quot;code_1708332760534&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Flux.class
protected static &amp;lt;T&amp;gt; Flux&amp;lt;T&amp;gt; onAssembly(Flux&amp;lt;T&amp;gt; source) {
  Function&amp;lt;Publisher, Publisher&amp;gt; hook = Hooks.onEachOperatorHook;
  if (hook != null) {
    source = (Flux)hook.apply(source);
  }

  ...

  return source;
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Flux.just(&amp;hellip;) &amp;rarr; onAssembly(&amp;hellip;)&lt;br /&gt;Flux.doOnNext(&amp;hellip;) &amp;rarr; doOnSignal(&amp;hellip;) &amp;rarr; onAssembly(&amp;hellip;)&lt;br /&gt;위 처럼 operator가 호출될 때 마다 onAssembly()가 호출 된다.&lt;br /&gt;onAssembly()는 Hooks의 onEachOperatorHook를 참조하여, hook function을 apply 하는 형태로 동작한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LiftFunction&lt;/h2&gt;
&lt;pre id=&quot;code_1708332782065&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Hooks.class
public static void onEachOperator(Function&amp;lt;? super Publisher&amp;lt;Object&amp;gt;, ? extends Publisher&amp;lt;Object&amp;gt;&amp;gt; onEachOperator) {
  onEachOperator(onEachOperator.toString(), onEachOperator);
}

// Operators$LiftFunction.class
final static class LiftFunction&amp;lt;I, O&amp;gt; implements Function&amp;lt;Publisher&amp;lt;I&amp;gt;, Publisher&amp;lt;O&amp;gt;&amp;gt; {
  ...
}

// Operators.class
public static &amp;lt;I, O&amp;gt; Function&amp;lt;? super Publisher&amp;lt;I&amp;gt;, ? extends Publisher&amp;lt;O&amp;gt;&amp;gt; lift(BiFunction&amp;lt;Scannable, ? super CoreSubscriber&amp;lt;? super O&amp;gt;, ? extends CoreSubscriber&amp;lt;? super I&amp;gt;&amp;gt; lifter) {
	return LiftFunction.liftScannable(null, lifter);
}
public static &amp;lt;O&amp;gt; Function&amp;lt;? super Publisher&amp;lt;O&amp;gt;, ? extends Publisher&amp;lt;O&amp;gt;&amp;gt; lift(
			Predicate&amp;lt;Scannable&amp;gt; filter,
			BiFunction&amp;lt;Scannable, ? super CoreSubscriber&amp;lt;? super O&amp;gt;, ? extends CoreSubscriber&amp;lt;? super O&amp;gt;&amp;gt; lifter) {
	return LiftFunction.liftScannable(filter, lifter);
}
public static &amp;lt;I, O&amp;gt; Function&amp;lt;? super Publisher&amp;lt;I&amp;gt;, ? extends Publisher&amp;lt;O&amp;gt;&amp;gt; liftPublisher(BiFunction&amp;lt;Publisher, ? super CoreSubscriber&amp;lt;? super O&amp;gt;, ? extends CoreSubscriber&amp;lt;? super I&amp;gt;&amp;gt; lifter) {
	return LiftFunction.liftPublisher(null, lifter);
}
public static &amp;lt;O&amp;gt; Function&amp;lt;? super Publisher&amp;lt;O&amp;gt;, ? extends Publisher&amp;lt;O&amp;gt;&amp;gt; liftPublisher(
			Predicate&amp;lt;Publisher&amp;gt; filter,
			BiFunction&amp;lt;Publisher, ? super CoreSubscriber&amp;lt;? super O&amp;gt;, ? extends CoreSubscriber&amp;lt;? super O&amp;gt;&amp;gt; lifter) {
	return LiftFunction.liftPublisher(filter, lifter);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Hooks의 onEachOperator는 publisher &amp;rarr; publisher인 function을 매 operator가 chaining될 때 호출되도록 한다.&lt;br /&gt;Publisher &amp;rarr; Publisher인 Function의 구현체 class를 reactor에서 제공하며 이는 LiftFunction.class이다.&lt;br /&gt;이를 생성할 수 있는 방법으로 Operators의 lift() 및 liftPublisher() 메서드들을 제공하고있다.&lt;br /&gt;Operators의 lift(), liftPublisher()는 각각 LiftFunction의 static method인 liftScannable(), liftPublisher()을 호출하여 LiftFunction 인스턴스를 생성하고 있다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;283&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bO9CxO/btsEYMJrFWE/UYy36iWLafaxkpaRMRkbLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bO9CxO/btsEYMJrFWE/UYy36iWLafaxkpaRMRkbLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bO9CxO/btsEYMJrFWE/UYy36iWLafaxkpaRMRkbLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbO9CxO%2FbtsEYMJrFWE%2FUYy36iWLafaxkpaRMRkbLK%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;1226&quot; height=&quot;283&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;283&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;lifter: (pub, sub) &amp;rarr; sub 함수, publisher를 decorate할 수 있는 실질적인 로직이 들어간다.&lt;br /&gt;name: 일반 name&lt;br /&gt;filter: function의 수행여부를 판단하는 predicate&lt;/blockquote&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;LiftPublisher()&lt;/h3&gt;
&lt;pre id=&quot;code_1708332830995&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// LiftFunction.class
static final &amp;lt;I, O&amp;gt; LiftFunction&amp;lt;I, O&amp;gt; liftPublisher(
				@Nullable Predicate&amp;lt;Publisher&amp;gt; filter,
				BiFunction&amp;lt;Publisher, ? super CoreSubscriber&amp;lt;? super O&amp;gt;, ? extends CoreSubscriber&amp;lt;? super I&amp;gt;&amp;gt; lifter) {
	Objects.requireNonNull(lifter, &quot;lifter&quot;);
	return new LiftFunction&amp;lt;&amp;gt;(filter, lifter, lifter.toString());
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;liftPublisher를 통해 생성된 LiftFunction은 CoreSubscriber를 decorate할 수 있도록 도와준다.&lt;br /&gt;내부적으로 (pub, sub) &amp;rarr; sub 인 function을 통해 subscriber에 기능을 추가한다.&lt;/blockquote&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;LiftScannable()&lt;/h3&gt;
&lt;pre id=&quot;code_1708332851910&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static final &amp;lt;I, O&amp;gt; LiftFunction&amp;lt;I, O&amp;gt; liftScannable(
				@Nullable Predicate&amp;lt;Scannable&amp;gt; filter,
				BiFunction&amp;lt;Scannable, ? super CoreSubscriber&amp;lt;? super O&amp;gt;, ? extends CoreSubscriber&amp;lt;? super I&amp;gt;&amp;gt; lifter) {
	Objects.requireNonNull(lifter, &quot;lifter&quot;);

	Predicate&amp;lt;Publisher&amp;gt; effectiveFilter =  null;
	if (filter != null) {
		effectiveFilter = pub -&amp;gt; filter.test(Scannable.from(pub));
	}

	BiFunction&amp;lt;Publisher, ? super CoreSubscriber&amp;lt;? super O&amp;gt;, ? extends CoreSubscriber&amp;lt;? super I&amp;gt;&amp;gt;
			effectiveLifter = (pub, sub) -&amp;gt; lifter.apply(Scannable.from(pub), restoreContextOnSubscriberIfAutoCPEnabled(pub, sub));

	return new LiftFunction&amp;lt;&amp;gt;(effectiveFilter, effectiveLifter, lifter.toString());
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 liftPublisher와 비슷하지만, 특수한 lifter를 가진 LiftFunction을 만드는 static 메서드이다.&lt;br /&gt;(scannable, sub) &amp;rarr; sub인 Function으로 lifter를 만든다.&lt;br /&gt;Scannable이 인자인 만큼 보다 디테일한 custom이 가능하다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Scannable이란?&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;Projectreactor에서 제공하는 component의 state를 탐색할 수 있는 interface이다.&lt;br /&gt;대부분의 publisher 및 subscriber가 Scannable interface를 구현했으며, 이를 통해 attribute 및 publisher chain을 탐색할 수 있다.&lt;br /&gt;https://projectreactor.io/docs/core/release/api/reactor/core/Scannable.Attr.html&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708332878978&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Hooks.onEachOperator(Operators.lift(
    (scannable, coreSubscriber) -&amp;gt; {
      if (scannable.tagsDeduplicated().containsKey(&quot;customTag&quot;)) {
        ...
      }
      return coreSubscriber;
    }
));

Hooks.onEachOperator(Operators.liftPublisher((publisher, coreSubscriber) -&amp;gt; {
  if (publisher instanceof Flux&amp;lt;?&amp;gt;) {
    ...
  }
  return coreSubscriber;
}));&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Operators를 활용하는 기본 방법&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708332907677&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MdcContextLifterConfiguration {
  public static final String MDC_CONTEXT_REACTOR_KEY = MdcContextLifterConfiguration.class.getName();

  @PostConstruct
  @SuppressWarnings(&quot;unchecked&quot;)
  public void contextOperatorHook() {
    Hooks.onEachOperator(MDC_CONTEXT_REACTOR_KEY, Operators.lift((scannable, subscriber) -&amp;gt; new MdcContextLifter(subscriber)));
  }

  @PreDestroy
  public void cleanupHook() {
    Hooks.resetOnEachOperator(MDC_CONTEXT_REACTOR_KEY);
  }

  /**
   * Helper that copies the state of Reactor [Context] to MDC on the #onNext function.
   */
  @RequiredArgsConstructor
  public static class MdcContextLifter&amp;lt;T&amp;gt; implements CoreSubscriber&amp;lt;T&amp;gt; {

    private final CoreSubscriber&amp;lt;T&amp;gt; coreSubscriber;

    @Override
    public void onSubscribe(Subscription subscription) {
      coreSubscriber.onSubscribe(subscription);
    }

    @Override
    public void onNext(T t) {
      copyToMdc(coreSubscriber.currentContext());
      coreSubscriber.onNext(t);
    }

    ...

    @Override
    public Context currentContext() {
      return coreSubscriber.currentContext();
    }

    /**
     * Extension function for the Reactor [Context]. Copies the current context to the MDC, if context is empty clears the MDC.
     * State of the MDC after calling this method should be same as Reactor [Context] state.
     * One thread-local access only.
     */
    void copyToMdc(Context context) {
      if (context != null &amp;amp;&amp;amp; !context.isEmpty()) {
        Map&amp;lt;String, String&amp;gt; map = context.stream()
            .collect(Collectors.toMap(e -&amp;gt; e.getKey().toString(), e -&amp;gt; e.getValue().toString()));
        MDC.setContextMap(map);
      } else {
        MDC.clear();
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Reactive 환경에서 MDC context를 활용하기 위한 lifter example 이다.&lt;br /&gt;@Configuration에서 Hooks에 onEachOperator로 MdcContextLifter라는 subscriber 교체를 진행한다.&lt;br /&gt;MdcContextLifter는 기존 subscriber를 decorator/delegate한 구현체이다.&lt;br /&gt;핵심 로직은 onNext()시 context(reactor)를 context(MDC)로 복사하는 것 이다.&lt;br /&gt;// context(reactor)에 traceId를 넣는 것은 rest filter에서 subscribeContext()를 통해서 한다 (후술)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Context&amp;nbsp;Propagation&amp;nbsp;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적으로 명령형 프로그래밍 시 사용하는 ThreadLocal은 리액티브 프로그래밍에서는 문제를 야기한다.&lt;br /&gt;리액티브 동작방식이 잦은 Thread의 전환이 있고, 개발자가 이를 인지하기 쉽지 않기 때문이다.&lt;br /&gt;대표적으로 ThreadLocal을 활용한 MDC가 있다. 사실상 Logback에서의 MDC 활용은 문제를 야기한다.&lt;br /&gt;이를 해결하기 위해 리액티브에서 3.1.0버전부터 제공하는 Context라는 기능이 있다.&lt;/blockquote&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;ContextView,&amp;nbsp;Context&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;899&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKUnPg/btsE6WJ2gKu/LgAwOR8m5nspVZJaOrNkF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKUnPg/btsE6WJ2gKu/LgAwOR8m5nspVZJaOrNkF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKUnPg/btsE6WJ2gKu/LgAwOR8m5nspVZJaOrNkF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKUnPg%2FbtsE6WJ2gKu%2FLgAwOR8m5nspVZJaOrNkF1%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;648&quot; height=&quot;899&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;899&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Context는 immutable한 map과 유사하다.&lt;br /&gt;of() : Map.of()와 유사하게, immutable한 context를 생성하는 메서드&lt;br /&gt;put(), putAll() : Context가 immutable하기 때문에 값을 create or update 하여 새로운 context instance를 반환&lt;br /&gt;delete() : delete() 후 새로운 instance를 반환&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cM3abr/btsE6WpLdSe/Zihqqhwf6imEMEninwhGfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cM3abr/btsE6WpLdSe/Zihqqhwf6imEMEninwhGfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cM3abr/btsE6WpLdSe/Zihqqhwf6imEMEninwhGfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcM3abr%2FbtsE6WpLdSe%2FZihqqhwf6imEMEninwhGfk%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;516&quot; height=&quot;140&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;실제 구현체는 context안의 element 수 만큼 맴버 key, value를 갖고 있으며, contextN은 LinkedHashMap을 상속받았다.&lt;/blockquote&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;deferContextual(Function&amp;lt;ContextView,? extends Publisher&amp;lt;T&amp;gt;&amp;gt; arg)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;469&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8qv6d/btsE5EwoptH/dVekwaUlHbQ9pwiXADGCYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8qv6d/btsE5EwoptH/dVekwaUlHbQ9pwiXADGCYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8qv6d/btsE5EwoptH/dVekwaUlHbQ9pwiXADGCYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8qv6d%2FbtsE5EwoptH%2FdVekwaUlHbQ9pwiXADGCYK%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;469&quot; height=&quot;348&quot; data-origin-width=&quot;469&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본적으로 sequence는 defer()와 동일하며, contextView를 참조할 수 있는 Operator이다.&lt;/blockquote&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;contextWrite(ContextView&amp;nbsp;contextToAppend)&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Context를 CUD할 수 있는 Operator이다.&lt;/blockquote&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;Use&amp;nbsp;of&amp;nbsp;Context&lt;/h3&gt;
&lt;pre id=&quot;code_1708333094533&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Mono.just(&quot;object&quot;)
    .delayUntil(s -&amp;gt; Mono.deferContextual(contextView -&amp;gt; printWithThread(contextView.get(&quot;traceId&quot;)))) // print
    .contextWrite(Context.of(&quot;traceId&quot;, &quot;2-&quot; + UUID.randomUUID()))
    .flatMap(this::doRest) // print
    .delayUntil(s -&amp;gt; Mono.deferContextual(contextView -&amp;gt; printWithThread(contextView.get(&quot;traceId&quot;)))) // print
    .contextWrite(Context.of(&quot;traceId&quot;, &quot;1-&quot; + UUID.randomUUID()))
    .subscribe();
    
&amp;gt;&amp;gt; [main] 2-e646d101-1c0b-48dc-bc38-05ea1cfddff1
&amp;gt;&amp;gt; [boundedElastic-1] 1-5f7b0ed2-5823-464d-ad72-ca497787e8d5 request rest &amp;gt;&amp;gt; object
&amp;gt;&amp;gt; [boundedElastic-1] 1-8c27fbd4-0100-4f80-a765-338d08b20ea0&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Context는 기본적으로 Subscription Chain 전역에서 사용되는 Map으로 이해할 수 있다.&lt;br /&gt;context 생성의 주도권은 subscriber에게 있으며, 따라서 context의 전파는 chain의 끝에서부터 올라간다.&lt;br /&gt;위 예제를 보면 Context는&lt;br /&gt;&amp;nbsp;- Scheduler와 상관없이 참조가 가능하다.&lt;br /&gt;&amp;nbsp;- context는 subscribe &amp;rarr; publish로 전파된다.&lt;br /&gt;를 알 수 있다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708333116694&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Kafka Consume with Micrometer Tracing Example
kafkaReceiver
    .receive()
    .flatMap(record -&amp;gt; {
      Observation receiverObservation =
          KafkaReceiverObservation.RECEIVER_OBSERVATION.start(null,
              KafkaReceiverObservation.DefaultKafkaReceiverObservationConvention.INSTANCE,
              () -&amp;gt;
                  new KafkaRecordReceiverContext(
                      record, &quot;user.receiver&quot;, receiverOptions.bootstrapServers()),
              observationRegistry);

      return Mono.just(record)
          .doOnNext(r -&amp;gt; log.info(&quot;topic: {}, key: {}, value: {}&quot;, r.topic(), r.key(), r.value()))
          .doOnTerminate(receiverObservation::stop)
          .doOnError(receiverObservation::error)
          .contextWrite(context -&amp;gt; context.put(ObservationThreadLocalAccessor.KEY, receiverObservation));})
    .subscribe();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 예제는 Micrometer에서 reactor-kafka로 receive할 때 tracing을 하는 예시이다.&lt;br /&gt;전체 flow는&lt;br /&gt;1. record와 topic을 기준으로 Observation 객체 생성&lt;br /&gt;2. record 처리&lt;br /&gt;3. terminate 또는 error 시에 observation의 stop 및 error 처리 호출&lt;br /&gt;4. contextWrite로 observation 객체 context에 put&lt;br /&gt;이다.&lt;br /&gt;contextWrite는 subscribe 시점에 위로 적용해야 upstream으로 전달되기 때문에 contextWrite에서 receiverObservation 인스턴스를 넣었다.&lt;br /&gt;Context의 기능 상, subscribe를 포함한 전체 로직을 개발자가 해야하기 때문에, Micrometer에서 contextWrite 기능을 library에서 제공할 수 없다.&lt;br /&gt;따라서, 위 예제를 참고해 observation을 개발자가 직접 consume 로직 구현시 마다 넣어주어야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Hooks 및 Context는 reactor의 부가 기능이며, 직접적인 활용보다는, Library의 기능들을 이해하는데 필요하다.&lt;br /&gt;Hooks는 Reactor Chaining 전반에 적용되는 기능으로, 신중하게 사용해야 한다.&lt;br /&gt;Context는 Subscribe하는 시점에 상위 chain에서 참조할 값으로 사용할 수 있지만, 메인 sequance에 관여하기에는 제한적인 기능이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;15677&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/reference/#hooks&quot;&gt;https://projectreactor.io/docs/core/release/reference/#hooks&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;15739&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/reference/#context&quot;&gt;https://projectreactor.io/docs/core/release/reference/#context&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;15803&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/Scannable.Attr.html&quot;&gt;https://projectreactor.io/docs/core/release/api/reactor/core/Scannable.Attr.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>java</category>
      <category>projectreactor</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/81</guid>
      <comments>https://p-bear.tistory.com/81#entry81comment</comments>
      <pubDate>Tue, 20 Feb 2024 19:59:34 +0900</pubDate>
    </item>
    <item>
      <title>Hot vs Cold Publisher (Projectreactor Advanced Features)</title>
      <link>https://p-bear.tistory.com/80</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YbSEB/btsE6H7qz1g/yYju2GtCbcIbKWSF6u4C7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YbSEB/btsE6H7qz1g/yYju2GtCbcIbKWSF6u4C7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YbSEB/btsE6H7qz1g/yYju2GtCbcIbKWSF6u4C7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYbSEB%2FbtsE6H7qz1g%2FyYju2GtCbcIbKWSF6u4C7k%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;280&quot; height=&quot;280&quot; data-origin-width=&quot;280&quot; data-origin-height=&quot;280&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Project Reactor 기능 중 심화 개념에 해당되는 파트들을 정리&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hot&amp;nbsp;Publisher&amp;nbsp;And&amp;nbsp;Cold&amp;nbsp;Publisher&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적으로 Webflux 등에서 사용되는 Publisher들은 subscribe()가 되어야 Element를 생성한다.&lt;br /&gt;이는 모두 Cold Publisher를 사용하기 때문이며, Hot Publisher는 구독과 상관없이 Element를 생성한다.&lt;br /&gt;cold와 hot의 차이점은 데이터 발행의 주도권이 누구에게 있는가 이다.&lt;/blockquote&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;Cold Publisher&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG8SOL/btsE7ozxcN9/vZcMi8MeqqKHZtRXbY7U00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG8SOL/btsE7ozxcN9/vZcMi8MeqqKHZtRXbY7U00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG8SOL/btsE7ozxcN9/vZcMi8MeqqKHZtRXbY7U00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG8SOL%2FbtsE7ozxcN9%2FvZcMi8MeqqKHZtRXbY7U00%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;831&quot; height=&quot;537&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;537&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;pre id=&quot;code_1708331995876&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Flux&amp;lt;String&amp;gt; source = Flux.fromIterable(Arrays.asList(&quot;blue&quot;, &quot;green&quot;, &quot;orange&quot;, &quot;purple&quot;))
                          .map(String::toUpperCase);

source.subscribe(d -&amp;gt; System.out.println(&quot;Subscriber 1: &quot;+d));
source.subscribe(d -&amp;gt; System.out.println(&quot;Subscriber 2: &quot;+d));&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1708332003684&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Subscriber 1: BLUE
Subscriber 1: GREEN
Subscriber 1: ORANGE
Subscriber 1: PURPLE
Subscriber 2: BLUE
Subscriber 2: GREEN
Subscriber 2: ORANGE
Subscriber 2: PURPLE&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Cold Publisher는 Subscribe()에 따라서 Data 원천으로부터 데이터를 뽑아서 element를 만든다.&lt;br /&gt;따라서 Subscriber들은 구독 시점의 데이터에 따라서 element 들이 결정된다.&lt;br /&gt;일반적으로 DB, Resource, Other System등이 데이터의 원천이 되며, 요청이 있을 때 만 데이터를 쿼리해서 제공하는 것이 합리적이다.&lt;/blockquote&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;Hot&amp;nbsp;Publisher&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;523&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d5WUfF/btsE1B1dfQT/DEjJv3BXyiUPekyevLJiH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d5WUfF/btsE1B1dfQT/DEjJv3BXyiUPekyevLJiH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5WUfF/btsE1B1dfQT/DEjJv3BXyiUPekyevLJiH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd5WUfF%2FbtsE1B1dfQT%2FDEjJv3BXyiUPekyevLJiH1%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;829&quot; height=&quot;523&quot; data-origin-width=&quot;829&quot; data-origin-height=&quot;523&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1708332044958&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 공식 예제
DirectProcessor&amp;lt;String&amp;gt; hotSource = DirectProcessor.create();

Flux&amp;lt;String&amp;gt; hotFlux = hotSource.map(String::toUpperCase);

// 1번 구독자 인입
hotFlux.subscribe(d -&amp;gt; System.out.println(&quot;Subscriber 1 to Hot Source: &quot;+d));

hotSource.onNext(&quot;blue&quot;);
hotSource.onNext(&quot;green&quot;);

// 2번 구독자 인입
hotFlux.subscribe(d -&amp;gt; System.out.println(&quot;Subscriber 2 to Hot Source: &quot;+d));

hotSource.onNext(&quot;orange&quot;);
hotSource.onNext(&quot;purple&quot;);
hotSource.onComplete();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;DirectProcessor는 deprecated된 개념으로 Sinks로 대체되었다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708332067834&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Sinks.Many&amp;lt;String&amp;gt; hotSource = Sinks.many().multicast().onBackpressureBuffer();
Flux&amp;lt;String&amp;gt; hotFlux = hotSource.asFlux().map(String::toUpperCase);

// 1번 구독자 인입
hotFlux.subscribe(d -&amp;gt; System.out.println(&quot;Subscriber 1 to Hot Source: &quot;+d));

hotSource.tryEmitNext(&quot;blue&quot;);
hotSource.tryEmitNext(&quot;green&quot;);

// 2번 구독자 인입
hotFlux.subscribe(d -&amp;gt; System.out.println(&quot;Subscriber 2 to Hot Source: &quot;+d));

hotSource.tryEmitNext(&quot;orange&quot;);
hotSource.tryEmitNext(&quot;purple&quot;);
hotSource.tryEmitComplete();&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1708332073611&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Subscriber 1 to Hot Source: BLUE
Subscriber 1 to Hot Source: GREEN
Subscriber 1 to Hot Source: ORANGE
Subscriber 2 to Hot Source: ORANGE
Subscriber 1 to Hot Source: PURPLE
Subscriber 2 to Hot Source: PURPLE&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Hot Publisher는 subscriber의 호출과 별도로 element를 발행하는 publisher로 볼 수 있다.&lt;br /&gt;따라서 구독자가 없어도 element는 발행할 수 있으며, &lt;b&gt;발행 주도권은 publisher&lt;/b&gt;에게 있다.&lt;/blockquote&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;Create&amp;nbsp;Hot&amp;nbsp;Publisher&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;just()&lt;/h4&gt;
&lt;pre id=&quot;code_1708332114527&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class MonoJust&amp;lt;T&amp;gt; extends Mono&amp;lt;T&amp;gt; implements Fuseable.ScalarCallable&amp;lt;T&amp;gt;, Fuseable, SourceProducer&amp;lt;T&amp;gt; {
  final T value;

  MonoJust(T value) {
    this.value = Objects.requireNonNull(value, &quot;value&quot;);
  }
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;just()로 생성하는 MonoJust, FluxArray 등 Publisher는 대표적인 Hot Publisher이다.&lt;br /&gt;생성자로 value 속성을 사전에 세팅하고 이를 하위 Sequence로 전달하기 때문이다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708332132223&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void doSomeThings() {
  Mono.just(this.createString());
}
private String createString() {
  String s = &quot;string&quot;;
  System.out.println(&quot;out: &quot; + s);
  return s;
}

-----------------------------
out: string&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;실제로 아무도 subscribe하지 않았지만, createString() 메서드는 호출이 되었다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1708332145410&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 명시적으로 success sink시에 실행
Mono.create(monoSink -&amp;gt; monoSink.success(this.createString()));

// lazy하게 실행하기 위해 defer()를 사용
Mono.defer(() -&amp;gt; Mono.just(this.createString()));&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Cold하게 생성하면 의도되지 않은 메서드 호출을 방지할 수 있다.&lt;br /&gt;하지만, cold하게 동작하기 때문에, createString은 element 마다 호출된다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;publish()&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ConnectableFlux는 Flux의 publish()로 생성되는 Hot Publisher이다.&lt;br /&gt;cold &amp;rarr; hot으로 변경하는 대표적인 방법이다.&lt;br /&gt;&lt;b&gt;자세한 사항은 후술&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;cache()&lt;/h4&gt;
&lt;pre id=&quot;code_1708332188163&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public final Flux&amp;lt;T&amp;gt; cache(int history) {
  return this.replay(history).autoConnect();
}

public final ConnectableFlux&amp;lt;T&amp;gt; replay(int history) {
  return history == 0 ? onAssembly((ConnectableFlux)(new FluxPublish(this, Queues.SMALL_BUFFER_SIZE, Queues.get(Queues.SMALL_BUFFER_SIZE), false))) : onAssembly((ConnectableFlux)(new FluxReplay(this, history, 0L, (Scheduler)null)));
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;cache() 또한 hot publisher로 만들 수 있는 방법이다.&lt;br /&gt;내부적으로는 ConnectableFlux를 생성하고, element 제공 방식 중 buffer를 통한 캐싱방식으로 동작한다.&lt;br /&gt;&lt;b&gt;자세한 사항은 후술&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ConnectableFlux&lt;/h2&gt;
&lt;pre id=&quot;code_1708332216807&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Publisher 생성
Flux&amp;lt;Integer&amp;gt; source = Flux.range(1, 3)
                           .doOnSubscribe(s -&amp;gt; System.out.println(&quot;subscribed to source&quot;));

// ConnectableFlux로 변경
ConnectableFlux&amp;lt;Integer&amp;gt; co = source.publish();

// 구독자 1
co.subscribe(System.out::println, e -&amp;gt; {}, () -&amp;gt; {});
// 구독자 2
co.subscribe(System.out::println, e -&amp;gt; {}, () -&amp;gt; {});

System.out.println(&quot;done subscribing&quot;);
Thread.sleep(500);
System.out.println(&quot;will now connect&quot;);

// upstream 구독자들을 트리거
co.connect();&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1708332222932&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;done subscribing
will now connect
subscribed to source
1
1
2
2
3
3&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본적인 흐름은 ConnectableFlux에 subscribe()한 구독자는 즉시 element를 전달받는 것이 아니라&lt;br /&gt;ConnectableFlux가 준비되고 connect()를 호출해야 element를 전달 받는다.&lt;br /&gt;이는 Publisher가 여러 subscriber들이 구독하기를 기다렸다가, 한번에 publish(broadcasting)할 수 있는 방법이다.&lt;/blockquote&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;ConnectableFlux와 BackPressure&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Projectreactor는 BackPressure가 핵심 개념 중 하나이다.&lt;br /&gt;얼핏 ConnectableFlux는 BackPressure에 정면으로 위배하는 것처럼 보인다.&lt;br /&gt;ConnectableFlux는 이를 해결하기 위한 두 가지 방법을 제시해준다.&lt;br /&gt;1. publish()를 통한 생성: 동적으로 여러 구독자들의 demand를 모니터링 하며, 하나라도 구독자가 pending된다면, 즉시 소스로의 요청을 중단한다.&lt;br /&gt;2. replay()를 통한 생성: 첫 번째 구독 데이터를 버퍼링하며, 이후 구독자에게 demand에 따라서 이 데이터를 제공한다.&lt;br /&gt;위 와 같은 방식으로 ConnectableFlux는 BackPressure를 유지하는 정책을 가지고 있다.&lt;/blockquote&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;cache&amp;nbsp;with&amp;nbsp;ConnectableFlux&lt;/h3&gt;
&lt;pre id=&quot;code_1708332315930&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final class FluxReplay&amp;lt;T&amp;gt; extends ConnectableFlux&amp;lt;T&amp;gt; implements Scannable, Fuseable, OptimizableOperator&amp;lt;T, T&amp;gt; {
  final CorePublisher&amp;lt;T&amp;gt; source;
  ...

  public final CoreSubscriber&amp;lt;? super T&amp;gt; subscribeOrReturn(CoreSubscriber&amp;lt;? super T&amp;gt; actual) throws Throwable {
    while(true) {
      // replay용 subscriber 생성
      ReplaySubscriber&amp;lt;T&amp;gt; c = this.connection;
      ...

      // 전달받은 subscriber와 ReplaySubscriber로 ReplayInner 생성
      ReplayInner&amp;lt;T&amp;gt; inner = new ReplayInner(actual, c);
      ...

      // 앞에 생성한 ReplaySubscriber의 buffer를 통해 ReplayInner를 Replay
      c.buffer.replay(inner);
      ...
    }
  }
  ...

  static final class ReplayInner&amp;lt;T&amp;gt; implements ReplaySubscription&amp;lt;T&amp;gt; {
    final CoreSubscriber&amp;lt;? super T&amp;gt; actual;
    final ReplaySubscriber&amp;lt;T&amp;gt; parent;
    int index;
    int tailIndex;
    Object node;
    int fusionMode;
    long totalRequested;
    ...
  }

  // ReplayBuffer의 구현체를 가지고 있음
  static final class ReplaySubscriber&amp;lt;T&amp;gt; implements InnerConsumer&amp;lt;T&amp;gt;, Disposable {
    final ReplayBuffer&amp;lt;T&amp;gt; buffer;
    ...
    
    public void onNext(T t) {
      ReplayBuffer&amp;lt;T&amp;gt; b = this.buffer;
      if (b.isDone()) {
        Operators.onNextDropped(t, this.currentContext());
      } else {
        ++this.produced;
        // onNext가 처리될 때 마다 buffer에 value를 추가
        b.add(t);
        ...
      }
    }
  }
  
  static final class UnboundedReplayBuffer&amp;lt;T&amp;gt; implements ReplayBuffer&amp;lt;T&amp;gt; {
    ...
    // subscribe mode에 따른 
    public void replay(ReplaySubscription&amp;lt;T&amp;gt; rs) {
      if (rs.enter()) {
        if (rs.fusionMode() == 0) {
          this.replayNormal(rs);
        } else {
          this.replayFused(rs);
        }

      }
    }
    
    ...
    
    void replayNormal(ReplaySubscription&amp;lt;T&amp;gt; rs) {
      int missed = 1;
      Subscriber&amp;lt;? super T&amp;gt; a = rs.actual();
      ...

      do {
        ...
        // node(element값)들을 ReplayInner에서 가져옴
        Object[] node = (Object[])((Object[])rs.node());
        ...
        while(e != r) {
          ...

          // 현재 index가 buffer된 사이즈라면, 다음 값을 가져오기 위해 break
          empty = index == this.size;
          if (empty) {
            break;
          }
          
          
          // tailIndex는 현재 처리할 노드
          T v = node[tailIndex];
          a.onNext(v);
          // index를 한칸 뒤로
          ++tailIndex;
          ++index;
          ...
        }

        ...

        rs.index(index);
        rs.tailIndex(tailIndex);
        rs.node(node);
        ...
      } while(missed != 0);
    }
  }

  static final class SizeBoundReplayBuffer&amp;lt;T&amp;gt; implements ReplayBuffer&amp;lt;T&amp;gt; {
    // 사이즈가 정해진 replay 시나리오, 중첩구조를 가진 Node(AtomicReferenece + index)를 사용
    // 나머지는 UnboundedReplayBuffer와 대동소이
    ...
  }

  static final class SizeAndTimeBoundReplayBuffer&amp;lt;T&amp;gt; implements ReplayBuffer&amp;lt;T&amp;gt; {
    // 사이즈와 time이 정해진 시나리오, 중첩구조를 가진 TimeNode(AtomicReferenece + index + time)를 사용
    // 나머지는 UnboundedReplayBuffer와 대동소이
    ...
  }

  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;cache()의 동작방식은&lt;br /&gt;1. 최초 subscribe시 buffer에 처리된 값을 넣고&lt;br /&gt;2. 이후 subscribe는 buffer에서 값을 빼서 전달해주는 방식&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;cache() 또한 ConnectableFlux을 확장한 FluxReplay를 사용하기 때문에&lt;br /&gt;Hot Publisher로 분류된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적인 Backpressure 개념과 어울리는 cold와 다르게, hot Source은 publisher 쪽에서 주도적으로 element를 발행한다.&lt;br /&gt;Hot Publisher는 Hot Source를 취급할 수 있는 Publisher이며, 대부분 reactive를 지원하는 프레임워크 및 라이브러리에서 사용 중이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;대부분의 비즈니스는 cold 만으로 충분하다.&lt;br /&gt;일부 비즈니스는 cold &amp;rarr; hot으로 전환하는 것이 유리하지만, hot publisher는 backpressure buffer, replay 정책등을 잘 고려해서 사용해야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8465&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/reference/#advanced&quot;&gt;https://projectreactor.io/docs/core/release/reference/#advanced&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8530&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://m.blog.naver.com/gngh0101/221560774000&quot;&gt;https://m.blog.naver.com/gngh0101/221560774000&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>java</category>
      <category>projectreactor</category>
      <category>publisher</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/80</guid>
      <comments>https://p-bear.tistory.com/80#entry80comment</comments>
      <pubDate>Mon, 19 Feb 2024 19:46:14 +0900</pubDate>
    </item>
    <item>
      <title>Clean Code in Reactive</title>
      <link>https://p-bear.tistory.com/79</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Reactive 모델은 익숙한 기존 Thread 모델보다 가독성이 떨어지는 이슈가 있다.&lt;br /&gt;이러한 문제는 Reactive의 한계가 아닌, 코드를 작성할 때 가독성을 고려하지 않고 작성하면 발생한다.&lt;br /&gt;이번 리뷰는 가독성 좋은 Reactive 코드를 위한 개념들에 대해 정리한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 비즈니스 로직은 Operator가 기준이 된다.&lt;/h2&gt;
&lt;pre id=&quot;code_1706172439533&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;userService.getFavorites(userId) 
           .map(Favorite::toRequestModel)
           .flatMap(favoriteService::getDetails)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Reactive 모델은 Data Flow가 곧 비즈니스이다.&lt;br /&gt;Data Flow는 결국 비즈니스의 &amp;ldquo;행위&amp;rdquo;가 되며, 행위는 operator의 묶음으로 표현하게 된다.&lt;br /&gt;이는 결국 추후 Reactive 코드를 읽을 때 operator를 기준으로 비즈니스를 이해하게 되도록 만든다.&lt;br /&gt;&lt;br /&gt;위 예제의 비즈니스 해석은&lt;br /&gt;1. userService에서 유저가 좋아하는 것들을 가져온다. (Create Data Flow)&lt;br /&gt;2. requestModel로 변환한다. (change data format)&lt;br /&gt;3. favoriteService로 부터 details를 가져오는 스트림으로 변환한다. (to another data flow)&lt;/blockquote&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;1-1.&amp;nbsp;Operator를&amp;nbsp;기준으로&amp;nbsp;구분을&amp;nbsp;나눈다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//&amp;nbsp;Reactive&amp;nbsp;Operator&amp;nbsp;is&amp;nbsp;the&amp;nbsp;key&amp;nbsp;to&amp;nbsp;Reactive&amp;nbsp;programming&lt;/p&gt;
&lt;pre id=&quot;code_1706172494395&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//Bad
userService.getFavorites(userId).map(Favorite:toRequestModel)
           .flatMap(favoriteService::getDetails) 
 
// Good
userService.getFavorites(userId) 
           .map(Favorite::toRequestModel)
           .flatMap(favoriteService::getDetails)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;코드를 읽을 때, 위 &amp;rarr; 아래, 왼쪽 &amp;rarr; 오른쪽 으로 읽는다.&lt;br /&gt;operator를 수직으로 배열하여, 하나의 행위를 하나의 라인으로 배치되도록 하면 가독성이 높아진다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1-2.&amp;nbsp;Operator간의&amp;nbsp;간격을&amp;nbsp;줄인다.&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//&amp;nbsp;Reduce&amp;nbsp;the&amp;nbsp;distance&amp;nbsp;between&amp;nbsp;Operators&lt;/p&gt;
&lt;pre id=&quot;code_1706172571086&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//Bad
userService.getFavorites(userId)
           .map { 
               val (favorites, user) = it
               val userRequest = user.toUserRequest()
               GetFavoriteDetailRequest(
                   favorites = favorites,
                   user = userRequest
               )
            }
           .flatMap(favoriteService::getDetails) 
  
//Good
userService.getFavorites(userId)
           .map(this:toRequestModel)
           .flatMap(favoriteService::getDetails) 
  
fun toRequestModel(input: Tuple2&amp;lt;Favorites, Users&amp;gt;) {
     val (favorites, user) = input
     userRequest = user.toUserRequest()
     GetFavoriteDetailRequest(
        favorites = favorites,
        user = userRequest
     )
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;operator간의 간격이 길어지면, 개발자는 비즈니스를 해석하기 위해 많은 라인을 읽어야 한다.&lt;br /&gt;따라서 길어지는 내부 로직을 별도의 function으로 정의하여, 피로도를 줄인다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이 방식은 function의 name으로 내용을 충분히 설명할 수 있어야한다.&lt;br /&gt;너무 포괄적인 function naming은 오히려 가독성을 떨어뜨릴 뿐 아니라, 혼란을 가중시킨다.&lt;br /&gt;ex) Map&amp;lt;String, Object&amp;gt; getOthers() &lt;br /&gt;&amp;rarr; 다른 무언가를 가져오는 함수로 보이지만, 구체적인 내용을 알 수 없어 개발자가 무조건 해당 함수를 들여다 보아야 한다.&lt;/blockquote&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;1-3.&amp;nbsp;적절한&amp;nbsp;Operator&amp;nbsp;사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//&amp;nbsp;Use&amp;nbsp;Operators&amp;nbsp;matching&amp;nbsp;the&amp;nbsp;name&lt;/p&gt;
&lt;pre id=&quot;code_1706172612644&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//Bad
userService.getFavorites(userId)
           .map { 
               log.info(&quot;Received favoirtes, $it&quot;)
               it.toRequestModel()
            }
           .flatMap(favoriteService::getDetails) 
  
//Good
userService.getFavorites(userId)
           .doOnNext { log.info(&quot;Received favoirtes, $it&quot;) }
           .map(this:toRequestModel)
           .flatMap(favoriteService::getDetails)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;operator를 적절히 사용하는 것이 가독성을 높이는 방법이다.&lt;br /&gt;위 log.info()는 로깅 작업으로, map Operation이랑 전혀 관련이 없다.&lt;br /&gt;이를 분리하여 가독성을 높일 수 있다.&lt;/blockquote&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;2.&amp;nbsp;Operation&amp;nbsp;결과는&amp;nbsp;Null&amp;nbsp;대신&amp;nbsp;empty()&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//&amp;nbsp;Null&amp;nbsp;equivalent&amp;nbsp;in&amp;nbsp;reactive&amp;nbsp;is&amp;nbsp;Empty&lt;/p&gt;
&lt;pre id=&quot;code_1706172654661&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//Bad
Flux.just(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;)
        .flatMap(s -&amp;gt; {
          if (s.equals(&quot;b&quot;)) {
            return null;
          }
          return Mono.just(s);
        })
        .filter(Objects::nonNull)
        .doOnNext(log::info)
        .subscribe();

//Good
Flux.just(&quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;)
        .flatMap(s -&amp;gt; {
          if (s.equals(&quot;b&quot;)) {
            return Mono.empty();
          }
          return Mono.just(s);
        })
        .doOnNext(log::info)
        .subscribe();&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Operation의 결과는 Null이 아닌 Empty로 처리한다.&lt;br /&gt;NPE의 위험 뿐 아니라, Null Check 로직은 가독성을 배로 힘들게 만든다.&lt;/blockquote&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;3.&amp;nbsp;Method&amp;nbsp;Reference&amp;nbsp;사용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//&amp;nbsp;Use&amp;nbsp;a&amp;nbsp;method&amp;nbsp;reference&lt;/p&gt;
&lt;pre id=&quot;code_1706172694567&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Bad
userService.getUser(userId)
           .map { it.toFavoriteReq() }
           .flatMap { 
               favoriteService
                       .getFavorites(it)
                       .flatMap { 
                          favoriteService.getDetails(it.toDetailRequest) 
                       }
  
           }
  
// Good 
userService.getUser(userId)
           .map(User::toFavoriteReq)
           .flatMap { favoriteReq -&amp;gt;
               favoriteService
                       .getFavorites(favoriteReq)
                       .map(Favorite:toDetailRequest)
                       .flatMap(favoriteService::getDetail) 
                            
           }&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Method Reference는 variable을 숨기며, 가독성을 높인다.&lt;br /&gt;zip 등 일부 Operator를 제외하면, 모두 넘어오는 인자는 단건이며, 이는 생략하여 가독성을 높일 수 있다.&lt;/blockquote&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;4.&amp;nbsp;Operator의&amp;nbsp;분리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//&amp;nbsp;Don&amp;rsquo;t&amp;nbsp;let&amp;nbsp;Collection&amp;nbsp;API&amp;nbsp;shadow&amp;nbsp;Operators&lt;/p&gt;
&lt;pre id=&quot;code_1706172728600&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//Bad
Flux.just(&quot;abcd&quot;, &quot;1234&quot;)
        .flatMap(s -&amp;gt; Flux.fromIterable(Arrays.stream(s.split(&quot;&quot;))
            .map(c -&amp;gt; c + &quot;X&quot;)
            .collect(Collectors.toList())))
        .doOnNext(log::info)
        .subscribe();
        
//Good
private Stream&amp;lt;String&amp;gt; mapToX(final Stream&amp;lt;String&amp;gt; upstream) {
  return upstream.map(s -&amp;gt; s + &quot;X&quot;);
}

Flux.just(&quot;abcd&quot;, &quot;1234&quot;)
        .flatMap(s -&amp;gt; Flux.fromIterable(
            this.mapToX(Arrays.stream(s.split(&quot;&quot;)))
                .collect(Collectors.toList())))
        .doOnNext(log::info)
        .subscribe();
        
//Kotlin
private fun &amp;lt;T, R&amp;gt; Iterable&amp;lt;T&amp;gt;.streamMap(transform: (T) -&amp;gt; R): List&amp;lt;R&amp;gt; = this.map(transform)

Flux.just(&quot;abcd&quot;, &quot;1234&quot;)
            .flatMap { Flux.fromIterable(it.toCharArray().toList().streamMap { c -&amp;gt; c + &quot;X&quot; }) }
            .doOnNext{println(it)}
            .subscribe()&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Reactor의 map과 java streams의 map은 operation 이름이 중복된다.&lt;br /&gt;이는 혼동을 불러올 수 있기 때문에, Function을 분리하여 사용한다.&lt;/blockquote&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;5.&amp;nbsp;Publisher&amp;nbsp;Type&amp;nbsp;변수명&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;//&amp;nbsp;Make&amp;nbsp;variable&amp;nbsp;name&amp;nbsp;distinguishable&lt;/p&gt;
&lt;pre id=&quot;code_1706172761011&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Bad
val numberKor = Mono.just(&quot;hana&quot;)
val numberEng = Flux.just(&quot;one&quot;, &quot;two&quot;)
numberEng
      .zipWith(numberKor)
      .doOnNext { zipped -&amp;gt;
         val (eng, kr) = zipped
         log.info(&quot;English:$eng, Korean:$kr&quot;)
       }
  
// Good
val numberKorMono = Mono.just(&quot;hana&quot;)
val numberEngFlux = Flux.just(&quot;one&quot;, &quot;two&quot;)
  
numberEngFlux
     .zipWith(numberKorMono)
     .doOnNext { zipped -&amp;gt;
        val (eng, kr) = zipped
        log.info(&quot;English:$eng, Korean:$kr&quot;)
     }&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Mono 또는 Flux 타입의 변수를 사용할 때, 기존 변수들과 차이를 두고 naming 해야한다.&lt;br /&gt;명확하게 해당 변수는 Publisher를 의미한다고 알려줘야, 혼동이 없다.&lt;/blockquote&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;6.&amp;nbsp;비즈니스&amp;nbsp;내&amp;nbsp;직접적인&amp;nbsp;subscibe()&amp;nbsp;피하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;// Avoid calling subscribe() directly&lt;/p&gt;
&lt;pre id=&quot;code_1706172795495&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Bad
userService
         .getUser(req)
         .flatMap(userService::changePassword)
         .doOnNext {
              auditLogger
                      .auditLog(it)
                      .subscribe()
         }&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;비즈니스에서 직접적인 subscribe()가 늘어나면, 좀비 프로세스가 발생할 수 있다.&lt;br /&gt;이는 시스템에 큰 부담으로 작용하기 때문에 피해야 한다.&lt;br /&gt;별도의 Subscribe를 담당하는 모듈을 만들어 관리하는 방안이 필요하다.&lt;/blockquote&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;7.&amp;nbsp;Log는&amp;nbsp;Operation과&amp;nbsp;함께&amp;nbsp;작성&lt;/h2&gt;
&lt;pre id=&quot;code_1706172819052&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//Bad
public Mono&amp;lt;String&amp;gt; funcA() {
  log.info(&quot;funcA() process&quot;);
  
  return Mono.just(&quot;a&quot;)
}

public Mono&amp;lt;String&amp;gt; service() {
  Mono&amp;lt;String&amp;gt; a = funcA();
  
  // no process with a
  return Mono.just(&quot;b&quot;);
}

-----------------------------------------------------------
//Good
public Mono&amp;lt;String&amp;gt; funcA() {
  return Mono.just(&quot;a&quot;)
    .doOnNext(s -&amp;gt; log.info(&quot;funcA() process&quot;))
}

public Mono&amp;lt;String&amp;gt; service() {
  Mono&amp;lt;String&amp;gt; a = funcA();
  
  // no process with a
  return Mono.just(&quot;b&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적으로 로그는 특정 프로세스의 현 상태 또는 프로세스 기록을 남긴다.&lt;br /&gt;reactive 환경에서는 [함수의 호출 != 데이터 처리] 이기 때문에 로그는 operation과 함께 작성해야 오해가 생기지 않는다.&lt;/blockquote&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;8.&amp;nbsp;reusable&amp;nbsp;Method&amp;nbsp;작성&lt;/h2&gt;
&lt;pre id=&quot;code_1706172847004&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//Bad
public Flux&amp;lt;Article&amp;gt; getUserArticle(final User user) {
  return articleService.findAllByUserId(user.getUserId)
}

//Good
public Flux&amp;lt;Article&amp;gt; getUserArticle(final String userId) {
  return articleService.findAllByUserId(userId)
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;지속적으로 늘어나는 함수를 줄이기 위해서는, 함수의 재사용은 필수이다.&lt;br /&gt;재사용 가능한 함수를 위해서는, 특정 service 또는 vo에 종속적이지 않은 함수를 작성하는 것이 바람직하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Reactive Code의 가독성을 높이기 위한 방법&lt;br /&gt;1. 적절한 Operator를 사용한다.&lt;br /&gt;2. 함수는 하나의 기능만 하도록 한다.&lt;br /&gt;3. 변수 명 및 함수 명에 공을 들인다.&lt;br /&gt;로 요약할 수 있다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;clean code를 지향할 때, 좋은 방법 중 하나는 꼼꼼한 코드리뷰가 있다.&lt;br /&gt;이후 코드 리뷰 시 변수 명 및 함수 명을 다 같이 확인하고, 논의를 거쳐 최종 merge 되는 과정이 코드 품질에 도움이 될 것으로 생각한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;당연한 이야기들로 보이기 때문에, 자주 간과하게 되는 것들이다...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://tacogrammer.com/en/clean-reactive-code-en/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://tacogrammer.com/en/clean-reactive-code-en/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techblog.woowahan.com/12903/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.woowahan.com/12903/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>clean code</category>
      <category>java</category>
      <category>kotlin</category>
      <category>reactive</category>
      <category>refactoring</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/79</guid>
      <comments>https://p-bear.tistory.com/79#entry79comment</comments>
      <pubDate>Thu, 25 Jan 2024 17:56:18 +0900</pubDate>
    </item>
    <item>
      <title>GraphQL Overview</title>
      <link>https://p-bear.tistory.com/78</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQoC0y/btsDrFjgM6G/3OAkbEpmiVT0VkJQY4pGZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQoC0y/btsDrFjgM6G/3OAkbEpmiVT0VkJQY4pGZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQoC0y/btsDrFjgM6G/3OAkbEpmiVT0VkJQY4pGZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQoC0y%2FbtsDrFjgM6G%2F3OAkbEpmiVT0VkJQY4pGZ0%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;225&quot; height=&quot;225&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GraphQL 이란&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;META에서 개발한 Query Language over HTTP.&lt;br /&gt;SQL이 Application &amp;rarr; DB로 데이터를 쿼리하는 언어라면, GraphQL은 Application &amp;rarr; Server로 데이터를 쿼리하는 언어&lt;/blockquote&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;GraphQL&amp;nbsp;is&amp;nbsp;for&amp;hellip; &lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cva75f/btsDuvN26ze/X1pepXMiNjxZeZgZw4xY40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cva75f/btsDuvN26ze/X1pepXMiNjxZeZgZw4xY40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cva75f/btsDuvN26ze/X1pepXMiNjxZeZgZw4xY40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcva75f%2FbtsDuvN26ze%2FX1pepXMiNjxZeZgZw4xY40%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;1418&quot; height=&quot;629&quot; data-origin-width=&quot;1418&quot; data-origin-height=&quot;629&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Client &amp;harr;︎ Server 간의 Data Provide Mismatch를 해결하기 위해 개발된 언어&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;No more Overfetching, Underfetching&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;데이터의 제공 주체는 서버이며, 서버는 다수의 client를 위한 서비스를 제공하기 때문에, 데이터에 기반하여, API를 제공한다.&lt;br /&gt;이에 client는 필요한 Data 이외의 데이터를 함께 제공받거나(overfetching), 필요한 데이터를 모으기 위해 여러번 호출 (underfetching, n+1) 해야 하는 상황이 발생할 수 있다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Client&amp;nbsp;n+1?&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;client가 특정 게시자의 게시글 내용 전체를 쿼리하고 싶다면&lt;br /&gt;일반적으로 다음의 과정을 따른다&lt;br /&gt;1. GET /xx/list API를 통해 xx의 특정 게시자 게시글 list를 받는다&lt;br /&gt;2. GET /xx/{id} API를 통해 1번 쿼리된 게시글 수 만큼 요청한다.&lt;br /&gt;이에 list요청 1번, 세부 게시글 n번의 요청이 필요하다.&lt;br /&gt;서버에서 전용 API를 만들어 줄 수 있지만, 이는 장기적으로 부적절하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GraphQL&amp;nbsp;장단점&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. Overfetching, Underfetching을 해결할 수 있다.&lt;br /&gt;&amp;nbsp; +응답 Size를 줄일 수 있다.&lt;br /&gt;2. Endpoint 및 request parameter, response Object 등 정의를 줄인다.&lt;br /&gt;3. 데이터 제공 로직이 간소화된다.&lt;br /&gt;&amp;nbsp; - Data의 Depth와 상관없이 한번에 쿼리가 가능하다.&lt;br /&gt;&amp;nbsp; - 표준화된 요청 로직 작성 가능&lt;br /&gt;&amp;nbsp; - 한번에 여러 Type의 Data를 쿼리 할 수 있다. &lt;br /&gt;4. 서버 &amp;harr;︎ 클라이언트의 커플링 일부 감소 효과&amp;hellip; (ex: data type 변경 시 api 사용자가 아닌, 해당 데이터를 사용하는 client에게만 영향)&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 서버 &amp;harr;︎ 클라이언트 간의 소통방식의 변화: client는 Data 구조에 대한 이해가 필요하고, server는 client에게 데이터 변화를 고지해야 함&lt;br /&gt;2. Data 설계의 복잡성 증가: client가 쿼리를 작성하기 때문에, Data 설계 시 많은 경우의 수를 고려해야 함&lt;br /&gt;3. HTTP 캐싱 이점을 활용하기 힘들다: Endpoint가 하나 이기 때문&lt;br /&gt;4. 에러 핸들링 난이도가 상승: 하나에 요청에 n개의 에러가 발생할 수 있기 때문&lt;br /&gt;5. 요청 및 응답이 무거워질 수 있다: 뎁스가 높아지거나, 데이터 자체가 큰 경우에 대해 서버에서 핸들링 하기가 힘들다&lt;br /&gt;6. MSA 구조에 대응하기 위해 다른 솔루션이 필요: MSA 구조에서 client가 각 서버로 graphQL 쿼리를 하면, 기존 목적에 위배되는 상황 발생 (후술)&lt;br /&gt;7. 파일업로드는 통합 불가&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Concepts&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Document&lt;/h3&gt;
&lt;pre id=&quot;code_1705281693342&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;query {
  author(limit: 5) {
    id
    name
    articles {
      id
      title
      content
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;GraphQL의 Request String(Body)를 documnet라고 한다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Fields&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;author, id, name, article, id, title, content가 field이다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Arguments&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;author(limit: 5)의 limit 부분이 Arguments이다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Variables&lt;/h4&gt;
&lt;pre id=&quot;code_1705281749466&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  limit: 5
}

query($limit: Int) {
  author(limit: $limit) {
    id
    name
    articles {
      id
      title
      content
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 쿼리의 Argument를 Variables로 바꿔서 쓰는 방법&lt;br /&gt;limit: 5로 variable을 선언하고, 쿼리에서 해당 variable을 사용한다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Aliases&amp;nbsp;/&amp;nbsp;Fragments&lt;/h4&gt;
&lt;pre id=&quot;code_1705281783446&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Alias
query fetchAuthor {
  author(id: 1) {
    name
    profile_pic_large: profile_pic(size: &quot;large&quot;)
    profile_pic_small: profile_pic(size: &quot;small&quot;)
  }
}

// Fragments
fragment authorFields on author {
  id
  name
  profile_pic
  created_at
}

query fetchAuthor {
  author(id: 1) {
    ...authorFields
  }
}

query fetchAuthors {
  author(limit: 5) {
    ...authorFields
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Alias는 response 받을 값을 다르게 지정하여 return 받기 위한 방법 &lt;br /&gt;Fragments는 query template를 만들어서 재사용하기 위한 방법&lt;/blockquote&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;Operation&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;GraphQL는 크게 3가지 동작을 가진다.&lt;br /&gt;1. Fetching: Read dataMutation: &lt;br /&gt;2. CUD data&lt;br /&gt;3. Subscription: Realtime update (over websocket)&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Fetching&lt;/h4&gt;
&lt;pre id=&quot;code_1705281843504&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  author {
    id
    name
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;author의 id와 name을 쿼리하는 예시&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Mutation&lt;/h4&gt;
&lt;pre id=&quot;code_1705281888844&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;mutation {
  insert_todos(objects: [{ title: &quot;New Todo&quot; }]) {
    returning {
      id
      title
      is_completed
      is_public
      created_at
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;ldquo;new todo&amp;rdquo;라는 object를 insert하고, response로 id 등 서버 생성 값을 받는 예시&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Subscription&lt;/h4&gt;
&lt;pre id=&quot;code_1705281930647&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;subscription {
  todos {
    id
    created_at
    is_completed
    is_public
    title
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;todos에 대한 신규 데이터를 실시간으로 제공 받는 operation 예시&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Apollo&amp;nbsp;Federation&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;MSA 구조에서 GraphQL을 적용하게 되면, 수 많은 서버로 각각 GraphQL요청을 보내야 한다.&lt;br /&gt;이는 over/underfetching을 방지하기 위한 GraphQL의 근본 사상을 위협하게 된다.&lt;br /&gt;Apollo팀에서는 이를 해결하기 위해 router를 통한 graph 연동으로 솔루션을 제공한다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LwJh6/btsDrlk3AUe/fMUIJK4bQiOAjTJa8GKE40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LwJh6/btsDrlk3AUe/fMUIJK4bQiOAjTJa8GKE40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LwJh6/btsDrlk3AUe/fMUIJK4bQiOAjTJa8GKE40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLwJh6%2FbtsDrlk3AUe%2FfMUIJK4bQiOAjTJa8GKE40%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;396&quot; height=&quot;269&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Router는 Root로 할당되며, 각 GraphQL API 서버들을 하위 뎁스로 갖는다.&lt;br /&gt;client는 router를 통해 GraphQL 통신을 하며, Router가 각 API 서버로 Data를 Fetching해주는 역할을 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사례&amp;nbsp;&amp;amp;&amp;nbsp;인사이트&amp;nbsp;(중복&amp;nbsp;내용&amp;nbsp;제거)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Naver&amp;nbsp;(2020)&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;강점:&amp;nbsp;&lt;br /&gt;&amp;nbsp;- 강력한 개발자 경험 제공: Client가 Data Schema를 이해하고 쿼리를 작성할 수 있음, 이는 투명한 정보를 통해 client의 책임 있는 API 호출을 유도함&lt;br /&gt;&amp;nbsp;- 성능 이점: 모바일 디바이스처럼 한정된 네트워크 대역폭 또는 한정된 리소스인 경우, 원 쿼리를 통한 데이터 access는 큰 이점을 가짐&lt;br /&gt;&amp;nbsp;- 안정적인 subscription: 원하는 data만 구독하여 실시간 데이터 처리를 효율적으로 할 수 있음&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dr5CT/btsDrEEGU3Z/vmI6oq68fkSlSMlcY9LBRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dr5CT/btsDrEEGU3Z/vmI6oq68fkSlSMlcY9LBRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dr5CT/btsDrEEGU3Z/vmI6oq68fkSlSMlcY9LBRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDr5CT%2FbtsDrEEGU3Z%2FvmI6oq68fkSlSMlcY9LBRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;142&quot; data-origin-width=&quot;1294&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;- Schema 관리를 위한 모니터링 툴이 필수&lt;br /&gt;&amp;nbsp;- schema 버전 관리가 필수&lt;br /&gt;&amp;nbsp;- Data Schema 설계를 GraphQL의 관계 중심으로 설계해야 함&lt;br /&gt;&amp;nbsp;- Data Graph를 관리하기 위한 비용이 적지 않음&lt;/blockquote&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;Kakao&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;gql은 퍼포먼스적인 장점이 분명 존재합니다. 하지만 개인적으로 더 관심이 가는 장점은 바로 생산성 향상입니다. gql은 기존 백앤드-프론트앤드 협업 문화를 많이 바꿀것으로 예상합니다. gql의 협업 구조상 프론트앤드쪽에 조금 더 할일이 많아지고 힘이 실리는 느낌입니다. 에자일하게 웹사이트 프로젝트를 진행하는데 gql이 많은 도움이 될 것이라고 생각합니다.&lt;br /&gt;개발자 여러분에게 안타까운 소식을 전하자면, 이 글을 읽었다고 해서 gql을 바로 실전에서 사용하기는 쉽지 않을 것입니다. 이 글에서는 gql의 클라이언트 모듈에 대해서는 구체적으로 언급하지도 않았습니다. 특히 react를 혼합해서 사용하려면, 또 다시 가파른 고개를 넘어서야 제대로 사용 할 수 있을 것입니다. &lt;br /&gt;&amp;hellip;&lt;/blockquote&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;Google Cloud&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그래프를&amp;nbsp;데이터&amp;nbsp;중심&amp;nbsp;계층&amp;nbsp;구조로&amp;nbsp;취급&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;- GraphQL은 Data에 Access 할 수 있는 하나의 계층으로 취급해야 합니다.&lt;br /&gt;&amp;nbsp;- REST의 /a의 GET, POST는 a 객체를 조회/추가 한다고 하듯이, GraphQL의 Query 및 operation도 그러해야 합니다.&lt;br /&gt;&amp;nbsp;- GraphQL을 활용할 때 빠지기 쉬운 함정은 operation 시 이를 함수처럼 사용하는 경우 입니다.&lt;br /&gt;&amp;nbsp;- 함수로 취급하여 operation하면, 점점 데이터 스키마와 멀어지게 되며, 이는 schema를 더 이상 관리할 수 없는 상황이 발생 함&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;GraphQL을&amp;nbsp;고집하지&amp;nbsp;않기&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;nbsp;- GraphQL은 항상 최선의 선택이 아니라는 것을 염두 해야 한다.&lt;br /&gt;&amp;nbsp;- GraphQL은 REST의 대체제가 아닌, 선택사항이며, 데이터 구조 상 REST가 더 적합한 경우도 많다.&lt;br /&gt;&amp;nbsp;- 가령 4번째 depth안의 모든 name에 대한 조회를 한다면, graphQL은 쿼리의 비대함 뿐 아니라, 서버의 부답을 작용할 것&lt;/blockquote&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;AWS&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;GraphQL이 유리한 경우&lt;br /&gt;&amp;nbsp;- 대역폭이 제한되어 있으며 요청 및 응답 수를 최소화하려는 경우&lt;br /&gt;&amp;nbsp;- 여러 데이터 소스가 있고 이를 하나의 엔드포인트에서 결합하려는 경우&lt;br /&gt;&amp;nbsp;- 고객의 요청이 매우 다양하고 기대하는 응답도 크게 다른 경우&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;REST가 유리한 경우&lt;br /&gt;&amp;nbsp;- 애플리케이션 규모가 작고 데이터가 덜 복잡한 경우&lt;br /&gt;&amp;nbsp;- 모든 클라이언트에서 유사하게 사용되는 데이터와 작업이 있는 경우&lt;br /&gt;&amp;nbsp;- 복잡한 데이터 쿼리가 필요 없는 경우&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;GraphQL 도입 시의 이점 대비 리스크 및 단점이 너무 크게 다가온다.&lt;br /&gt;장단점을 미루어 보았을 때, 비즈니스 Data 구조가 depth가 적고 wide하게 확장되는 상황에서 매우 유용할 것으로 생각된다.&lt;br /&gt;&amp;nbsp;- depth가 늘어날 수록 서버의 성능 부담이 크게 증가한다. 하지만, 클라이언트가 서버의 부담을 고려하여 쿼리를 하는 것은 말이 안된다. 반면 wide한 data가 많아질 수록 클라이언트의 호출 수가 증가하게 된다.&lt;br /&gt;여러 사례를 보았을 때, 비즈니스 또는 데이터 구조에 따라서 GraphQL 도입 여부를 결정하는 것이 바람직 하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5597&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hasura.io/learn/graphql/intro-graphql/core-concepts/&quot;&gt;https://hasura.io/learn/graphql/intro-graphql/core-concepts/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5659&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.howtographql.com/basics/1-graphql-is-the-better-rest/&quot;&gt;https://www.howtographql.com/basics/1-graphql-is-the-better-rest/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5726&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.apollographql.com/docs/federation/&quot;&gt;https://www.apollographql.com/docs/federation/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5774&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://deview.kr/data/deview/session/attach/1100_T1_&quot;&gt;https://deview.kr/data/deview/session/attach/1100_T1_&lt;/a&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://deview.kr/data/deview/session/attach/1100_T1_%E1%84%87%E1%85%A1%E1%86%A8%E1%84%89%E1%85%A5%E1%86%BC%E1%84%92%E1%85%A7%E1%86%AB_GraphQL%20API%20%E1%84%81%E1%85%A1%E1%84%8C%E1%85%B5%E1%86%BA%E1%84%80%E1%85%A5%20%E1%84%8B%E1%85%AE%E1%86%AB%E1%84%8B%E1%85%A7%E1%86%BC%E1%84%92%E1%85%A2%E1%84%87%E1%85%A9%E1%84%8C%E1%85%B5%20%E1%84%86%E1%85%AF.pdf&quot; data-renderer-mark=&quot;true&quot; data-testid=&quot;link-with-safety&quot;&gt;박성현_GraphQL API 까짓거 운영해보지 뭐.pdf&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>graphQL</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/78</guid>
      <comments>https://p-bear.tistory.com/78#entry78comment</comments>
      <pubDate>Mon, 15 Jan 2024 10:32:39 +0900</pubDate>
    </item>
    <item>
      <title>Netflix OSS란 (feat. Service Mesh)</title>
      <link>https://p-bear.tistory.com/77</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;224&quot; data-origin-height=&quot;224&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7nVdT/btsC84pB4kv/1jhJv3N1wHwoa4ku1zKKpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7nVdT/btsC84pB4kv/1jhJv3N1wHwoa4ku1zKKpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7nVdT/btsC84pB4kv/1jhJv3N1wHwoa4ku1zKKpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7nVdT%2FbtsC84pB4kv%2F1jhJv3N1wHwoa4ku1zKKpk%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;224&quot; height=&quot;224&quot; data-origin-width=&quot;224&quot; data-origin-height=&quot;224&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;style5&quot; /&gt;
&lt;h2 id=&quot;개요&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. Netflix OSS 및 Spring Cloud Netflic 구성 요소 소개&lt;br /&gt;2. Spring with Netflix에서 제시한 MSA 구조 소개&lt;br /&gt;3. 부록으로 MSA 서비스간의 통신을 infra로 풀어낸 side-car 패턴의 구성방안에 대해 소개&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;Service-Mesh란?&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;156&quot; data-ke-size=&quot;size26&quot;&gt;Service Mesh란?&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-node-type=&quot;mediaSingle&quot; data-width-type=&quot;pixel&quot; data-width=&quot;866&quot; data-layout=&quot;center&quot;&gt;
&lt;div&gt;
&lt;div data-alt=&quot;image-20240104-052357.png&quot; data-file-mime-type=&quot;&quot; data-file-size=&quot;1&quot; data-file-name=&quot;file&quot; data-collection=&quot;contentId-345145420&quot; data-id=&quot;49f38fad-e7d8-472d-bec6-a76c0fbc4d52&quot; data-height=&quot;828&quot; data-width=&quot;1428&quot; data-node-type=&quot;media&quot; data-type=&quot;file&quot; data-context-id=&quot;345145420&quot;&gt;
&lt;div id=&quot;newFileExperienceWrapper&quot; data-testid=&quot;media-card-view&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZTiGt/btsC7rrTxFH/FPYWmvepyLUkaiXu2kK9hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZTiGt/btsC7rrTxFH/FPYWmvepyLUkaiXu2kK9hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZTiGt/btsC7rrTxFH/FPYWmvepyLUkaiXu2kK9hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZTiGt%2FbtsC7rrTxFH%2FFPYWmvepyLUkaiXu2kK9hk%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;1428&quot; height=&quot;828&quot; data-origin-width=&quot;1428&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Service Mesh는 여러 서비스(MSA등)간의 의사소통을 책임지는 Layer를 말한다.&lt;br /&gt;Layer가 어떻게 구성되는지 장단점이 존재하며, 각 솔루션들이 존재한다.&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;방식&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.372%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;솔루션&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 61.5117%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;설명&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15%;&quot;&gt;&lt;span&gt;&lt;span&gt;PaaS&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.372%;&quot;&gt;&lt;span&gt;&lt;span&gt;Azure fabric, lagom&amp;hellip;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 61.5117%;&quot;&gt;&lt;span&gt;플랫폼 서비스로&lt;span&gt; mesh&lt;/span&gt;구조를 풀어낸 방안&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;개발자는&lt;span&gt; Mesh &lt;/span&gt;구성 및 동작에 대해서 신경 쓰지 않는다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Framework &lt;/span&gt;형태로 제공되며&lt;span&gt;, &lt;/span&gt;특화된 코드를 필요로 합니다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15%;&quot;&gt;&lt;span&gt;&lt;span&gt;Side car Proxy&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.372%;&quot;&gt;&lt;span&gt;&lt;span&gt;Istio, Consul&amp;hellip;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 61.5117%;&quot;&gt;&lt;span&gt;&lt;span&gt;Service &lt;/span&gt;형태로&lt;span&gt; mesh&lt;/span&gt;구조를 관리하는 방안&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;sideCar &lt;/span&gt;패턴으로&lt;span&gt; Application&lt;/span&gt;에&lt;span&gt; Binding &lt;/span&gt;된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;개발자는&lt;span&gt; sideCar&lt;/span&gt;를 바라보고 호출하며&lt;span&gt;, sideCar&lt;/span&gt;들에 대한&lt;span&gt; control&lt;/span&gt;을 중앙에서 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;이는 오케스트레이션에 매우 유리한 구조로&lt;span&gt;, &lt;/span&gt;쿠버네티스와 조합이 많이 된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15%;&quot;&gt;&lt;span&gt;&lt;span&gt;Library&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.372%;&quot;&gt;&lt;span&gt;&lt;span&gt;Netflix OSS, Spring Cloud&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 61.5117%;&quot;&gt;&lt;span&gt;라이브러리 형태로&lt;span&gt; mesh&lt;/span&gt;구조를 제공하는 방안&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;개발자는&lt;span&gt; library API&lt;/span&gt;를 통해 직접&lt;span&gt; Service Mesh&lt;/span&gt;를 구성 및&lt;span&gt; Custom &lt;/span&gt;할 수 있다&lt;span&gt;.&lt;/span&gt;&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;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Netflix OSS 란?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Netflix는 2007년 DB 문제로 인한 서비스 중단을 겪고 나서, 신뢰성 높고 scale-out이 가능한 시스템을 구축하기로 결정했다.&lt;br /&gt;Netflix는 명확한 목적성을 가지고 신규 시스템 개발을 진행했다.&lt;br /&gt;&amp;nbsp;- 고가용성: fail-over처리, 장애 확산 방지 등 서비스 안정성 확보&lt;br /&gt;&amp;nbsp;- scale-out: 수평적 확장이 가능한 시스템&lt;br /&gt;&amp;nbsp;- Easy Deploy: 손쉬운 배포 / 관리 환경&lt;br /&gt;Netflix는 위 목적성을 가지고 7년 이상 개발을 진행했으며, 그 노하우를 Netflix OSS라는 이름으로 공개하였다.&lt;br /&gt;2015년 Spring은 Netflix OSS를 Integration한 Spring Cloud Netflix 1.0 Release&lt;br /&gt;2018년 Netflix가 Spring Cloud Netflix 기여&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 id=&quot;Spring-Cloud-Netflix?&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1182&quot; data-ke-size=&quot;size26&quot;&gt;Spring Cloud Netflix?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Cloud Netflix provides &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Netflix OSS integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment&lt;/b&gt;&lt;/span&gt; and other Spring programming model idioms. With a few simple annotations, you can quickly enable and configure the common patterns inside your application and build large distributed systems with battle-tested Netflix components. The patterns provided include Service Discovery (Eureka).&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;Spring-with-Netflix-OSS&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1639&quot; data-ke-size=&quot;size26&quot;&gt;Spring with Netflix OSS&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnHL3a/btsC9mjjw0h/vezOU7s7ZoENTmL8neTQl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnHL3a/btsC9mjjw0h/vezOU7s7ZoENTmL8neTQl1/img.png&quot; data-alt=&quot;https://dzone.com/articles/microservices-journey-from-netflix-oss-to-istio-se&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnHL3a/btsC9mjjw0h/vezOU7s7ZoENTmL8neTQl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnHL3a%2FbtsC9mjjw0h%2FvezOU7s7ZoENTmL8neTQl1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;404&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dzone.com/articles/microservices-journey-from-netflix-oss-to-istio-se&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.0233%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;항목&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.8605%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;상세&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.0233%;&quot;&gt;&lt;span&gt;&lt;span&gt;Eureka&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.8605%;&quot;&gt;&lt;span&gt;&lt;span&gt;Service Discovery&lt;/span&gt;를 담당하는 모듈&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Service Registry (ex: Zookeeper)&lt;/span&gt;와 연동하여&lt;span&gt;, Enable &lt;/span&gt;된&lt;span&gt; Service&lt;/span&gt;정보를 찾을 수 있게 하는 역할&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.0233%;&quot;&gt;&lt;span&gt;&lt;span&gt;API gateway&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Zuul&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.8605%;&quot;&gt;&lt;span&gt;&lt;span&gt;Single Endpoint&lt;/span&gt;를 제공해주는 모듈&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;MSA &lt;/span&gt;구조에서 쪼개진&lt;span&gt; Service&lt;/span&gt;들을 통합하여 사용하는 효과를 보이고&lt;span&gt;, &lt;/span&gt;인증 등 공통으로 필요한 기능을 통합 관리 할 수 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Zuul&lt;/span&gt;은&lt;span&gt; API Gateway&lt;/span&gt;의 구현체에 해당되며&lt;span&gt;, &lt;/span&gt;핵심은&lt;span&gt; filter&lt;/span&gt;를 통해&lt;span&gt; pre &amp;rarr; routing &amp;rarr; post &amp;rarr; error &lt;/span&gt;단계로 요청을 수행하는 구조를 가지고 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.0233%;&quot;&gt;&lt;span&gt;&lt;span&gt;Ribbon&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.8605%;&quot;&gt;&lt;span&gt;&lt;span&gt;Client Side Load Balancer&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;정적 분산 또는&lt;span&gt; Eureka&lt;/span&gt;와 연동하여 동적 분산도 가능&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;분산 룰을&lt;span&gt; custom &lt;/span&gt;할 수 있으며&lt;span&gt; netflix&lt;/span&gt;는 지역별 서버 대응하기 위해 사용&lt;span&gt; (Zone Aware Load Balancer)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.0233%;&quot;&gt;&lt;span&gt;&lt;span&gt;hystrix&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.8605%;&quot;&gt;&lt;span&gt;&lt;span&gt;Circuit breaker&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;MSA &lt;/span&gt;환경에서 장애 확산을 최대한 억제하기 위한&lt;span&gt; circuit breaker&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;서비스 장애&lt;span&gt;, &lt;/span&gt;응답지연&lt;span&gt;, &lt;/span&gt;과 트레픽 상황의 전체 시스템으로 확산 되는 것을 막을 수 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.0233%;&quot;&gt;&lt;span&gt;&lt;span&gt;Config Server&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.8605%;&quot;&gt;&lt;span&gt;통합&lt;span&gt; Config &lt;/span&gt;관리&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;외부에서&lt;span&gt; config&lt;/span&gt;를 주입받을 수 있는 기능을 제공&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 13.0233%;&quot;&gt;&lt;span&gt;&lt;span&gt;OpenFeign&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 86.8605%;&quot;&gt;&lt;span&gt;&lt;span&gt;MSA&lt;/span&gt;의 네트워크&lt;span&gt; Client&lt;/span&gt;를 제공&lt;span&gt;&lt;/span&gt;&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;style5&quot; /&gt;
&lt;h2 id=&quot;Spring-Cloud에서-제시하는-MSA-구조&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2494&quot; data-ke-size=&quot;size26&quot;&gt;Spring Cloud에서 제시하는 MSA 구조&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daN1Vx/btsC00pjlTv/9lwEAL82h3nYzynQSpMzD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daN1Vx/btsC00pjlTv/9lwEAL82h3nYzynQSpMzD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daN1Vx/btsC00pjlTv/9lwEAL82h3nYzynQSpMzD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaN1Vx%2FbtsC00pjlTv%2F9lwEAL82h3nYzynQSpMzD1%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;844&quot; height=&quot;764&quot; data-origin-width=&quot;844&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;전체 application의 config 관리, 모니터링, 장애 확산 방지, 서비스 디스커버리 등 MSA 환경의 scale-out, 고가용성을 제공할 수 있는 구조&lt;br /&gt;다수의 유사한 서비스를 scale-out할 수 있는 구조에 대한 예시이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;Appendix:-Side-Car-Proxy를-통한-Service-Mesh-구성&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2668&quot; data-ke-size=&quot;size26&quot;&gt;Appendix: Side Car Proxy를 통한 Service Mesh 구성&lt;/h2&gt;
&lt;h3 id=&quot;Sidecar-Pattern&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2714&quot; data-ke-size=&quot;size23&quot;&gt;Sidecar Pattern&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J8nTy/btsC33evic6/JYPX6QP8tRIqVoNXHmN4x1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J8nTy/btsC33evic6/JYPX6QP8tRIqVoNXHmN4x1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J8nTy/btsC33evic6/JYPX6QP8tRIqVoNXHmN4x1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ8nTy%2FbtsC33evic6%2FJYPX6QP8tRIqVoNXHmN4x1%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;661&quot; height=&quot;421&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;421&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;모든 Application(Container)에 추가로 sidecar(Container)를 배포하고 Application의 통신은 sidecar를 통해서 진행하도록 세팅&lt;br /&gt;Service &amp;harr;︎ Service간의 직접 통신을 infra로 분리하여 MSA 환경의 필요 기능을 비즈니스에서 분리합니다.&lt;br /&gt;MSA 운영 배포에 필요한 기능을 sidecar가 가지는 구조는, Application의 수정 없이, MSA 환경에 대응할 수 있는 이점을 기집니다.&lt;br /&gt;sidecar로 Istio, Consul 같은 솔루션들은 MSA 환경에서 필요한 대부분의 기능들을 제공&lt;br /&gt;&amp;nbsp;- Service Discovery&lt;br /&gt;&amp;nbsp;- load balancing&lt;br /&gt;&amp;nbsp;- Dynamic Routing&lt;br /&gt;&amp;nbsp;- Circuit Breaking&lt;br /&gt;&amp;nbsp;- TLS&lt;br /&gt;&amp;nbsp;- 인증&lt;br /&gt;&amp;nbsp;- Health check, retry,timeout&lt;br /&gt;&amp;nbsp;- Metric&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;REFERENCE&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3187&quot; data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3198&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://spring.io/projects/spring-cloud-netflix/&quot;&gt;https://spring.io/projects/spring-cloud-netflix/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3248&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bravenamme.github.io/2020/07/21/msa-netflix/&quot;&gt;https://bravenamme.github.io/2020/07/21/msa-netflix/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3302&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://programmaticponderings.com/2016/02/15/diving-deeper-into-getting-started-with-spring-cloud/&quot;&gt;https://programmaticponderings.com/2016/02/15/diving-deeper-into-getting-started-with-spring-cloud/&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3403&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@tedigom/MSA-제대로-이해하기-4Service-Mesh-f8k317qn1b&quot;&gt;https://velog.io/@tedigom/MSA-제대로-이해하기-4Service-Mesh-f8k317qn1b&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;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;</description>
      <category>개발 일지</category>
      <category>MSA</category>
      <category>Netflix OSS</category>
      <category>Service Mesh</category>
      <category>spring cloud</category>
      <category>spring cloud netflix</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/77</guid>
      <comments>https://p-bear.tistory.com/77#entry77comment</comments>
      <pubDate>Fri, 5 Jan 2024 15:21:18 +0900</pubDate>
    </item>
    <item>
      <title>Spring Properties from Lib Resources (PropertySourcesPlaceholderConfigurer, EnvironmentPostProcessor)</title>
      <link>https://p-bear.tistory.com/76</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;327&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6z9Oh/btsCQslCSBh/4MrCkpO6BVk5egz7k2Kvc0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6z9Oh/btsCQslCSBh/4MrCkpO6BVk5egz7k2Kvc0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6z9Oh/btsCQslCSBh/4MrCkpO6BVk5egz7k2Kvc0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6z9Oh%2FbtsCQslCSBh%2F4MrCkpO6BVk5egz7k2Kvc0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;327&quot; height=&quot;154&quot; data-origin-width=&quot;327&quot; data-origin-height=&quot;154&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring 프로젝트의 Properties를 구성하는 방법은 여러 가지가 있다.&lt;br /&gt;그중 프로젝트 외부(external jar)의 resources에서 Load 하여 추가하는 방법에 대해 정리하고자 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. Github Project를 public으로 설정하면 일부 정보들은 노출되지 않도록 관리해야 한다.&lt;br /&gt;ex) db password.. etc&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2. Multi Project 구조 같이 공통적으로 적용되어야 하는 Properties를 따로 관리해야 할 때&lt;br /&gt;ex) Actuator 설정, 접속정보 등&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Properties file을 Runtime에 외부에서 Load 하게 되면 위 문제상황을 해결할 수 있다.&lt;br /&gt;일반적으로는 Server에 관리되는 Properties 파일을 두고 runtime 시점에 로드하거나, System Properties로 설정하는 방법을 사용한다.&lt;br /&gt;이번에 소개하는 방식은, lib(jar) 안에 들어있는 Properties 파일을 runtime 시점에 load 하는 방법이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PropertySourcesPlaceholderConfigurer&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;479&quot; data-origin-height=&quot;610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tdvYy/btsCWM4ncxR/XTTqHdGam2Aj7NJq3pKEYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tdvYy/btsCWM4ncxR/XTTqHdGam2Aj7NJq3pKEYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tdvYy/btsCWM4ncxR/XTTqHdGam2Aj7NJq3pKEYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtdvYy%2FbtsCWM4ncxR%2FXTTqHdGam2Aj7NJq3pKEYK%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;479&quot; height=&quot;610&quot; data-origin-width=&quot;479&quot; data-origin-height=&quot;610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;792&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCESAc/btsCUYcUtRm/8NNvxmj79TRJqSFKeBycE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCESAc/btsCUYcUtRm/8NNvxmj79TRJqSFKeBycE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCESAc/btsCUYcUtRm/8NNvxmj79TRJqSFKeBycE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCESAc%2FbtsCUYcUtRm%2F8NNvxmj79TRJqSFKeBycE1%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;505&quot; height=&quot;534&quot; data-origin-width=&quot;749&quot; data-origin-height=&quot;792&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;PropertySourcesPlaceholderConfigurer는 Context에 적용된 Properties에 접근하여 작업을 할 수 있는 config bean이다.&lt;br /&gt;org.springframework.core.env의 PropertySources를 통해 get/set 할 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;PropertySourcesPlaceholderConfigurer 분석&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;b&gt;Member Variables&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;b&gt;Desc&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;MutablePropertySources&amp;nbsp;propertySources&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;적용할 PropertySource 들&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;PropertySources&amp;nbsp;appliedPropertySources&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;기존에 적용된 PropertySource들&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Environment&amp;nbsp;environment&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Spring Context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 85px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50.0001%; height: 17px;&quot;&gt;&lt;b&gt;Method&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 49.9999%; height: 17px;&quot;&gt;&lt;b&gt;Desc&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50.0001%; height: 17px;&quot;&gt;void&amp;nbsp;setPropertySources(PropertySources&amp;nbsp;propertySources)&lt;/td&gt;
&lt;td style=&quot;width: 49.9999%; height: 17px;&quot;&gt;PropertySources를 추가할 수 있는 method이다.&lt;br /&gt;configurer당 1개의 PropertySources(Iterable)을 가지며 최종 적용 시 기존 Sources에 추가된다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50.0001%; height: 17px;&quot;&gt;PropertySources&amp;nbsp;getAppliedPropertySources()&lt;/td&gt;
&lt;td style=&quot;width: 49.9999%; height: 17px;&quot;&gt;Runtime 상 설정된 Properties를 가져오는 method&lt;br /&gt;PropertySources를 add할 수 있다.&lt;br /&gt;대부분 자동설정되는 property는 immutable 하다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;configurer에서 주로 사용되는 property getter, setter들이다.&lt;br /&gt;그 외 상위 클래스의 method를 활용해서 세팅하는 것도 가능하다.&lt;/blockquote&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;Secret Application Yaml 적용&lt;/h3&gt;
&lt;pre id=&quot;code_1704179015531&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@ConditionalOnResource(resources = &quot;classpath:application-secret.yml&quot;)
public YamlPropertiesFactoryBean yamlPropertiesFactoryBean() {
  YamlPropertiesFactoryBean bean = new YamlPropertiesFactoryBean();
  bean.setResources(new ClassPathResource(&quot;application-secret.yml&quot;));
  return bean;
}

@Bean
@ConditionalOnBean(YamlPropertiesFactoryBean.class)
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(final YamlPropertiesFactoryBean yamlPropertiesFactoryBean) {
  PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
  Properties secretProperties = yamlPropertiesFactoryBean.getObject();
  if (secretProperties != null) {
    log.info(&quot;apply application-secret.yml&quot;);
    configurer.setProperties(secretProperties);
  }
  return configurer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;classpath의 yaml 파일을 읽어서 PropertySourcesPlaceholderConfigurer에 적용하는 코드이다.&lt;br /&gt;configurer를 Bean으로 등록하면, BeanFactoryPostProcessor에서 설정파일 로드 및 주입작업을 진행한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;BeanFactoryPostProcessor?&lt;/b&gt;&lt;br /&gt;bean 생성 완료 전 개입가능한 processor&lt;br /&gt;BeanPostProcessor보다 먼저 수행되며 특정 bean 생성 시점에 트리거링 됨&lt;br /&gt;일반적으로 Dynamic 하게 bean을 생성 또는 변경하기 위해 사용됨&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제점 발생&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;@Value 등 Bean 생성 시점이 Processor보다 이후 life-cycle을 가진 부분에서는 문제가 없었으나&lt;br /&gt;PropertySource load 순서가 보장될 수 없는 상황이 발생&lt;br /&gt;&lt;br /&gt;&lt;b&gt;문제 상황:&lt;/b&gt;&lt;br /&gt;&lt;b&gt;&amp;nbsp;- Actuator 설정을 전체 Multi-project에서 공유하기 위해 적용했지만, Actuator 라이브러리에서 management application properties를 주입받는 시점이 &lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;PropertySourcesPlaceholderConfigurer 보다 이전에 동작함.&lt;br /&gt;&lt;/span&gt;따라서 PropertySourcesPlaceholderConfigurer로 actuator Property 주입이 불가&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;EnvironmentPostProcessor&lt;/h2&gt;
&lt;pre id=&quot;code_1704179764906&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package org.springframework.boot.env;

@FunctionalInterface
public interface EnvironmentPostProcessor {
  void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring env를 구성할 수 있는 Processor이다.&lt;br /&gt;주요 메서드인 postProcessEnvironment()의 invoke 시점은 ApplicationContext가 재생성(refresh)하는 시점이다.&lt;br /&gt;따라서 spring run부터 ~ context 구성 전에 어딘가에서 실행되기 때문에, lib의 properties 사용시점 전에 주입이 보장될 수 있다.&lt;/blockquote&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;919&quot; data-origin-height=&quot;595&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w7x4c/btsCTzq1LPP/3rYQK8hltov4OysxD7JDYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w7x4c/btsCTzq1LPP/3rYQK8hltov4OysxD7JDYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w7x4c/btsCTzq1LPP/3rYQK8hltov4OysxD7JDYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw7x4c%2FbtsCTzq1LPP%2F3rYQK8hltov4OysxD7JDYk%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;919&quot; height=&quot;595&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;595&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PropertiesApplier&lt;/h3&gt;
&lt;pre id=&quot;code_1704180255278&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface PropertiesApplier extends EnvironmentPostProcessor {
  @Override
  default void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application){
    try {
      environment
          .getPropertySources()
          .addFirst(this.getTargetProperties());
    } catch (IOException e) {
      // no logger loaded yet...
      e.printStackTrace(System.out);
    }
  }

  PropertySource&amp;lt;?&amp;gt; getTargetProperties() throws IOException;

  default PropertySource&amp;lt;?&amp;gt; getTargetProperties(final String fileName) throws IOException {
    List&amp;lt;PropertySource&amp;lt;?&amp;gt;&amp;gt; secretPropertySources = new YamlPropertySourceLoader()
        .load(&quot;classpath:&quot; + fileName, new ClassPathResource(fileName));
    if (secretPropertySources == null || secretPropertySources.isEmpty()) {
      throw new IOException(&quot;fail to find &quot; + fileName);
    }
    System.out.println(((OriginTrackedMapPropertySource) secretPropertySources.get(0)).getSource());
    return secretPropertySources.get(0);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;EnviromentPostProcessor를 상속받는 interface로 공통 함수를 구현했다.&lt;br /&gt;전체 flow는&lt;br /&gt;1. load property&lt;br /&gt;2. add property&lt;br /&gt;이며, 공통 함수로 classpath에서 file 가져오는 메서드가 있다.&lt;/blockquote&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;Implements&lt;/h3&gt;
&lt;pre id=&quot;code_1704181562096&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// load secret properties from classpath
public class SecretPropertiesApplier implements PropertiesApplier {
  @Override
  public PropertySource&amp;lt;?&amp;gt; getTargetProperties() throws IOException {
    return this.getTargetProperties(&quot;application-secret.yml&quot;);
  }
}

// load actuator properties from classpath
public class ActuatorPropertiesApplier implements PropertiesApplier {
  @Override
  public PropertySource&amp;lt;?&amp;gt; getTargetProperties() throws IOException {
    return this.getTargetProperties(&quot;application-actuator.yml&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;application-secret.yml 및 application-actuator.yml을 classpath에서 읽어서 적용되도록 구현&lt;/blockquote&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;resources/META-INF/spring.factories&lt;/h3&gt;
&lt;pre id=&quot;code_1704181616225&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.springframework.boot.env.EnvironmentPostProcessor=\
com.pbear.lib.properties.ActuatorPropertiesApplier,\
com.pbear.lib.properties.SecretPropertiesApplier&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;EnviromentPostProcessor를 적용하기 위해서는 spring.factories에 명시를 해줘야 한다.&lt;br /&gt;implements 한 두 class의 full path를 명시하여 적용.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion + Next..&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Property 주입은 매우 다양한 방법을 제공하고 있다.&lt;br /&gt;덕분에 큰 작업 없이, 원하는 시점에 property 주입이 가능했다.&lt;br /&gt;Spring에서는 기본적으로 EnviromentPostProcessor를 통한 property 주입을 추천하고 있다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;현재로 기본 LIB 구성은 완료되었다.&lt;br /&gt;이후 공통부분이 나올 때마다 LIB를 업데이트하려고 한다.&lt;br /&gt;&lt;br /&gt;다음 작업 예정은 starter library들이다.&lt;br /&gt;작업 예정으로는&lt;br /&gt;1. pbear-starter-webflux: webclient를 설정 및 build 하여 bean으로 등록, webflux관련 기본 로직 관련 starter&lt;br /&gt;2. pbear-starter-r2dbc: r2dbc connection factory를 build하여 bean 등록 starter&lt;br /&gt;3. pbear-starter-kafka: kafka Topic에 대한 정보를 제공해 주는 starter&lt;br /&gt;4. pbear-starter-redis: Redis connection factory 및 기타 template 설정 starter&lt;br /&gt;5. pbear-starter-mongodb: ...&lt;br /&gt;6. pbear-starter-websocket: ...&lt;br /&gt;7. ...&lt;br /&gt;등 있다...&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;재사용성이 높은 모듈로 만드는 것이 목표&lt;br /&gt;&lt;/b&gt;&lt;b&gt;-&amp;gt; starter library만 implement 하면, 기본적인 접속정보 주입 등은 완료되도록.. + custom config에 열려있는 구조&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;xx. annotation 기반의 openfeign도 고려 중&lt;br /&gt;&lt;br /&gt;기본 starter 구성 후 기본적인 구성인&lt;br /&gt;account 관리 서버, gateway, access-control server (OAuth) 등 작업하고자 한다.&lt;br /&gt;해당 서버들이 24년 첫 프로젝트가 될 예정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.baeldung.com/spring-boot-environmentpostprocessor&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.baeldung.com/spring-boot-environmentpostprocessor&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;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;</description>
      <category>[시즌2] 개인서버 개발/시즌2 설계(완)</category>
      <category>EnvironmentPostProcessor</category>
      <category>PropertySourcesPlaceholderConfigurer</category>
      <category>spring</category>
      <category>Spring Boot</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/76</guid>
      <comments>https://p-bear.tistory.com/76#entry76comment</comments>
      <pubDate>Tue, 2 Jan 2024 17:07:41 +0900</pubDate>
    </item>
    <item>
      <title>AnnotationProcessor를 통한 project 내 ServerInfo Enum 생성</title>
      <link>https://p-bear.tistory.com/75</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;2195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A5DIR/btsCAOgYBJL/MUVQUXsE5oCe68gi8SgfUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A5DIR/btsCAOgYBJL/MUVQUXsE5oCe68gi8SgfUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A5DIR/btsCAOgYBJL/MUVQUXsE5oCe68gi8SgfUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA5DIR%2FbtsCAOgYBJL%2FMUVQUXsE5oCe68gi8SgfUk%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;219&quot; height=&quot;401&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;2195&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt; 개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;중복되는 코드를 줄이기 위해 개인 서버 프로젝트를 gradle을 활용한 subproject 구조로 잡았었다.&lt;br /&gt;새로 생성되는 subproject에서 중복 로직들을 모은 LIB project를 implementation 하여, 중복을 줄이고자 시도 중이다.&lt;br /&gt;문제는, 프로젝트가 추가될 때마다 LIB project의 코드를 추가해 주는 방식은, 목표했던 편의성과는 거리가 있다.&lt;br /&gt;이에 프로젝트 전체를 지속적으로 모니터링하여, 자동으로 최신화하는 방법을 모색했다.&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;목표는 수동으로 코드의 추가 없이, 신규 프로젝트의 설정 및 properties 등을 스크랩하여 추가할 수 있는 방안을 마련하는 것이다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AnnotationProcessor란&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;JAVA 5에서 신규 도입된 개념으로&lt;br /&gt;Compile 시점에 JAVA의 소스를 AST(Abstract Syntax tree) 구조로 참조할 수 있으며, 수정 및 생성이 가능하게 해주는 기능이다.&lt;br /&gt;AnnotationProcessor라는 이름처럼, annotation 기반으로 element를 참조한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;대표적으로 AnnotationProcessor를 활용한 library로 lombok, queryDSL 등이 있다.&lt;/blockquote&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;Abstract Syntax Tree란&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/X0fC8/btsCAMKgtSI/HgqbnLUq14JTImu57zkoIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/X0fC8/btsCAMKgtSI/HgqbnLUq14JTImu57zkoIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/X0fC8/btsCAMKgtSI/HgqbnLUq14JTImu57zkoIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FX0fC8%2FbtsCAMKgtSI%2FHgqbnLUq14JTImu57zkoIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;1354&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;1354&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;blockquote data-ke-style=&quot;style2&quot;&gt;AST란 소스코드를 트리구조로 추상화한 것이다.&lt;br /&gt;컴파일러에서 자주 사용되는 자료구조이며, 소스코드를 분석하는 데 사용된다.&lt;br /&gt;javac의 컴파일 단계에서 쓰이는 것은 물론이며, javascript의 lint, formatter등에서도 활용된다.&lt;br /&gt;더 깊게 들어가면 CFG(context-free-grammar), DOM(Document Object Model) 등 여러 개념들이 함께 사용된다. 더 자세한 사항은 다른 포스팅에서 다루도록 하겠다.&lt;br /&gt;이번 포스팅에서 &lt;b&gt;AST는 소스코드를 분석하여 트리구조화 시키는 자료구조 정도로 이해하고 넘어가겠다.&lt;/b&gt;&lt;/blockquote&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;javax.annotation.processing.Processor&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddGkbN/btsCzsFzjwd/lhIE74pmgRiyGRlCvhkZXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddGkbN/btsCzsFzjwd/lhIE74pmgRiyGRlCvhkZXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddGkbN/btsCzsFzjwd/lhIE74pmgRiyGRlCvhkZXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddGkbN%2FbtsCzsFzjwd%2FlhIE74pmgRiyGRlCvhkZXk%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;966&quot; height=&quot;760&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;AnnotationProcessor에서 제공하는 대표적인 interface 및 abstract class이다.&lt;br /&gt;일반적으로 AbstractProcessor를 구현한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;필수 구현 method로 process(Set &amp;lt;TypeElement&amp;gt;, RoundEnvironment)가 있다.&lt;br /&gt;compile 시점에 process method가 invoke 된다고 생각해도 무방하다.&lt;/blockquote&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;RoundEnvironment&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qnELi/btsCEHVWKEc/gG9AvkmZ1aKNMKOMfUBEeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qnELi/btsCEHVWKEc/gG9AvkmZ1aKNMKOMfUBEeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qnELi/btsCEHVWKEc/gG9AvkmZ1aKNMKOMfUBEeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqnELi%2FbtsCEHVWKEc%2FgG9AvkmZ1aKNMKOMfUBEeK%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;851&quot; height=&quot;350&quot; data-origin-width=&quot;851&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;annotated element를 받을 수 있는 evvironment이다.&lt;br /&gt;Annotation을 element를 찾아서, code generate 등 작업을 진행할 수 있도록 도와준다.&lt;/blockquote&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;ProcessingEnvironment&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;482&quot; data-origin-height=&quot;391&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xtSKr/btsCy7hchmf/08S8p8bu147OYVbJ25I5G0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xtSKr/btsCy7hchmf/08S8p8bu147OYVbJ25I5G0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xtSKr/btsCy7hchmf/08S8p8bu147OYVbJ25I5G0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxtSKr%2FbtsCy7hchmf%2F08S8p8bu147OYVbJ25I5G0%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;482&quot; height=&quot;391&quot; data-origin-width=&quot;482&quot; data-origin-height=&quot;391&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;processing을 위한 환경을 제공하는 인터페이스&lt;br /&gt;AbstractProcessor에서 기본적으로 구성해서 제공해 준다.&lt;br /&gt;이번에는 getFiler를 통해 filer를 불러와서 generated에 java 파일을 생성하려 한다.&lt;/blockquote&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;TypeElement (Element)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;1163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CZ3Yr/btsCzJNTbqa/xctiEmgxIMrK2fVMogkqwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CZ3Yr/btsCzJNTbqa/xctiEmgxIMrK2fVMogkqwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CZ3Yr/btsCzJNTbqa/xctiEmgxIMrK2fVMogkqwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCZ3Yr%2FbtsCzJNTbqa%2FxctiEmgxIMrK2fVMogkqwK%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;714&quot; height=&quot;1163&quot; data-origin-width=&quot;714&quot; data-origin-height=&quot;1163&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Element는 소스코드상의 type, parameter 등 여러 정보를 AST로 분석하여 제공해 준다.&lt;br /&gt;이번 요구사항에서는 크게 활용되지 않기 때문에 생략하겠다.&lt;br /&gt;Java Interface로 추상화해서 제공하기 때문에, 사용법은 비교적 간단하다. (코드 분석 및 제공은 Javac에서 해준다...)&lt;/blockquote&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;Google AutoService&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;This is an annotation processor library that helps us with generating&amp;nbsp;Java Service Provider Interface&amp;nbsp;(SPI) configuration files.&lt;br /&gt;...&lt;br /&gt;Google AutoService&amp;nbsp;is an open source code generator tool, developed under the Google Auto project. There are also two other tools besides AutoService:&amp;nbsp;AutoValue&amp;nbsp;and&amp;nbsp;AutoFactory.&lt;br /&gt;The purpose of this library is to save effort and time and, at the same time, to prevent misconfiguration.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;간단하게 구글에서 제공하는 AnnotationProcessor 라이브러리로, Java SPI 설정파일, manifest 등 메타파일을 자동으로 생성해 주는 역할을 한다.&lt;br /&gt;해당 라이브러리로 AnnotationProcessor를 구성하기 위한 작업을 간소화하고 안정성을 확보할 수 있다.&lt;br /&gt;물론 주의사항으로, 의존성이 복잡한 구조로 엮이는 경우 의도하지 못한 상황이 발생할 수 있다. 대표적으로 local, release, test 환경의 의존성 구조가 크게 달라지는 경우, 의존성의 순서와 관련하여 특정 processor가 load 하지 못하거나 실패하게 될 수 도 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AnnotationProcessor기반 serverInfo 수집 processor 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목표&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. compile 시점에 동작&lt;br /&gt;2. Project의 root로부터 하위 모든 file을 full scan&lt;br /&gt;3. 모든 yml file search 및 파싱&lt;br /&gt;4. spring.application.name 및 server.port 속성들을 찾아서 객체화&lt;br /&gt;5. name port 등을 속성으로 가지는 enum java파일 생성&lt;br /&gt;6. 다른 subproject에서 generate 된 enum 파일을 사용하여, 프로젝트 내 모든 serverInfo를 get 할 수 있음&lt;/blockquote&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;Gradle 구조&lt;/h3&gt;
&lt;pre id=&quot;code_1703429123788&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Root project 'pbear-root'
\--- Project ':pbear-spring'
     +--- Project ':pbear-spring:pbear-devtool'
     +--- Project ':pbear-spring:pbear-lib'
     \--- Project ':pbear-spring:pbear-sample'&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build.gradle (Root Project)&lt;/h4&gt;
&lt;pre id=&quot;code_1703430041230&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

dependencies {
	...

    if (project.name != 'pbear-devtool') {
        compileOnly project(':pbear-spring:pbear-devtool')
        annotationProcessor project(':pbear-spring:pbear-devtool')

        if (project.name != 'pbear-lib') {
            implementation project(':pbear-spring:pbear-lib')
        }
    }
}

...&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;AnnotationProcessor가 동작하는 project인 pbear-devtool 생성&lt;br /&gt;devtool은 annotationProcessor를 구현할 library가 될 것이며 추후 다른 기능을 추가하기 위해 모든 프로젝트에서 의존성을 갖게 한다.&lt;br /&gt;또한 pbear-lib도 모든 project에서 의존성을 가지게 되는 library성 프로젝트이다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build.gradle (devtool)&lt;/h4&gt;
&lt;pre id=&quot;code_1703430896316&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version = '0.0.1'

bootJar { enabled = false }

dependencies {
    implementation 'org.yaml:snakeyaml:2.2'
    implementation 'com.google.auto.service:auto-service:1.1.1'
    annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;dependencies로&lt;br /&gt;1. yml 파싱을 위한 snakeyaml&lt;br /&gt;2. annotationProcessor meta 생성을 위한 Google의 auto-service&lt;br /&gt;Main Class가 없기 때문에, spring boot plugins의 bootjar는 disable&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build.gradle (lib)&lt;/h4&gt;
&lt;pre id=&quot;code_1703431008021&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

tasks.named('compileJava') {
    it.dependsOn('clean')
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;지속적으로 serverInfo가 최신화되기 위한 compileJava의 clean 의존성 설정&lt;/blockquote&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;Processor 구현&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Custom Annotation Class&lt;/h4&gt;
&lt;pre id=&quot;code_1703431153211&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public @interface EnablePBearServerInfoDevtool {
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;적용되는 프로젝트를 특정하기 위한 annotation class&lt;/blockquote&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;Processor&lt;/h3&gt;
&lt;pre id=&quot;code_1703432131274&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SupportedAnnotationTypes(&quot;com.pbear.devtool.annotation.EnablePBearServerInfoDevtool&quot;)
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@AutoService(Processor.class)
@SuppressWarnings({&quot;unused&quot;})
public class ServerProperitesProcessor extends AbstractProcessor {
  private static final String SERVER_ENUM_PACKAGE_NAME = &quot;com.pbear.devtool&quot;;
  private static final String SERVER_ENUM_CLASS_NAME = &quot;Server&quot;;

  @Override
  public boolean process(Set&amp;lt;? extends TypeElement&amp;gt; annotations, RoundEnvironment roundEnv) {
    roundEnv.getElementsAnnotatedWith(EnablePBearServerInfoDevtool.class)
        .stream()
        .filter(element -&amp;gt; element.getKind() == ElementKind.CLASS)
        .findFirst()
        .ifPresent(element -&amp;gt; this.createServerResource((TypeElement) element));

    return true;
  }

  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기본 구성&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;@SupportedAnnotationTypes(&quot;com.pbear.devtool.annotation.EnablePBearServerInfoDevtool&quot;)&lt;br /&gt;@SupportedSourceVersion(SourceVersion.RELEASE_17)&lt;br /&gt;로 target annotation 및 java version 지정&lt;br /&gt;&lt;br /&gt;Google AutoService 선언&lt;br /&gt;&lt;br /&gt;AbstractProcessor extends&lt;br /&gt;&lt;br /&gt;EnablePBearServerInfoDevtool 어노테이션이 하나라도 있으면 실행이 되며  비즈니스로직이 실행된다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;createServerResource()&lt;/h4&gt;
&lt;pre id=&quot;code_1703432595248&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void createServerResource(TypeElement typeElement) {
    try {
      System.out.println(typeElement.getQualifiedName());
      File currentDir = new File(System.getProperty(&quot;user.dir&quot;));
      File rootDir = this.getRootProjectDirectory(currentDir);
      System.out.println(&quot;root dir:&quot; + rootDir.getCanonicalPath());
      List&amp;lt;File&amp;gt; ymlFileList = this.matchChildFileExtension(rootDir);
      List&amp;lt;ServerInfo&amp;gt; serverInfoList = this.getSpringServerInfoList(ymlFileList);
      System.out.println(serverInfoList);
      this.writeServerInfoEnum(serverInfoList);

    } catch (IOException e) {
      throw new RuntimeException(e);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 실행되는 현 directory를 기준으로 root를 찾는다.&lt;br /&gt;2. root를 기준으로 하위 디렉터리를 순회하며 yml을 찾는다. (nio files 활용)&lt;br /&gt;3. spring.application.name 및 server.port를 찾아서 ServerInfo 생성&lt;br /&gt;4. enum file 작성&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1703432907919&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void writeServerInfoEnum(final List&amp;lt;ServerInfo&amp;gt; serverInfoList) throws IOException {
    FileObject fileObject = processingEnv
        .getFiler()
        .createSourceFile(SERVER_ENUM_PACKAGE_NAME + &quot;.&quot; + SERVER_ENUM_CLASS_NAME);
    try (PrintWriter out = new PrintWriter(fileObject.openWriter())) {
      String tab = &quot;  &quot;;

      // package com.pbear.devtool;
      out.print(&quot;package &quot;);
      out.print(SERVER_ENUM_PACKAGE_NAME);
      out.println(&quot;;&quot;);
      out.println();

      // public enum Server {
      out.print(&quot;public enum &quot;);
      out.print(SERVER_ENUM_CLASS_NAME);
      out.println(&quot; {&quot;);

      ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;AbstartProcessor에서 제공하는 processingEnv를 통해 generated에 sourceFile을 작성했다.&lt;br /&gt;작성은 PrintWriter를 통해 손수 작성...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;devtool 사용&lt;/h2&gt;
&lt;pre id=&quot;code_1703433198948&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.pbear.lib;

import com.pbear.devtool.annotation.EnablePBearServerInfoDevtool;

@EnablePBearServerInfoDevtool
public class ServerInfoDevtool {
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;간단하게 Annotation을 달아서 사용&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;562&quot; data-origin-height=&quot;543&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d2AFX9/btsCybEuJhx/uURJVQYe421VVyTY9Kb7rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d2AFX9/btsCybEuJhx/uURJVQYe421VVyTY9Kb7rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d2AFX9/btsCybEuJhx/uURJVQYe421VVyTY9Kb7rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd2AFX9%2FbtsCybEuJhx%2FuURJVQYe421VVyTY9Kb7rk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;562&quot; height=&quot;543&quot; data-origin-width=&quot;562&quot; data-origin-height=&quot;543&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;compile 후 build/generated/annotationProcessor/java 밑으로 java 파일이 생성된 것을 확인할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Next&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;처음 그린 그림은 zookeeper를 통한 service registry를 사용하는 구성이었다.&lt;br /&gt;Service Registry를 통해 msa구조를 잡으면 cloud 환경에서는 적은 수고로 서버 간 연동 및 오토스케일링을 대응할 수 있다.&lt;br /&gt;하지만 로컬 서버 하나 운영하는 상황에서 이는 상당히 불필요한 작업이다.&lt;br /&gt;1. 개발환경에서 zookeeper에 대한 대응을 해야 한다.&lt;br /&gt;2. 동적으로 서버를 discovery 할 수 있는 장점이, 1 서버 구성에서는 무의미하다.&lt;br /&gt;3. zookeeper를 관리해야 한다.&lt;br /&gt;&lt;br /&gt;이에, 이런 부분을 대체하기 위해 AnnotationProcessor를 구현해서 Server 정보를 빌드 시점에 모아서 쓸 수 있도록 시도 중이다.&lt;br /&gt;하지만, 이러면 앞단의 gateway는 서버가 추가될 때마다 다시 빌드해서 배포해야 하는 상황이 발생할 것이다.&lt;br /&gt;이에 대한 해결책은 아직 고민 중이다.&lt;br /&gt;&lt;br /&gt;일단 다음은 일단 github에 올라가면 안 되는 secret properties를 배포 시점에 로드하는 구성을 lib에 구현하고자 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://taes-k.github.io/2021/04/18/java-annotation-processing/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://taes-k.github.io/2021/04/18/java-annotation-processing/&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://github.com/taes-k/sample-annotation-processing&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/taes-k/sample-annotation-processing&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://www.baeldung.com/java-annotation-processing-builder&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.baeldung.com/java-annotation-processing-builder&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>[시즌2] 개인서버 개발/시즌2 설계(완)</category>
      <category>abstract syntax tree</category>
      <category>annotationProcessor</category>
      <category>AST</category>
      <category>java</category>
      <category>lombok</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/75</guid>
      <comments>https://p-bear.tistory.com/75#entry75comment</comments>
      <pubDate>Mon, 25 Dec 2023 01:13:53 +0900</pubDate>
    </item>
    <item>
      <title>Gradle Docker build (feat. Jenkins CI/CD)</title>
      <link>https://p-bear.tistory.com/74</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HB9sw/btsCiMkFlhC/0GVUtDInKE9gqafZXlhe10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HB9sw/btsCiMkFlhC/0GVUtDInKE9gqafZXlhe10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HB9sw/btsCiMkFlhC/0GVUtDInKE9gqafZXlhe10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHB9sw%2FbtsCiMkFlhC%2F0GVUtDInKE9gqafZXlhe10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;600&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;600&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Jenkins에 CI / CD를 구성하는 작업은&lt;br /&gt;신규 프로젝트 생성시마다 해줘야하는 귀찮은 작업 중 하나다.&lt;br /&gt;또한 개발 / 배포 환경 상 자주 바뀌는 부분도 없고, 대부분 비슷하게 동작한다.&lt;br /&gt;이러한 공통부분을 묶어서, 하나의 CI / CD로 모든 project (gradle subproject)를 빌드 배포할 수 있도록 구성하려 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gradle docker build&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;'com.bmuschko.docker-spring-boot-application'는 docker build를 지원하는 gradle plugin이다.&lt;br /&gt;이를 활용하여, gradle의 task를 통해 docker image 생성까지 구성하고자 한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;docker image 생성을 gradle에서 하는 이유?&lt;br /&gt;기본적으로 docker image 생성 시 project의 name 및 version이 필요하다.&lt;br /&gt;project name은 jenkins에서 argument로 받는다고 해도, gradle의 project.version을 jenkins에서 가져오기에는 복잡하고 효율적이지 않다고 판단했다.&lt;br /&gt;따라서 gradle에서 docker image build까지 담당하는 것이 깔끔할 것이라 판단했다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;gradle-docker-plugin (bmuschko)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;231&quot; data-origin-height=&quot;105&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sHWU3/btsCj3zB0SE/uWySwAc2Z68HcfHbd70cEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sHWU3/btsCj3zB0SE/uWySwAc2Z68HcfHbd70cEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sHWU3/btsCj3zB0SE/uWySwAc2Z68HcfHbd70cEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsHWU3%2FbtsCj3zB0SE%2FuWySwAc2Z68HcfHbd70cEK%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;231&quot; height=&quot;105&quot; data-origin-width=&quot;231&quot; data-origin-height=&quot;105&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;docker-spring-boot-application plugin은 4가지 task를 지원한다. (상세 스펙은 reference 참조)&lt;br /&gt;dockerCreateDockerfile&lt;br /&gt;&amp;nbsp;- dockerfile을 작성할 수 있도록 도와주는 task이다. &lt;br /&gt;&amp;nbsp;- springBootApplication Object로 baseImage, jvmArgs등을 설정할 수 있도록 도와준다.&lt;br /&gt;dockerBuildImage&lt;br /&gt;&amp;nbsp;- docker Image를 build하는 task이다.&amp;nbsp;&lt;br /&gt;&amp;nbsp;- dependsOn(dockerCreateDockerfile)이며 앞 task에서 생성되는 Dockerfile을 기반으로 docker image를 build한다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703051383490&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker {
    springBootApplication {
        baseImage = 'openjdk:8-alpine'
        ports = [9090, 8080]
        images = ['awesome-spring-boot:1.115', 'awesome-spring-boot:latest']
        jvmArgs = ['-Dspring.profiles.active=production', '-Xmx2048m']
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Guild에 나오는 간단 세팅이다.&lt;br /&gt;기본적으로 SpringBootApplication annotation을 찾아서 main으로 세팅하고&lt;br /&gt;부가적인 정보를 springBootApplication Object로 받아서 추가한다.&lt;/blockquote&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;Read yml in gradle docker task&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;[root] gradle.build&lt;/h4&gt;
&lt;pre id=&quot;code_1703051646924&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.yaml.snakeyaml.Yaml

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath('org.yaml:snakeyaml:2.0')
    }
}

plugins {
    id 'com.bmuschko.docker-spring-boot-application' version '9.4.0'
}

...

subprojects {
    ...

    docker {
        dockerCreateDockerfile.doFirst {
            def appConfig = new Yaml().load( project.file('src/main/resources/application.yml').newInputStream() )
            springBootApplication {
                baseImage = 'openjdk:17'
                ports = [appConfig.server.port, appConfig.server.port]
                images = [&quot;${project.name}:${project.version}&quot;, &quot;${project.name}:latest&quot;]
                jvmArgs = ['-Dspring.profiles.active=release', '-Duser.timezone=Asia/Seoul']
            }
        }
        dockerBuildImage.dependsOn('build')
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;plugin user guide의 sample code를 그대로 사용하기에는 문제가 있다.&lt;br /&gt;현 project 계획은 root 밑으로 subprojects들이 있고 jenkins에서 ci / cd의 task에 subproject를 지정하여 빌드 및 배포를 하려한다.&lt;br /&gt;따라서 예시처럼, 각 build.gradle마다 port 등 을 지정하는 방식은 쓸 수 없다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;task 실행 시점에 해당 프로젝트의 설정을 읽어오는 방식으로 적용하고자 한다.&lt;br /&gt;spring의 application.yml을 읽어서 server.port 값을 세팅하도록 설정했다.&lt;br /&gt;snakeyaml library로 yml을 파싱하여 값을 추출했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Jenkins CI / CD 구성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CI&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build parameter&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;616&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5WTEf/btsCg6DOD9J/UIupd7jK1dGPTx2XraID0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5WTEf/btsCg6DOD9J/UIupd7jK1dGPTx2XraID0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5WTEf/btsCg6DOD9J/UIupd7jK1dGPTx2XraID0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5WTEf%2FbtsCg6DOD9J%2FUIupd7jK1dGPTx2XraID0K%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;1278&quot; height=&quot;616&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;616&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;빌드할 project를 선택할 수 있도록 parameter 구성&lt;br /&gt;프로젝트가 추가될 때 마다. Choices를 추가해주어야 한다...&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Build Steps&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RjnQr/btsCk6iqyrS/jldKLmwidiK7EsuzGfjAk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RjnQr/btsCk6iqyrS/jldKLmwidiK7EsuzGfjAk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RjnQr/btsCk6iqyrS/jldKLmwidiK7EsuzGfjAk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRjnQr%2FbtsCk6iqyrS%2FjldKLmwidiK7EsuzGfjAk1%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;1276&quot; height=&quot;514&quot; data-origin-width=&quot;1276&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;gradle 레퍼로 실행&lt;br /&gt;build parameter로 dockerBuildImage task execute&lt;br /&gt;이는 clean -&amp;gt; ... -&amp;gt; build -&amp;gt; dockerCreateDockerfile -&amp;gt; dockerBuildImage 순으로 진행된다.&lt;/blockquote&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;CD&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;build parameter&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;839&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOj91W/btsClJ1FVY8/dICHY0yHs6z4xDuBiT6D31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOj91W/btsClJ1FVY8/dICHY0yHs6z4xDuBiT6D31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOj91W/btsClJ1FVY8/dICHY0yHs6z4xDuBiT6D31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOj91W%2FbtsClJ1FVY8%2FdICHY0yHs6z4xDuBiT6D31%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;1282&quot; height=&quot;839&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;839&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CD 역시 build할 project와 버전을 parameter로 받는다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Build Steps&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;373&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/srbro/btsColy7GOQ/oqypZZFuuZUDfnjCKRFQ9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/srbro/btsColy7GOQ/oqypZZFuuZUDfnjCKRFQ9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/srbro/btsColy7GOQ/oqypZZFuuZUDfnjCKRFQ9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsrbro%2FbtsColy7GOQ%2FoqypZZFuuZUDfnjCKRFQ9K%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;1266&quot; height=&quot;373&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;373&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh1OZr/btsCk8N5lkB/ng9fMugebSYnlrkoU9Vif1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh1OZr/btsCk8N5lkB/ng9fMugebSYnlrkoU9Vif1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh1OZr/btsCk8N5lkB/ng9fMugebSYnlrkoU9Vif1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh1OZr%2FbtsCk8N5lkB%2Fng9fMugebSYnlrkoU9Vif1%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;1270&quot; height=&quot;629&quot; data-origin-width=&quot;1270&quot; data-origin-height=&quot;629&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1269&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKoCdr/btsCoBV1zfn/x1cLyt7EeWwyutptTxQHPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKoCdr/btsCoBV1zfn/x1cLyt7EeWwyutptTxQHPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKoCdr/btsCoBV1zfn/x1cLyt7EeWwyutptTxQHPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKoCdr%2FbtsCoBV1zfn%2Fx1cLyt7EeWwyutptTxQHPK%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;1269&quot; height=&quot;375&quot; data-origin-width=&quot;1269&quot; data-origin-height=&quot;375&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Jenkins의 Docker Build Plugin으로 로컬 이미지를 컨테이너 run하는 방식&lt;br /&gt;복잡한 구성은 최대한 root의 gradle.build로 빼는 방향으로 develop할 예정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Next..&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2024에는 각 project들의 호흡을 짧게 가져가려고 한다.&lt;br /&gt;인프라는 기존에 있던, RDB, Kafka, Redis, MongoDB 등을 활용한다.&lt;br /&gt;다음 작업 사항은 각 인프라에 접속하는 정보를 제공하는 provider를 lib에 구현하고자 한다.&lt;br /&gt;이후 프로젝트는 lib를 implementation하여 provider를 통해 접속정보를 포함한 config를 얻을 수 있을 것이다.&lt;br /&gt;이는 신규 프로젝트를 손쉽게 시작할 수 있게 할것이라 기대한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/bmuschko/gradle-docker-plugin&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/bmuschko/gradle-docker-plugin&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://plugins.gradle.org/plugin/com.bmuschko.docker-spring-boot-application&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://plugins.gradle.org/plugin/com.bmuschko.docker-spring-boot-application&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bmuschko.github.io/gradle-docker-plugin/current/user-guide/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bmuschko.github.io/gradle-docker-plugin/current/user-guide/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://bmuschko.github.io/gradle-docker-plugin/current/user-guide/#spring-boot-plugin-extension&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://bmuschko.github.io/gradle-docker-plugin/current/user-guide/#spring-boot-plugin-extension&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>[시즌2] 개인서버 개발/시즌2 설계(완)</category>
      <category>Docker</category>
      <category>gradle</category>
      <category>Jenkins</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/74</guid>
      <comments>https://p-bear.tistory.com/74#entry74comment</comments>
      <pubDate>Fri, 22 Dec 2023 08:01:24 +0900</pubDate>
    </item>
    <item>
      <title>Multi-project 구조 설계 (Gradle)</title>
      <link>https://p-bear.tistory.com/73</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q3Fvi/btsCokfynIj/5o4xTCX4XVtCYPGcgYqVm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q3Fvi/btsCokfynIj/5o4xTCX4XVtCYPGcgYqVm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q3Fvi/btsCokfynIj/5o4xTCX4XVtCYPGcgYqVm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq3Fvi%2FbtsCokfynIj%2F5o4xTCX4XVtCYPGcgYqVm0%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;380&quot; height=&quot;132&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;132&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;지난 시즌 개발 중 신규 프로젝트 시작할 때 큰 귀찮음이 있었다.&lt;br /&gt;1. 프로젝트마다 중복되는 코드: log 설정, db설정, kafka 설정, 등등..&lt;br /&gt;2. 프로젝트마다 중복되는 작업: github, jenkins CI/CD, docker config 등등..&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이런 귀찮음은 결국 신규 프로젝트를 시작할 때 부담으로 작용하게 되었고&lt;br /&gt;간단한 아이디어를 테스트 하기에 좋지 못했다.&lt;br /&gt;이번 시즌에는 이러한 부분을 사전에 방지하고자 한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이 글은 Gradle의 Multi-Project 구조를 통해&lt;br /&gt;이러한 문제상황을 해결해보려 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Multi-Project 구조 설계&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Gradle은 Multi-Project 구조를 지원한다.&lt;br /&gt;이번 프로젝트는 2-depth 이상으로 구성하려 한다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703048058087&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Root project 'pbear-root'
\--- Project ':pbear-spring'
     +--- Project ':pbear-spring:pbear-lib'
     \--- Project ':pbear-spring:pbear-sample'&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Project 구조이다.&lt;br /&gt;전체적으로 java 프로젝트를 대상으로 하며&lt;br /&gt;일단 Spring Project를 1-depth로 잡았다.&lt;br /&gt;2-depth부터 개발이 진행될 프로젝트들이 나열되는데&lt;br /&gt;기본적으로 lib를 두고, spring-project에서 사용될 공통 코드 및 설정을 implementation 해서 사용하고자 한다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[root] setting.gradle&lt;/h3&gt;
&lt;pre id=&quot;code_1703048210386&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rootProject.name = 'pbear-root'

include 'pbear-spring'
include 'pbear-spring:pbear-lib'
include 'pbear-spring:pbear-sample'&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;특이사항은 없으며&lt;br /&gt;하위 프로젝트의 build.gradle을 include 시켰다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[root] build.gradle&lt;/h3&gt;
&lt;pre id=&quot;code_1703048291714&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;group = 'com.pbear'

subprojects {
    apply plugin: 'java'
    apply plugin: 'java-library'

    sourceCompatibility = 17
    targetCompatibility = 17

    dependencies {
        compileOnly 'org.projectlombok:lombok:1.18.28'
        annotationProcessor 'org.projectlombok:lombok:1.18.28'
    }

    repositories {
        mavenCentral()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;subprojects를 통해 하위 프로젝트에 적용될 plugin, dependencies 및 repositories를 설정했다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[pbear-spring] build.gradle&lt;/h3&gt;
&lt;pre id=&quot;code_1703048364937&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;plugins {
    id 'org.springframework.boot' version '3.2.0' apply false
    id 'io.spring.dependency-management' version '1.1.4' apply false
}

ext {
    set('springCloudVersion', &quot;2023.0.0&quot;)
}

subprojects {
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    dependencyManagement {
        imports {
            mavenBom &quot;org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}&quot;
        }
    }

    test {
        useJUnitPlatform()
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1-depth의 spring을 주제로 묶은 프로젝트이다.&lt;br /&gt;여기서 spring의 dependencies를 통합해서 관리하고자 한다.&lt;br /&gt;기본적으로 spring의 dependency-management 플러그인을 사용하고, spring boot 및 spring cloud version을 관리할 수 있도록 세팅했다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[pbear-lib]&amp;nbsp; build.gradle&lt;/h3&gt;
&lt;pre id=&quot;code_1703048489538&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version = '0.0.1'

dependencies {
    implementation 'org.springframework.boot:spring-boot-autoconfigure'
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2-depth의 Library에 해당하는 프로젝트이다.&lt;br /&gt;추후 추가될 예정이며, 여기부터는 project version 관리를 한다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[pbear-sample] build.gradle&lt;/h3&gt;
&lt;pre id=&quot;code_1703048574115&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version = '0.0.0'

dependencies {
    implementation project(':pbear-spring:pbear-lib')

    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;REST echo기능을 하나 넣은 sample project이다.&lt;br /&gt;추후 추가되는 프로젝트는 위처럼 pbear-lib를 implementation을 해서 사용한다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;위 프로젝트 구조를 통해&lt;br /&gt;프로젝트 추가 시 낭비되는 작업들을 최소화시킬 수 있을 것이라 판단된다.&lt;br /&gt;ex) github project 추가, spring dependency 작업, 중복 기능 구현, 설정 등&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Common logback 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[pbear-lib] logback.xml&lt;/h3&gt;
&lt;pre id=&quot;code_1703049115053&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Root project 'pbear-root'
\--- Project ':pbear-spring'
     +--- Project ':pbear-spring:pbear-lib'
          \--- src/main/resources/logback.xml
     \--- Project ':pbear-spring:pbear-sample'&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1703049045176&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;
&amp;lt;configuration&amp;gt;
    &amp;lt;springProperty name=&quot;APP_NAME&quot; source=&quot;spring.application.name&quot;/&amp;gt;

    &amp;lt;springProfile name=&quot;local&quot;&amp;gt;
        &amp;lt;property name=&quot;FILE_PATH&quot; value=&quot;/users/xxxxx/workspace/logs&quot;/&amp;gt;
        &amp;lt;property name=&quot;FILE_NAME&quot; value=&quot;${APP_NAME}&quot;/&amp;gt;
    &amp;lt;/springProfile&amp;gt;

    &amp;lt;springProfile name=&quot;release&quot;&amp;gt;
        &amp;lt;property name=&quot;FILE_PATH&quot; value=&quot;/logs&quot;/&amp;gt;
        &amp;lt;property name=&quot;FILE_NAME&quot; value=&quot;${APP_NAME}&quot;/&amp;gt;
    &amp;lt;/springProfile&amp;gt;

    &amp;lt;appender name=&quot;STDOUT&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&amp;gt;
        &amp;lt;encoder&amp;gt;
            &amp;lt;pattern&amp;gt;%d{yyyyMMdd HH:mm:ss} [%thread] %-5level%logger{5} - %msg %n&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;appender name=&quot;RollingFileAppender&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&amp;gt;
        &amp;lt;file&amp;gt;${FILE_PATH}/${FILE_NAME}.log&amp;lt;/file&amp;gt;

        &amp;lt;encoder&amp;gt;
            &amp;lt;pattern&amp;gt;%d{yyyyMMdd HH:mm:ss} [%thread] %-5level%logger{5} - %msg %n&amp;lt;/pattern&amp;gt;
        &amp;lt;/encoder&amp;gt;

        &amp;lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&amp;gt;
            &amp;lt;fileNamePattern&amp;gt;${FILE_PATH}/${FILE_NAME}.log.%d{yyyy-MM-dd}_%i.gz&amp;lt;/fileNamePattern&amp;gt;
            &amp;lt;timeBasedFileNamingAndTriggeringPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP&quot;&amp;gt;
                &amp;lt;maxFileSize&amp;gt;1GB&amp;lt;/maxFileSize&amp;gt;
            &amp;lt;/timeBasedFileNamingAndTriggeringPolicy&amp;gt;
            &amp;lt;maxHistory&amp;gt;30&amp;lt;/maxHistory&amp;gt;
        &amp;lt;/rollingPolicy&amp;gt;
    &amp;lt;/appender&amp;gt;

    &amp;lt;springProfile name=&quot;local&quot;&amp;gt;
        &amp;lt;root level=&quot;INFO&quot;&amp;gt;
            &amp;lt;appender-ref ref=&quot;STDOUT&quot; /&amp;gt;
        &amp;lt;/root&amp;gt;
    &amp;lt;/springProfile&amp;gt;

    &amp;lt;springProfile name=&quot;release&quot;&amp;gt;
        &amp;lt;root level=&quot;INFO&quot;&amp;gt;
            &amp;lt;appender-ref ref=&quot;RollingFileAppender&quot; /&amp;gt;
        &amp;lt;/root&amp;gt;
    &amp;lt;/springProfile&amp;gt;
&amp;lt;/configuration&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;공통 library에 자주 사용하는 logback을 넣고 다른 project에서 implementation 하면&lt;br /&gt;실제 기동 시 해당 logback이 적용된다.&lt;br /&gt;변수는 모두 spring properties로 받아온다.&lt;br /&gt;&lt;br /&gt;이로서 신규 프로젝트는 로깅에 대한 설정을 스킵할 수 있다.&lt;br /&gt;또한 log관련 변동사항이 다 같이 적용되고, log의 형식도 통일할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Next..&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;신규 프로젝트마다. CI / CD설정하고, docker 설정 만드는 것도 많이 귀찮은 작업이었다.&lt;br /&gt;프로젝트 구조를 잡았으니&lt;br /&gt;다음은 Jenkins를 통해 CI / CD를 공통화할 계획이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.gradle.org/current/userguide/multi_project_builds.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.gradle.org/current/userguide/multi_project_builds.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>[시즌2] 개인서버 개발/시즌2 설계(완)</category>
      <category>gradle</category>
      <category>gradle build</category>
      <category>multi-project</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/73</guid>
      <comments>https://p-bear.tistory.com/73#entry73comment</comments>
      <pubDate>Thu, 21 Dec 2023 08:16:21 +0900</pubDate>
    </item>
    <item>
      <title>Jenkins Docker Builder 연동 이슈 (connection refused)</title>
      <link>https://p-bear.tistory.com/72</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIUvOZ/btsCg1aB9vn/LFPUpwVfkIkfCBD4k0Dcd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIUvOZ/btsCg1aB9vn/LFPUpwVfkIkfCBD4k0Dcd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIUvOZ/btsCg1aB9vn/LFPUpwVfkIkfCBD4k0Dcd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIUvOZ%2FbtsCg1aB9vn%2FLFPUpwVfkIkfCBD4k0Dcd1%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;225&quot; height=&quot;225&quot; data-origin-width=&quot;225&quot; data-origin-height=&quot;225&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Jenkins는 CI / CD를 구성할 수 있는 강력한 툴이다.&lt;br /&gt;Docker는 환경을 포함해서 Application을 이미지화 할 수 있는 툴이며&lt;br /&gt;Jenkins를 통해 docker로 배포하는 방법은 많이 활용되는 조합이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 발생&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기존 배포 방식&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반적인 배포방식으로는&lt;br /&gt;1. Jenkins에서 build 및 docker image 생성&lt;br /&gt;2. docker image를 DockerHub로 업로드&lt;br /&gt;3. 배포 받을 서버에 dockerHub에서 특정 이미지를 받아서 실행하도록 command&lt;br /&gt;이 있다.&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;2023 개인서버에는 위 로직을 통해 배포를 진행했었다.&lt;br /&gt;넥스트 계획의 핵심은, 중복되는 작업들을 최대한 자동화 시키고 요구사항에서 벗어나는 확장성을 줄이고자한다.&lt;br /&gt;이에, 서버 한곳에서 빌드 및 배포가 진행될 것이기에 로직을 단순화 시켰다.&lt;br /&gt;1. Jenkins에서 build 및 docker image 생성&lt;br /&gt;2. 서버에 Docker Command를 통해 컨테이너 생성 및 run&lt;/blockquote&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;Jenkins Docker Plugin 적용&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1353&quot; data-origin-height=&quot;507&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctsw1f/btsCk7HDZKN/yLuKlMaIGzUyrnmtKRR0Mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctsw1f/btsCk7HDZKN/yLuKlMaIGzUyrnmtKRR0Mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctsw1f/btsCk7HDZKN/yLuKlMaIGzUyrnmtKRR0Mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fctsw1f%2FbtsCk7HDZKN%2FyLuKlMaIGzUyrnmtKRR0Mk%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;1353&quot; height=&quot;507&quot; data-origin-width=&quot;1353&quot; data-origin-height=&quot;507&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;단순화된 배포방식으로, 기존 복잡한 Jenkins File대신&lt;br /&gt;Jenkins Plugin을 최대한 활용하기로 결정&lt;/blockquote&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;Connection Refuse 발생&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/969qG/btsCguD9mCn/ousE4XmZtXct53e64YWM9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/969qG/btsCguD9mCn/ousE4XmZtXct53e64YWM9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/969qG/btsCguD9mCn/ousE4XmZtXct53e64YWM9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F969qG%2FbtsCguD9mCn%2FousE4XmZtXct53e64YWM9K%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;1598&quot; height=&quot;295&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1703033764088&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Something went wrong, cannot connect to unix:///var/run/docker.sock, cause: org.apache.hc.client5.http.HttpHostConnectException: Connect to unix://localhost:2375 [localhost/127.0.0.1] failed: Connection refused&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;포럼에서 Jenkins는 Docker Builder 설정을 수동으로 해야한다고 가이드하여&lt;br /&gt;해당 설정을 하면, docker에서 connection refuse가 떨어진다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703034276901&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;netstat -tnlp | grep 2375

/etc/docker# docker --version
Docker version 23.0.4, build f480fb1&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;확인해보니 2375로 LISTEN중인 어플리케이션이 없었다.&lt;br /&gt;docker는 apt-get으로 설치한후 mount point 변경 등 작업을 한 상황이다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703035211754&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/etc/docker/damon.json

{
  &quot;log-driver&quot;: &quot;json-file&quot;,
  &quot;log-opts&quot;: {
    &quot;max-size&quot;: &quot;10m&quot;,
    &quot;max-file&quot;: &quot;3&quot;
  },
  &quot;hosts&quot;: [&quot;unix:///var/run/docker.sock&quot;, &quot;tcp://0.0.0.0:2375&quot;]
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;docker에서 REST API가 열리지 않은 것으로 판단되여&lt;br /&gt;docker reference를 통해 설정을 진행했다.&lt;br /&gt;&quot;hosts&quot;: [&quot;unix:///var/run/docker.sock&quot;, &quot;tcp://0.0.0.0:2375&quot;]를 json 설정파일에 추가했다.&lt;/blockquote&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;Service Run Fail 발생&lt;/h3&gt;
&lt;pre id=&quot;code_1703035384868&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;systemctl restart docker
Job for docker.service failed because the control process exited with error code.
See &quot;systemctl status docker.service&quot; and &quot;journalctl -xeu docker.service&quot; for details.&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;docker service가 실패했으며&lt;br /&gt;journal을 확인해 보니&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703035483489&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;journalctl -xeu docker.service
...

.... dockerd[2153802]: unable to configure the Docker daemon with file /etc/docker/daemon.json: the following directives are specified both as a flag and in the configuration file: hosts: (from fla&amp;gt;
.... systemd[1]: docker.service: Main process exited, code=exited, status=1/FAILURE

...&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;specified both as a flag and in the configuration file: hosts&lt;br /&gt;해당 문구로 볼때, docker.service 파일에서 hosts관련 옵션이 있는것으로 추정&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703035589756&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/lib/systemd/system/docker.service

...
[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutStartSec=0
RestartSec=2
Restart=always
...&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;역시 dockerd execute할 때 -H 옵션이 있었다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1703035653044&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;systemctl daemon-reload
systemctl start docker

// docker service status 확인
root@m-H110M4-C2D:/etc/docker# systemctl status docker
● docker.service - Docker Application Container Engine
     Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
     Active: active (running) since Wed 2023-12-20 10:27:46 KST; 1min 3s ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 2153904 (dockerd)
      Tasks: 139
     Memory: 58.6M
        CPU: 4.071s
     CGroup: /system.slice/docker.service
             ├─2153904 /usr/bin/dockerd --containerd=/run/containerd/containerd.sock
             ├─2154352 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 9021 -container-ip 172.29.0.3 -container-port 9021
             ├─2154360 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 9021 -container-ip 172.29.0.3 -container-port 9021
             ...

// LISTEN port 확인
netstat -tnlp | grep 2375
tcp6       0      0 :::2375                 :::*                    LISTEN      2153904/dockerd&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;1598&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dHWpyW/btsCiK7BmbB/Inwq3hFU08NhqkRmxQR4fk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dHWpyW/btsCiK7BmbB/Inwq3hFU08NhqkRmxQR4fk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dHWpyW/btsCiK7BmbB/Inwq3hFU08NhqkRmxQR4fk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdHWpyW%2FbtsCiK7BmbB%2FInwq3hFU08NhqkRmxQR4fk%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;1598&quot; height=&quot;304&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Jenkins 연동도 정상적으로 작동 확인&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/72</guid>
      <comments>https://p-bear.tistory.com/72#entry72comment</comments>
      <pubDate>Wed, 20 Dec 2023 10:31:09 +0900</pubDate>
    </item>
    <item>
      <title>Micrometer Log Tracing (feat. Spring Boot 3)</title>
      <link>https://p-bear.tistory.com/71</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;252&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGrDOV/btsBXSFfBUe/O7kBrg6Bx6ECd5wIJJCCgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGrDOV/btsBXSFfBUe/O7kBrg6Bx6ECd5wIJJCCgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGrDOV/btsBXSFfBUe/O7kBrg6Bx6ECd5wIJJCCgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGrDOV%2FbtsBXSFfBUe%2FO7kBrg6Bx6ECd5wIJJCCgk%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;252&quot; height=&quot;252&quot; data-origin-width=&quot;252&quot; data-origin-height=&quot;252&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Micrometer란&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Vendor-neutral application observability facade&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;micrometer는 observability의 파사드(고수준 인터페이스)를 제공해주는 프로젝트이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;observability는 Application의 가시성을 제공해주는 것을 말하며, 이는 Application 내에서 발생하는 process를 외부로 제공해주는 것 이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JVM -based Application라면 ventdor에 상관없이 적용할 수 있다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;MicrometerLogTracing-ProjectDevelopHistory&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Project Develop History&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2016 [Spring] Create tracing library(Spring Cloud Sleuth) from Spring Cloud team&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2017 [Micrometer] Micrometer Project 시작 (only for jvm based application observation)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2018 [Micrometer] 1.0.0 RELEASE&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2022.1 [Micrometer-tracing] 1.0.0-M1 RELEASE&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2022.11 [Micrometer] 1.10.0 (GA version) RELESE&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2022.11 [Micrometer-tracing] 1.0.0 RELEASE &amp;rarr; Spring Cloud Sleuth를 흡수&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;hellip;&lt;/span&gt;&lt;/blockquote&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;624&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fhvay/btsBYbRWJwG/cxKTB3PxNXcKosF65D12s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fhvay/btsBYbRWJwG/cxKTB3PxNXcKosF65D12s1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fhvay/btsBYbRWJwG/cxKTB3PxNXcKosF65D12s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFhvay%2FbtsBYbRWJwG%2FcxKTB3PxNXcKosF65D12s1%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;624&quot; height=&quot;195&quot; data-origin-width=&quot;624&quot; data-origin-height=&quot;195&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Spring 공식 Document에서 Spring Cloud Sleuth는 Micrometer-Tracing Project로 이사 간 사항을 고시하고 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Spring-Cloud 2022 버전부터 Sleuth 적용 시 Initilization Error와 함께 위 공지가 나온다.&lt;/span&gt;&lt;/blockquote&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;span style=&quot;color: #000000;&quot;&gt;Features&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1.&amp;nbsp;&amp;nbsp; Easy to Integration with Spring Project&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2.&amp;nbsp;&amp;nbsp; Easy to pass Metrics to Monitoring tool&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.&amp;nbsp;&amp;nbsp; Provide Tracing Library&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4.&amp;nbsp;&amp;nbsp; Provide Context Propagation Library&lt;/span&gt;&lt;/blockquote&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;span style=&quot;color: #000000;&quot;&gt;Instrumentation&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1.&amp;nbsp;&amp;nbsp; Provide Spring Metrics&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2.&amp;nbsp;&amp;nbsp; Provide JVM Metrics&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.&amp;nbsp;&amp;nbsp; Provide Cache Metrics&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4.&amp;nbsp;&amp;nbsp; Provide Instrumentation for Netty&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5.&amp;nbsp;&amp;nbsp; Provide OkHttpClient Metrics&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6.&amp;nbsp;&amp;nbsp; Provide Instrumentation for Jetty, Jersey&lt;/span&gt;&lt;/blockquote&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;span style=&quot;color: #000000;&quot;&gt; Support Monitoring Tools &lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;567&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctNIuG/btsB1L5Mc2I/hf8s2UYkXXIg2iGmIlYOlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctNIuG/btsB1L5Mc2I/hf8s2UYkXXIg2iGmIlYOlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctNIuG/btsB1L5Mc2I/hf8s2UYkXXIg2iGmIlYOlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctNIuG%2FbtsB1L5Mc2I%2Fhf8s2UYkXXIg2iGmIlYOlk%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;1454&quot; height=&quot;567&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;567&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;//-Log-Tracing-이란&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1264&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;// Log Tracing 란&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1k79s/btsBXIiACJo/0POnVAdcPd4lsKKY0ofmEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1k79s/btsBXIiACJo/0POnVAdcPd4lsKKY0ofmEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1k79s/btsBXIiACJo/0POnVAdcPd4lsKKY0ofmEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1k79s%2FbtsBXIiACJo%2F0POnVAdcPd4lsKKY0ofmEK%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;890&quot; height=&quot;476&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MSA 구조에서 End to End를 사이에 일어난 사항을 확인하기 위한 Tracing 기법&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;log를 기반으로, traceId, spanId (+parentSpanId)를 활용하여 전체적인 flow를 파악한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;slow process, error point를 잡아내기에 용이 함&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; Tracing 적용 가이드 &lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring boot 3 기반 프로젝트에서 REST, RSocket, Kafka(reactor)에 Tracing 및 Propagation, Zipkin 연동 가이드&lt;/span&gt;&lt;/blockquote&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;span style=&quot;color: #000000;&quot;&gt;Test Version Specification&lt;/span&gt;&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;항목&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;Version&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Java&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;17&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;org.springframework.boot&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.1.5&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;io.spring.dependency-management&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1.1.2&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;org.springframework.cloud:spring-cloud-dependencies&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2022.0.3&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Reactor&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;io.projectreactor.kafka:reactor-kafka&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1.3.22&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Brave&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;io.zipkin.brave:brave-bom&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5.15.1&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RSocket&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;io.rsocket:rsocket-micrometer&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1.1.4&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Docker&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;openzipkin/zipkin&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.0.0-rc0 (latest)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;span style=&quot;color: #000000;&quot;&gt; ZipKin Setup &lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1702536748782&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;docker run -d -p 9411:9411 openzipkin/zipkin&lt;/code&gt;&lt;/pre&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;span style=&quot;color: #000000;&quot;&gt; REST Tracing &lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REST Tracing의 경우 Conifg 만으로 적용이 가능하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Actuator를 통해서 연동이 가능&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;WebClient는 반드시 Builder를 인자로 받아 Bean으로 선언해야한다.&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Gradle&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702536834634&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
// for report
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;application.yml&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702536859397&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;logging:
  pattern:
    level: &quot;%5p [%X{traceId},%X{spanId}]&quot;
management:
  tracing:
    sampling:
      probability: 1.0
    propagation:
      consume: b3
      produce: b3_multi
    enabled: true
  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Hooks&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702536894255&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static void hooks() {
  Hooks.enableAutomaticContextPropagation();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;WebClient&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702536909601&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public WebClient webClient(WebClient.Builder builder) {
  return builder.build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Test Log&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702536924817&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Application 1
2023-12-07 10:22:36 [reactor-http-nio-6] INFO [65711e5cd3177ba2125ebb529a63dbf4,125ebb529a63dbf4] c.e.tracing4spring3.rest.RestT - get /test start
...
2023-12-07 10:22:36 [reactor-http-nio-6] INFO [65711e5cd3177ba2125ebb529a63dbf4,125ebb529a63dbf4] c.e.tracing4spring3.rest.RestT - res: {result=success}

// Application 2
2023-12-07T10:22:17.100+09:00  INFO [65711e48970b05e498223b2f239e48ca,1c828e842276ba45] 12932 --- [ctor-http-nio-4] : test1 &amp;lt;&amp;lt;&amp;lt;, header: 
  [X-B3-TraceId:&quot;65711e48970b05e498223b2f239e48ca&quot;, X-B3-SpanId:&quot;f7fe2062438d0f0e&quot;, X-B3-ParentSpanId:&quot;98223b2f239e48ca&quot;, X-B3-Sampled:&quot;1&quot;, accept-encoding:&quot;gzip&quot;, user-agent:&quot;ReactorNetty/1.1.12&quot;, host:&quot;127.0.0.1:9083&quot;, accept:&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;&lt;span style=&quot;color: #000000;&quot;&gt;ZipKin&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;670&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T98x7/btsBUDuU6e8/6MBl9BMEjZkZoNK2C5KOVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T98x7/btsBUDuU6e8/6MBl9BMEjZkZoNK2C5KOVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T98x7/btsBUDuU6e8/6MBl9BMEjZkZoNK2C5KOVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT98x7%2FbtsBUDuU6e8%2F6MBl9BMEjZkZoNK2C5KOVk%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;1454&quot; height=&quot;670&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;670&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 id=&quot;RSocket-Tracing&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3669&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RSocket Tracing&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RSocket Tracing의 경우 Integration을 위한 코드 작성이 필요하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 1. RSocketResponderTracingObservationHandler, RSocketRequesterTracingObservationHandler 를 Bean으로 생성&amp;nbsp; &amp;nbsp; &amp;nbsp; 2. Micrometer에서 제공하는 RSocketProxy 구현체를 RSocketConnector, RSocketServer의 Interceptor에 등록한다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; // Inteceptor 등록은 Customizer를 통해 적용&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;micormeter에서 Sample Code를 제공한다.&lt;/span&gt;&lt;/blockquote&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;span style=&quot;color: #000000;&quot;&gt;Gradle&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702537090063&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'io.rsocket:rsocket-micrometer:1.1.4'&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;application.yml&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702537107847&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;상동&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;ObservationHandler-Bean등록&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4091&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ObservationHandler Bean등록&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702537131413&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
RSocketResponderTracingObservationHandler rSocketResponderTracingObservationHandler(Tracer tracer, Propagator propagator) {
  return new RSocketResponderTracingObservationHandler(tracer, propagator, new ByteBufGetter(), true);
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
RSocketRequesterTracingObservationHandler rSocketRequesterTracingObservationHandler(Tracer tracer, Propagator propagator) {
  return new RSocketRequesterTracingObservationHandler(tracer, propagator, new ByteBufSetter(), true);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;RSocketServer-설정&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;4838&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RSocketServer 설정&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702537167261&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
ObservationRSocketServerCustomizer observationRSocketServerCustomizer(ObservationRegistry observationRegistry) {
  return new ObservationRSocketServerCustomizer(observationRegistry);
}


class ObservationRSocketServerCustomizer implements RSocketServerCustomizer {

  private final ObservationRegistry observationRegistry;

  ObservationRSocketServerCustomizer(ObservationRegistry observationRegistry) {
    this.observationRegistry = observationRegistry;
  }

  @Override
  public void customize(RSocketServer rSocketServer) {
    rSocketServer.interceptors(ir -&amp;gt; ir.forResponder(
            (RSocketInterceptor) rSocket -&amp;gt; new ObservationResponderRSocketProxy(rSocket, this.observationRegistry))
        .forRequester((RSocketInterceptor) rSocket -&amp;gt; new ObservationRequesterRSocketProxy(rSocket,
            this.observationRegistry)));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;RSocketReuqester-(Connector)-설정&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5709&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RSocketReuqester (Connector) 설정&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702537186829&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
ObservationRSocketConnectorConfigurer observationRSocketConnectorConfigurer(
    ObservationRegistry observationRegistry) {
  return new ObservationRSocketConnectorConfigurer(observationRegistry);
}

class ObservationRSocketConnectorConfigurer implements RSocketConnectorConfigurer {

  private final ObservationRegistry observationRegistry;

  ObservationRSocketConnectorConfigurer(ObservationRegistry observationRegistry) {
    this.observationRegistry = observationRegistry;
  }

  @Override
  public void configure(RSocketConnector rSocketConnector) {
    rSocketConnector.interceptors(ir -&amp;gt; ir.forResponder(
            (RSocketInterceptor) rSocket -&amp;gt; new ObservationResponderRSocketProxy(rSocket, this.observationRegistry))
        .forRequester((RSocketInterceptor) rSocket -&amp;gt; new ObservationRequesterRSocketProxy(rSocket,
            this.observationRegistry)));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Test Log&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702537215341&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Application 1
2023-12-07 11:27:49 [reactor-http-nio-6] INFO [65712da58aa02571bfd0fef42b4056f9,bfd0fef42b4056f9] c.e.t.rsocket.RSocketService - rsocket request start
...
Frame =&amp;gt; Stream ID: 1 Type: REQUEST_RESPONSE Flags: 0b100000000 Length: 175
Metadata:
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| fe 00 00 09 08 74 65 73 74 2e 31 2e 31 0b 58 2d |.....test.1.1.X-|
|00000010| 42 33 2d 54 72 61 63 65 49 64 00 00 20 36 35 37 |B3-TraceId.. 657|
|00000020| 31 32 64 61 35 38 61 61 30 32 35 37 31 62 66 64 |12da58aa02571bfd|
|00000030| 30 66 65 66 34 32 62 34 30 35 36 66 39 0a 58 2d |0fef42b4056f9.X-|
|00000040| 42 33 2d 53 70 61 6e 49 64 00 00 10 31 30 37 32 |B3-SpanId...1072|
|00000050| 36 35 37 36 61 34 66 63 65 34 65 39 10 58 2d 42 |6576a4fce4e9.X-B|
|00000060| 33 2d 50 61 72 65 6e 74 53 70 61 6e 49 64 00 00 |3-ParentSpanId..|
|00000070| 10 62 66 64 30 66 65 66 34 32 62 34 30 35 36 66 |.bfd0fef42b4056f|
|00000080| 39 0b 58 2d 42 33 2d 53 61 6d 70 6c 65 64 00 00 |9.X-B3-Sampled..|
|00000090| 01 31                                           |.1              |
+--------+-------------------------------------------------+----------------+
...

// Application 2
2023-12-07T11:27:49.831+09:00  INFO [65712da58aa02571bfd0fef42b4056f9,9618da9fe0e57514] 12932 --- [ctor-http-nio-9] : do handle
...&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Zipkin&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJBJ7M/btsBVBDI26m/uXGWms1rEGDTQKS4d4sKkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJBJ7M/btsBVBDI26m/uXGWms1rEGDTQKS4d4sKkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJBJ7M/btsBVBDI26m/uXGWms1rEGDTQKS4d4sKkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJBJ7M%2FbtsBVBDI26m%2FuXGWms1rEGDTQKS4d4sKkK%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;1454&quot; height=&quot;667&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;Kafka-(Reactor)&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8118&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Kafka (Reactor)&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Reactor-Kafka에서는 비교적 최근에 Tracing Observation Integration이 추가 되었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이슈번호 321에서 다뤄졌으며, 2023.10.27에 머지 되었다. (https://github.com/reactor/reactor-kafka/issues/321)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;적용된 버전은 1.3.22 이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Reactor-Kafka에서 Tracing을 위한 interface로 SenderOptions, ReceiverOptions에서 withObservation() 메서드를 제공한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;인자로 ObservationRegistry(Micrometer)를 넣어서 tracing을 propagation 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Receiver의 경우 Propagation된 context를 flow에 할당하는 추가적인 로직이 필요하다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 id=&quot;Gradle,-application.yml&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8550&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Gradle, application.yml&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702537300480&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;상동&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;KafkaSender-(producer)&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8585&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;KafkaSender (producer)&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702537312391&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SenderOptions&amp;lt;String, Object&amp;gt; senderOptions = SenderOptions.create(senderProps);
return KafkaSender.create(senderOptions.withObservation(observationRegistry));&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1702537324732&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ReceiverOptions&amp;lt;String, String&amp;gt; receiverOptions = ReceiverOptions.create(consumerConfig);
KafkaReceiver&amp;lt;String, String&amp;gt; kafkaReceiver = KafkaReceiver.create(
    receiverOptions
        .withObservation(observationRegistry)
        .atmostOnceCommitAheadSize(20)
        .subscription(Pattern.compile(&quot;*&quot;)));
        

kafkaReceiver
        .receive()
        .flatMap(record -&amp;gt; {
          Observation receiverObservation =
              KafkaReceiverObservation.RECEIVER_OBSERVATION.start(null,
                  KafkaReceiverObservation.DefaultKafkaReceiverObservationConvention.INSTANCE,
                  () -&amp;gt;
                      new KafkaRecordReceiverContext(
                          record, &quot;user.receiver&quot;, receiverOptions.bootstrapServers()),
                  observationRegistry);

          return Mono.just(record)
              .doOnNext(r -&amp;gt; log.info(&quot;topic: {}, key: {}, value: {}&quot;, r.topic(), r.key(), r.value()))
              .doOnTerminate(receiverObservation::stop)
              .doOnError(receiverObservation::error)
              .contextWrite(context -&amp;gt; context.put(ObservationThreadLocalAccessor.KEY, receiverObservation));})
        .subscribe();&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Test Log&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1702537347027&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2023-12-07 13:28:53 [reactor-http-nio-4] INFO [65714a05e50e542ff15b4dfb2c7c3906,f15b4dfb2c7c3906] c.e.t.kafka.ProducerConfig - produce data &amp;gt;&amp;gt; ProducerRecord(topic=chchoi.test.2, partition=null, headers=RecordHeaders(headers = [], isReadOnly = false), key=1, value={&quot;id&quot;:&quot;xxx&quot;,&quot;message&quot;:&quot;test&quot;}, timestamp=null)
2023-12-07 13:28:53 [reactor-http-nio-4] INFO [65714a05e50e542ff15b4dfb2c7c3906,f15b4dfb2c7c3906] c.e.t.kafka.ProducerConfig - produce data &amp;gt;&amp;gt; ProducerRecord(topic=chchoi.test.2, partition=null, headers=RecordHeaders(headers = [], isReadOnly = false), key=2, value={&quot;id&quot;:&quot;xxx&quot;,&quot;message&quot;:&quot;test&quot;}, timestamp=null)
2023-12-07 13:28:53 [reactive-kafka-test-1-1] INFO [65714a05e50e542ff15b4dfb2c7c3906,71bd25628c1e61ec] c.e.t.kafka.ConsumerConfig - topic: chchoi.test.2, key: 1, value: &quot;{\&quot;id\&quot;:\&quot;xxx\&quot;,\&quot;message\&quot;:\&quot;test\&quot;}&quot;
2023-12-07 13:28:53 [reactive-kafka-test-1-1] INFO [65714a05e50e542ff15b4dfb2c7c3906,d0527c836ddc587c] c.e.t.kafka.ConsumerConfig - topic: chchoi.test.2, key: 2, value: &quot;{\&quot;id\&quot;:\&quot;xxx\&quot;,\&quot;message\&quot;:\&quot;test\&quot;}&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Kafdrop&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wcAgd/btsBXGyjVYh/Yii0w28ddj5i7y2KAYKFvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wcAgd/btsBXGyjVYh/Yii0w28ddj5i7y2KAYKFvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wcAgd/btsBXGyjVYh/Yii0w28ddj5i7y2KAYKFvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwcAgd%2FbtsBXGyjVYh%2FYii0w28ddj5i7y2KAYKFvk%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;1109&quot; height=&quot;159&quot; data-origin-width=&quot;1109&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Zipkin&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;785&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t0UkN/btsBUASvy5z/GpwbxTGQ5vaXtQN6u9XbX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t0UkN/btsBUASvy5z/GpwbxTGQ5vaXtQN6u9XbX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t0UkN/btsBUASvy5z/GpwbxTGQ5vaXtQN6u9XbX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft0UkN%2FbtsBUASvy5z%2FGpwbxTGQ5vaXtQN6u9XbX1%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;1714&quot; height=&quot;785&quot; data-origin-width=&quot;1714&quot; data-origin-height=&quot;785&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Conclusion&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Cloud Slueth가 Micrometer로 move 됨&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Micrometer-tracing에서 Spring Cloud Sleuth로 migration guide를 제공&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Micrometer는 Spring이랑 호환성이 매우 뛰어남&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring Project에서 Log Tracing을 적용하기에 적절&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추후 ZipKin 등 log monitoring 도입을 위한 확장성 제공&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추후 JVM Metrics등 정보를 받을 수 있도록 확장성을 제공&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Spring boot 3에 적용된 reference는 부족함&lt;/span&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REFERENCE&lt;/span&gt;&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://micrometer.io/&quot;&gt;https://micrometer.io/&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://micrometer.io/docs/tracing&quot;&gt;https://micrometer.io/docs/tracing&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide&quot;&gt;https://github.com/micrometer-metrics/tracing/wiki/Spring-Cloud-Sleuth-3.1-Migration-Guide&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;11587&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;REST&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://micrometer.io/docs/observation#instrumentation_of_reactive_libraries_after_reactor_3_5_3&quot;&gt;https://micrometer.io/docs/observation#instrumentation_of_reactive_libraries_after_reactor_3_5_3&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/micrometer-metrics/micrometer-samples/blob/main/webclient/build.gradle&quot;&gt;https://github.com/micrometer-metrics/micrometer-samples/blob/main/webclient/build.gradle&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;11788&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RSocket&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/micrometer-metrics/micrometer-samples/blob/bb777d40daacd0dc108e20731ce4dc4f72d47a2f/rsocket-client/src/main/java/com/example/micrometer/ManualConfiguration.java&quot;&gt;https://github.com/micrometer-metrics/micrometer-samples/blob/bb777d40daacd0dc108e20731ce4dc4f72d47a2f/rsocket-client/src/main/java/com/example/micrometer/ManualConfiguration.java&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/micrometer-metrics/micrometer-samples/blob/bb777d40daacd0dc108e20731ce4dc4f72d47a2f/rsocket-server/src/main/java/com/example/micrometer/ManualConfiguration.java&quot;&gt;https://github.com/micrometer-metrics/micrometer-samples/blob/bb777d40daacd0dc108e20731ce4dc4f72d47a2f/rsocket-server/src/main/java/com/example/micrometer/ManualConfiguration.java&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;12165&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Reactor-Kafka&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/reactor/reactor-kafka/issues/321&quot;&gt;https://github.com/reactor/reactor-kafka/issues/321&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/reactor/reactor-kafka/pull/325&quot;&gt;https://github.com/reactor/reactor-kafka/pull/325&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/reactor/reactor-kafka/pull/325/files/38168795bdd6d86af18fd5308689a483bd332610..3e20f3dcab1a7c4a5160214a1c977008e9e2cc05#diff-a43be6da00bb7683292fd18443d388a44e23a6354dee611a27d07884509e101a&quot;&gt;https://github.com/reactor/reactor-kafka/pull/325/files/38168795bdd6d86af18fd5308689a483bd332610..3e20f3dcab1a7c4a5160214a1c977008e9e2cc05#diff-a43be6da00bb7683292fd18443d388a44e23a6354dee611a27d07884509e101a&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://github.com/reactor/reactor-kafka/pull/325/files/38168795bdd6d86af18fd5308689a483bd332610..3e20f3dcab1a7c4a5160214a1c977008e9e2cc05&quot;&gt;https://github.com/reactor/reactor-kafka/pull/325/files/38168795bdd6d86af18fd5308689a483bd332610..3e20f3dcab1a7c4a5160214a1c977008e9e2cc05&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>Kafka</category>
      <category>log tracing</category>
      <category>micrometer</category>
      <category>Rest</category>
      <category>rsocket</category>
      <category>spring boot 3</category>
      <category>tracing</category>
      <category>webclient</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/71</guid>
      <comments>https://p-bear.tistory.com/71#entry71comment</comments>
      <pubDate>Thu, 14 Dec 2023 16:08:00 +0900</pubDate>
    </item>
    <item>
      <title>시즌2 기획</title>
      <link>https://p-bear.tistory.com/70</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baJSBv/btsAnE9AsG8/qNgdSn9gY0kQb6EMvlYA51/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baJSBv/btsAnE9AsG8/qNgdSn9gY0kQb6EMvlYA51/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baJSBv/btsAnE9AsG8/qNgdSn9gY0kQb6EMvlYA51/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaJSBv%2FbtsAnE9AsG8%2FqNgdSn9gY0kQb6EMvlYA51%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;450&quot; height=&quot;450&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;연말을 준비하면서 개인 서버의 서비스들을 정리해보고자 한다.&lt;br /&gt;한 동안 바쁜 일정도 있었지만, 최근 서비스 개발이 없었던 이유 중 가장 큰 것은 모노리틱 구조로 설계한 서버도 한몫을 하고 있다.&lt;br /&gt;이전에 MSA 식으로 개발해 봤던 부분과, 올해 모노리틱으로 설계한 부분의 장단점을 분석하고 다음 시즌에는 보다 길고 탄탄한 서비스를 만드는 방향으로 기획하려 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;모노리틱 서비스 구조 문제&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;모노리틱 서비스를 개발할 때 발생하는 문제점에 대한 고찰&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;예를 들면, 한 프로젝트 내에서 여러 DataSource를 관리해야 되는 문제가 발생했다.&lt;br /&gt;여러 서비스의 성격이 다르고, 적합한 DataSource가 다르다 보니, 서버에 지속적으로 Data Access Point가 늘어나게 되었다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;881&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6gFOx/btsAq2WhIZq/77mEESk5UpsjN0a9PDkRy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6gFOx/btsAq2WhIZq/77mEESk5UpsjN0a9PDkRy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6gFOx/btsAq2WhIZq/77mEESk5UpsjN0a9PDkRy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6gFOx%2FbtsAq2WhIZq%2F77mEESk5UpsjN0a9PDkRy0%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;1514&quot; height=&quot;881&quot; data-origin-width=&quot;1514&quot; data-origin-height=&quot;881&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;blockquote data-ke-style=&quot;style2&quot;&gt;현시점 연동 상황...&lt;br /&gt;서비스 1은 사실 레디스가 필요 없지만, 프로젝트가 하나기에 영향을 받는다.&lt;br /&gt;개발 시점에, 여러 인프라를 신경 써야 한다...&lt;br /&gt;실제로 db를 안 쓰더라도, 로컬 개발 시, 개발환경의 모든 인프라를 챙겨서 세팅해야 run이 가능하다.&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;개인 개발 특성상, &lt;br /&gt;실험적인 서비스 또는 한시적인 기능을 돌릴 때가 있다.&lt;br /&gt;하지만, 모노리틱 구조상, 패키지 구조를 신경을 많이 써야, 스무스한 제거가 가능하다.&lt;br /&gt;(하지만, 그런 한시적, 실험적인 부분에 공들여서 만들 리가 없었다..)&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;일반적인 문제&lt;br /&gt;소스가 점점 뚱뚱해지면서, 리펙토링 등 대부분의 작업에서 리소스가 추가적으로 투입된다.&lt;br /&gt;단순 빌드시간만 해도 계속 늘어난다...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MSA 구조 문제&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이전에 MSA 구조로 개발을 진행했었다.&lt;br /&gt;하지만, 여러 문제점이 발생했으며&lt;br /&gt;모든 문제는 하나의 이유로 귀결된다&lt;br /&gt;-&amp;gt; 1인 개발이기에 투입가능한 리소스(시간+열정)가 부족하다..&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;당연하게도 1인 개발에서 서버 인프라 관리는 스스로 해야 한다.&lt;br /&gt;또한 비용 투입 없이 하기에, Cloud 서비스 같은 부분은 불가하다.&lt;br /&gt;이에 인프라 하나 추가할 때 모든 관련 설정을 스스로 해야 하고, 이에 투입되는 비용이 너무 컸다.&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;개인 개발의 최대 동기는 새로운 걸 해보면서 느끼는 재미이다.&lt;br /&gt;하지만, 여러 이유로 반복적인 작업이 늘어난다.&lt;br /&gt;가령 CI / CD도 프로젝트 생성 시마다 일일이 작업해야 하며, 시스템 간 연동을 위한 코드, ip / port 매핑 작업 등등이 있다.&lt;br /&gt;이에 점점 새로운 시도를 하기 귀찮아지며, 신규 개발에서 멀어져 갔다..&lt;/blockquote&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;Point to Point 연결, 서비스 의존성&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;A, B 서비스 간의 변경인데, 연동되어 있던 C에서 문제가 발생&lt;br /&gt;인증 쪽 프로세스 변경이라도 일어나면 머리가 아파옵니다.&lt;br /&gt;앞단 인증 쪽에 role 베이스 인가관리 추가하다가, 엄청 터져나갔던 기억이 있네요...&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;모듈 여러 개의 통합 테스트를 진행하려면 띄울 거 다 띄우고 진행해야 합니다.&lt;br /&gt;리소스가 부족한 상황에서 mocking을 통한 test case 작성은, 너무나도 힘들었네요...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Next 설계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSA 구조 채택&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;모노리틱으로 구성해 본 결과, 1인 개발은 빠른 치고 빠지기가 가능한 MSA 구조가 적합할 것이라 판단됩니다.&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;최대한 신규 서비스를 추가하기 쉬운 구조를 확보하고 시작&lt;br /&gt;현 계획은&lt;br /&gt;1. Jenkins Script를 활용하여, CI / CD 구성 간소화&lt;br /&gt;2. Zookeeper를 통한 신규 시스템 연동 과정 단순화&lt;br /&gt;3. 관제 서버를 구현하여, 전체적인 서버들의 상황을 모니터링&lt;br /&gt;정도 있다.&lt;/blockquote&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;Point to Point -&amp;gt; Event Driven&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka를 활용하여, 대부분의 비즈니스를 event driven으로 구성,&lt;br /&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이번에는 전체적인 서버 아키텍처를 설계하고 시작할 것&lt;br /&gt;지금까지의 경험을 최대한 살려서, 지속 가능한 서버 운영을 목표로 함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>[시즌2] 개인서버 개발</category>
      <category>next 기획</category>
      <category>개인 서버</category>
      <category>결산</category>
      <category>실패원인분석</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/70</guid>
      <comments>https://p-bear.tistory.com/70#entry70comment</comments>
      <pubDate>Thu, 16 Nov 2023 14:51:32 +0900</pubDate>
    </item>
    <item>
      <title>Windows 10 부팅 지연 이슈 (시스템 예약 위치..)</title>
      <link>https://p-bear.tistory.com/69</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;96&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vHyQp/btsAkuEyDOx/kunGT7zDYCcj8f1khUUVzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vHyQp/btsAkuEyDOx/kunGT7zDYCcj8f1khUUVzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vHyQp/btsAkuEyDOx/kunGT7zDYCcj8f1khUUVzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvHyQp%2FbtsAkuEyDOx%2FkunGT7zDYCcj8f1khUUVzk%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;523&quot; height=&quot;96&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;96&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;어느 날부터인가 부팅이 수십 초 느려지는 현상이 생겼다.&lt;br /&gt;해결 과정에 대해 정리한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추측 1&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Boot Disk 우선순위 문제?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;느려지는 구간은 구동 후 메인보드 로고가 뜨고, BIOS 설정으로 들어갈 수 있는 로딩화면이었다.&lt;br /&gt;이와 비슷한 현상은 이전에 경험해 봤었는데,&lt;br /&gt;바로 boot disk의 우선순위가 잘못 설정되어 있을 때였다.&lt;br /&gt;정기적으로 윈도우를 포맷하여 사용하기에, 이번에도 그런 이슈 일 것이라 추측했다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;스크린샷은 못 찍었지만&lt;br /&gt;확인한 사항으로는 부팅 우선순위는 윈도우가 설치된 SSD었다.&lt;br /&gt;우선 BIOS를 초기 설정으로 되돌리고, 디스크를 윈도우가 설치된 SSD로 돌려보았다.&lt;br /&gt;하지만, 현상은 동일했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추측 2&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MainBoard 이슈?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;두 번째로 의심한 사항은, 메인보드의 자체적인 이슈이다.&lt;br /&gt;사용 후 1~2년 후에 갑자기 나타난 이슈라 솔직히 그럴 리 없다 생각했다.&lt;br /&gt;그래도 일단 확인정도는 해봐야 하기에 관련 이슈를 검색해 보았다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;부팅속도 관련 이슈가 있었으나, 이는 특정 제조사에서 발생하는 이슈였고, 내가 골랐던 메인보드 제조사는 관련 사항이 없었다.&lt;br /&gt;생각해 보면, 메인보드 고를 때 꽤나 시간 들여서 하드웨어 이슈사항에 대해서 검색해 보았었기에, 이 문제는 아닐 거라 생각했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실마리 발견!&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1676&quot; data-origin-height=&quot;813&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkmOgL/btsAjhloKQR/AjnK7U8DP0Axi03X8gufM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkmOgL/btsAjhloKQR/AjnK7U8DP0Axi03X8gufM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkmOgL/btsAjhloKQR/AjnK7U8DP0Axi03X8gufM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkmOgL%2FbtsAjhloKQR%2FAjnK7U8DP0Axi03X8gufM0%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;1676&quot; height=&quot;813&quot; data-origin-width=&quot;1676&quot; data-origin-height=&quot;813&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;결국 이슈는 해결 못한 체 어제까지 해당 이슈를 체념하고 있었다.&lt;br /&gt;다른 사항 때문에 디스크 관리자 쪽을 보았었는데, 시스템 예약이 이상하게 잡혀있던 것을 발견했다!&lt;br /&gt;분명 시스템 예약은 C드라이브 쪽 디스크에 있어야 정상일 텐데...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시스템 예약이란&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;시스템 예약은 윈도우 설치 도중 생성되는 파티션으로, 시스템에서 나중에 사용하기 위해 일부 공간을 미리 할당해 놓는 곳이다. 중요한 점은 부팅을 위한 구성요소 중 일부가 여기에 저장되는 것이다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;역할&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;여기에는 부팅을 위한 데이터 (BCD), 윈도우 복구 환경 (WinRE) 등 시스템에서 필요한 데이터를 저장하는 역할을 한다. BCD는 부팅할 때 참조하는 데이터이고, WinRE는 윈도우 복구 툴을 사용할 때 참조하는 데이터이다.&lt;br /&gt;요약하면, 윈도우 시스템에서 필요한 1차적인 데이터들이라 볼 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추측&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;현상은 윈도우 Booting이 오래 걸리는 것이며, 디스크를 못 찾는 현상과 유사했었다.&lt;br /&gt;이에 부팅 시 시스템 예약 파티션을 찾지 못해서 지연이 발생한 것이라 추측했다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;검색해 보니 윈도우 설치위치와 다른 곳에 할당된 시스템 예약을 복구할 수 있는 방법이 있었다.&lt;br /&gt;이를 적용해 보았다.&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결과정&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;여러 블로그를 뒤져서 다른 파티션의 시스템 예약을 옮기는 방법을 찾았다.&lt;br /&gt;요약하면&lt;br /&gt;1. 간단하게 BCD 부팅 파일을 윈도우가 설치된 디스크에 새로 생성하고&lt;br /&gt;2. 부팅 옵션을 해당 디스크로 설정하고&lt;br /&gt;3. 잘못된 시스템 예약을 disable 시킨다.&lt;/blockquote&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;BCD 부트 매니저 생성 및 설정&lt;/h3&gt;
&lt;pre id=&quot;code_1699889305101&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// bcdboot 명령을 통해 bootmgr를 생성한다.
bcdboot c:\windows /s c: /l ko-kr 
// bcdedit을 통해 부팅할 파티션을 지정한다.
bcdedit /set {bootmgr} device partition=c:&lt;/code&gt;&lt;/pre&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;807&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NcbbB/btsz91EuVCR/wcl8TtuQgqbRzUbA96eKOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NcbbB/btsz91EuVCR/wcl8TtuQgqbRzUbA96eKOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NcbbB/btsz91EuVCR/wcl8TtuQgqbRzUbA96eKOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNcbbB%2Fbtsz91EuVCR%2Fwcl8TtuQgqbRzUbA96eKOK%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;807&quot; height=&quot;357&quot; data-origin-width=&quot;807&quot; data-origin-height=&quot;357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;지금은 이미 활성해서 회색이지만, 파티션을 활성 파티션으로 표시한다.&lt;/blockquote&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;잘못된 시스템 예약 파티션 Disable&lt;/h3&gt;
&lt;pre id=&quot;code_1699889548549&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;C:\windows\system32&amp;gt;diskpart

...

DISKPART&amp;gt; list disk

  디스크 ###  상태           크기     사용 가능     Dyn  Gpt
  ----------  -------------  -------  ------------  ---  ---
  디스크 0    온라인        931 GB       1024 KB
  디스크 1    온라인       1863 GB           0 B
  디스크 2    온라인        232 GB           0 B
  디스크 3    온라인        111 GB       1024 KB

DISKPART&amp;gt; select disk 0

0 디스크가 선택한 디스크입니다.

DISKPART&amp;gt; list partition

  파티션 ###  종류              크기     오프셋
  ----------  ----------------  -------  -------
  파티션 1    주                  549 MB  1024 KB
  파티션 2    주                  930 GB   550 MB

DISKPART&amp;gt; select partition 1

1 파티션이 선택한 파티션입니다.

DISKPART&amp;gt; inactive

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;diskpart를 통해 500MB쯤 되는 시스템 예약을 inactive 시켰다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;주저리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;부팅 지연 이슈는 해결되었다.&lt;br /&gt;기분이 좋은 것을 보니 생각보다 그동안 스트레스를 받았던 것 같다.&lt;br /&gt;이 이슈는 아마 언젠가 윈도우 포맷 중 설정을 잘못해서 발생한 것으로 추정된다.&lt;br /&gt;&lt;br /&gt;정기적으로 포맷하는 습관을 줄여야 하는데...&lt;br /&gt;학창 시절 윈도우는 SSD에 설치해야겠는데, 돈은 없어서 SSD 용량이 적었다...&lt;br /&gt;이에 SSD로 윈도우만 설치하고, 상대적으로 싼 HDD로 다른 프로그램을 사용하던 시절이 있었다.&lt;br /&gt;하지만 윈도우는 사용하다 보면 local 등 여러 잡다한 데이터가 쌓이게 되고, SSD의 용량이 부족해졌다.&lt;br /&gt;그 시절부터 정기적으로 용량 확보를 위해 windows 포맷을 즐겼었다.&lt;br /&gt;한 번씩 대청소를 하는 기분 + 새로 PC를 최적화 세팅하는 즐거움? 이 있어 아직도 하고 있다...&lt;br /&gt;&lt;br /&gt;지금 쓰는 윈도우 SSD가 120GB 밖에 안 되는 것도 이 영향이 있다.&lt;br /&gt;근데 슬슬 이 습관도 자제하는 게 좋을지도 모른다고 생각한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://quasarzone.com/bbs/qf_cmr/views/2111658&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://quasarzone.com/bbs/qf_cmr/views/2111658&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://rootblog.tistory.com/218&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://rootblog.tistory.com/218 &lt;/a&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;(Special Thanks)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>windows10</category>
      <category>windows10 부팅</category>
      <category>부들부들</category>
      <category>부팅 느림</category>
      <category>해결</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/69</guid>
      <comments>https://p-bear.tistory.com/69#entry69comment</comments>
      <pubDate>Tue, 14 Nov 2023 00:40:46 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux Threading (publishOn, subscribeOn) 분석</title>
      <link>https://p-bear.tistory.com/68</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edvH6h/btszgZ8qDrf/kmboNqIitYNeftsDkkxcG0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edvH6h/btszgZ8qDrf/kmboNqIitYNeftsDkkxcG0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edvH6h/btszgZ8qDrf/kmboNqIitYNeftsDkkxcG0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FedvH6h%2FbtszgZ8qDrf%2FkmboNqIitYNeftsDkkxcG0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;168&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개요&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Reactor에서 제공하는 Pub / Sub 모델은 실행될 Thread를 전략적으로 선택할 수 있게 interface를 제공한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- publishOn(Scheduler scheduler): 어디에서 Publish 될 것인가&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- subscribeOn(Scheduler scheduler): 어디에서 subscribe 할 것인가&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Scheduler&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1698399998231&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Scheduler extends Disposable {
    
  Disposable schedule(Runnable task);
  Disposable schedule(Runnable task, long initialDelay, TimeUnit delayUnit);
  Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit);
  
  long now(TimeUnit unit);
  
  Worker createWorker();
  
  interface Worker extends Disposable {
    Disposable schedule(Runnable task);
    Disposable schedule(Runnable task, long initialDelay, TimeUnit delayUnit);
    Disposable schedulePeriodically(Runnable task, long initialDelay, long period, TimeUnit unit);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Scheduler는 사용자에게 threading을 효율적으로 제어할 수 있도록 설계된 interface이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Worker는 Scheduler에 의해 create되며, 역할은 task를 execute 하는 것이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적으로 구현체는 각각 용도에 맞는 ExecutorService 또는 그 reference를 가지고 있으며&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 executorService를 활용하여 Thread를 구현로직에 따라 Execute한다. (일반적으로 스케쥴링을 위한 ScheduledExecutorService를 가지고 있다.)&lt;/span&gt;&lt;/blockquote&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;span style=&quot;color: #000000;&quot;&gt;Default Schedulers&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;784&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdooN7/btszjFuztl6/N94oaKZBMc6E5XymW1ZeY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdooN7/btszjFuztl6/N94oaKZBMc6E5XymW1ZeY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdooN7/btszjFuztl6/N94oaKZBMc6E5XymW1ZeY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdooN7%2FbtszjFuztl6%2FN94oaKZBMc6E5XymW1ZeY0%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;1151&quot; height=&quot;784&quot; data-origin-width=&quot;1151&quot; data-origin-height=&quot;784&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Project Reactor에서는 기 구현된 클래스들을 제공한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이는 Schedulers 클래스의 static 메서드로 제공 받을 수 있다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1257&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYTjk3/btszlkiIrJ5/wRzObsbDuu6gpNSAWRvNv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYTjk3/btszlkiIrJ5/wRzObsbDuu6gpNSAWRvNv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYTjk3/btszlkiIrJ5/wRzObsbDuu6gpNSAWRvNv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYTjk3%2FbtszlkiIrJ5%2FwRzObsbDuu6gpNSAWRvNv1%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;1257&quot; height=&quot;265&quot; data-origin-width=&quot;1257&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;주요 Scheduler&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ImmediateScheduler: &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- Thread 변경 없는 scheduler &amp;rarr; ExecutorService를 쓰지 않는다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- interface상 scheduler가 notNull일 시 사용&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;singleScheduler: &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- Thread 하나 짜리 executorService에서 돌아가는 scheduler, 한번에 하나만 실행될 작업에 적합&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ParallelScheduler: &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- Parall한 작업을 수행하기 위한 scheduler, 다수의 작업을 짧게 실행하기에 적합하다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- I/O등 작업을 수행하기 부적합. (parall Thread는 기본적으로 CPU 갯수)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- 기본적인 Reactor 연산의 기본값 (reactor-http-nio 등)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;BoundedElasticScheduler: &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- ExecutorService에서 Worker를 생성하고, 재사용&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- I/O 작업 등 시간이 걸리는 작업을 수행하기 적합&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- worker는 1분 이상 idle 되면 제거&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ExecutorScheduler:&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- custom executor를 세팅할 수 있는 scheduler&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- 개발자는 반드시 executorService로 lifeCycle 관리를 해야 함&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;Operator:-PublishOn(),-SubscribeOn()&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;1835&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Operator: PublishOn(), SubscribeOn()&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Reactor에서 Scheduler를 적용할 수 있게 제공하는 method 이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;- publishOn(): Source로부터 이하 로직이 실행 될 Scheduler를 지정할 수 있는 메서드 subscribeOn(): Source부터&amp;nbsp; &amp;nbsp;- Scheduler가 지정되지 않은 upStream의 시작점부터 Scheduler를 적용할 수 있는 메서드&lt;/span&gt;&lt;/blockquote&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;span style=&quot;color: #000000;&quot;&gt;PublishOn()&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1698400193466&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
  Mono.just(&quot;pubEx&quot;)
      .doOnNext(body -&amp;gt; log.info(&quot;1&quot;))
      .publishOn(Schedulers.newSingle(&quot;single1&quot;))
      .doOnNext(body -&amp;gt; log.info(&quot;2&quot;))
      .publishOn(Schedulers.parallel())
      .doOnNext(body -&amp;gt; log.info(&quot;3&quot;))
      .publishOn(Schedulers.boundedElastic())
      .doOnNext(body -&amp;gt; log.info(&quot;4&quot;))
      .subscribe();
}

09:47:37.881 [main] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 1
09:47:37.886 [single1-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 2
09:47:37.887 [parallel-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 3
09:47:37.888 [boundedElastic-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 4&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최초 실행 쓰레드는 main으로 1번 로그는 main Thread에서 실행된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2, 3, 4는 각각 doOnNext 전 Scheduler를 지정하여 실행된 것을 확인할 수 있다.&lt;/span&gt;&lt;/blockquote&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;span style=&quot;color: #000000;&quot;&gt;SubscribeOn()&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1698400229257&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
  Mono.just(&quot;subEx&quot;)
      .doOnNext(body -&amp;gt; log.info(&quot;1&quot;))
      .doOnNext(body -&amp;gt; log.info(&quot;2&quot;))
      .subscribeOn(Schedulers.newSingle(&quot;single1&quot;))
      .doOnNext(body -&amp;gt; log.info(&quot;3&quot;))
      .subscribeOn(Schedulers.parallel())
      .doOnNext(body -&amp;gt; log.info(&quot;4&quot;))
      .subscribeOn(Schedulers.boundedElastic())
      .subscribe();
}

10:18:58.911 [single1-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 1
10:18:58.914 [single1-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 2
10:18:58.914 [single1-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 3
10:18:58.914 [single1-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 4&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SubscribeOn은 PublishOn과 다르게, Scheduler가 적용되지 않은 upStream에 Scheduler를 등록한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 최초의 newSingle() 스케쥴러는 적용되었지만, 이하 parallel, boundedElastic은 적용되지 못 했다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자세한 사항은 후술&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1698400242813&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;ServerResponse&amp;gt; test(final ServerRequest serverRequest) {
  return serverRequest.bodyToMono(HashMap.class)
      .flatMap(x -&amp;gt; Mono.just(x)
          .subscribeOn(Schedulers.boundedElastic())
          .doOnNext(body -&amp;gt; log.info(Thread.currentThread().getName()))
          .doOnNext(body -&amp;gt; log.info(&quot;1&quot;))
          .doOnNext(body -&amp;gt; log.info(&quot;2&quot;)))
      .flatMap(x -&amp;gt; ServerResponse.ok().build());
}

2023-10-26T14:04:40.575+09:00  INFO 10540 --- [oundedElastic-1] c.h.i.s.n.s.inbound.rest.TestRouter      : boundedElastic-1
2023-10-26T14:04:40.575+09:00  INFO 10540 --- [oundedElastic-1] c.h.i.s.n.s.inbound.rest.TestRouter      : 1
2023-10-26T14:04:40.575+09:00  INFO 10540 --- [oundedElastic-1] c.h.i.s.n.s.inbound.rest.TestRouter      : 2&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;scheduler 적용 예시&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;Scheduler-적용/미적용-비교&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;4602&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Scheduler 적용/미적용 비교&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1698400269314&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    Flux.fromIterable(List.of(&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;))
        .doOnNext(TestRouter::wait1000)
        .subscribe();
    Flux.fromIterable(List.of(&quot;A&quot;,&quot;B&quot;,&quot;C&quot;,&quot;D&quot;))
        .doOnNext(TestRouter::wait1000)
        .subscribe();
  }

10:00:06.700 [main] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 1
10:00:07.712 [main] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 2
10:00:08.720 [main] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 3
10:00:09.726 [main] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 4
10:00:10.737 [main] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- A
10:00:11.741 [main] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- B
10:00:12.752 [main] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- C
10:00:13.765 [main] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- D&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1초씩 딜레이를 줘서 로깅하는 로직이 실행 된다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1번 Subscribe가 종료되어야, 2번 subscribe가 진행되는 것을 확인할 수 있다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이는 내부 로직이 main Thread에서 진행되기 때문에, main이 blocking으로 동작하여, 1번 subscribe line이 끝나야 2번 subscribe line이 실행되기 때문&lt;/span&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1698400287157&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
    Flux.fromIterable(List.of(&quot;1&quot;,&quot;2&quot;,&quot;3&quot;,&quot;4&quot;))
        .publishOn(Schedulers.boundedElastic())
        .doOnNext(TestRouter::wait1000)
        .subscribe();
    Flux.fromIterable(List.of(&quot;A&quot;,&quot;B&quot;,&quot;C&quot;,&quot;D&quot;))
        .publishOn(Schedulers.boundedElastic())
        .doOnNext(TestRouter::wait1000)
        .subscribe();
  }

10:02:43.690 [boundedElastic-2] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- A
10:02:43.690 [boundedElastic-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 1
10:02:44.704 [boundedElastic-2] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- B
10:02:44.704 [boundedElastic-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 2
10:02:45.714 [boundedElastic-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 3
10:02:45.714 [boundedElastic-2] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- C
10:02:46.727 [boundedElastic-2] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- D
10:02:46.727 [boundedElastic-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 4&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Scheduler 적용 시 이하 프로세스는 Scheduler에서 관리되며 main Thread의 경우 다음 line으로 넘어간다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Scheduler는 각각의 Thread에서 로직을 executorService를 통해서 실행하며, 서로 간섭하지 않는다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;PublishOn()-VS-SubscribeOn()&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;6983&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PublishOn() VS SubscribeOn()&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1698400310893&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void main(String[] args) {
  Mono.just(&quot;pub vs sub&quot;)
      .doOnNext(body -&amp;gt; log.info(&quot;1&quot;))
      .publishOn(Schedulers.newSingle(&quot;pub&quot;))
      .doOnNext(body -&amp;gt; log.info(&quot;2&quot;))
      .publishOn(Schedulers.parallel())
      .doOnNext(body -&amp;gt; log.info(&quot;3&quot;))
      .subscribeOn(Schedulers.newSingle(&quot;sub&quot;))
      .doOnNext(body -&amp;gt; log.info(&quot;4&quot;))
      .subscribe();
}

10:28:30.634 [sub-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 1
10:28:30.638 [pub-2] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 2
10:28:30.638 [parallel-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 3
10:28:30.638 [parallel-1] INFO com.ht.iot.smr.node.sample.inbound.rest.TestRouter -- 4&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1번 로직은 본래 main 쓰레드에서 execute 했었지만 .subscribeOn(Schedulers.newSingle(&quot;sub&quot;)) 으로 인하여 sub(single)에서 execute되었다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2번은 .publishOn(Schedulers.newSingle(&quot;pub&quot;)) 으로 pub(single)에서 실행 되었으며&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3,4번은 다시 parallel로 변경 된 상황이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;Conclusion&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;7948&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Conclusion&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id=&quot;Schedule-in-Webflux&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;7960&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Schedule in Webflux&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;정리하면 PubOn과 SubOn은 주도권을 누가 가져갈 것 인가에 대한 차이이다.&lt;br /&gt;&amp;nbsp;- PubOn은 이하 로직은 내가 주도권을 가지고 Schedule을 지정 하는 것&lt;br /&gt;&amp;nbsp;- SubOn은 수동적으로 Schedule을 지정하는 것 이다.&lt;br /&gt;가령 일반적으로 http 요청을 받아 response를 주는 과정은 Webflux에서 주도권을 가지고 Schedule을 지정한다.&lt;br /&gt;따라서 HttpHandle 과정 request &amp;rarr; executeHandler &amp;rarr; (handle) &amp;rarr; handleResult &amp;rarr; response는 reactor의 schedule(reactor-http-nio-x)에서 처리하게 된다.&lt;br /&gt;해당 스케쥴은 netty(or jetty, undertow, etc&amp;hellip;) 에서 관리되는 executor를 사용하고 있다.&lt;br /&gt;따라서 capacity가 유한하며, 서버 기동 시 설정에 의해 결정된다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;// 주의!&lt;br /&gt;reactor-http 쪽 WorkerPool 또한 유한하기 때문에, I/O 작업 등 기타 시간이 걸리는 작업은 최대한 다른 Scheduler에서 실행되는 것이 바람직하다.&lt;br /&gt;ex) DB 작업은 r2dbc에서 관리되는 scheduler로 실행되게 flatmap해서 사용해야, http-netty-connector가 효율적으로 request를 accept 할 수 있다. 그렇기 때문에, R2DBC 또는 reactive하지 않은 방식으로 DB를 사용할 경우 오히려 servlet 방식보다 성능이 떨어질 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;pre id=&quot;code_1698400371095&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;ServerResponse&amp;gt; test(final ServerRequest serverRequest) {
  return serverRequest.bodyToMono(HashMap.class)
      .doOnNext(body -&amp;gt; log.info(Thread.currentThread().getName()))
      .doOnNext(body -&amp;gt; log.info(&quot;1&quot;))
      .doOnNext(body -&amp;gt; log.info(&quot;2&quot;))
      .subscribeOn(Schedulers.boundedElastic())
      .flatMap(x -&amp;gt; ServerResponse.ok().build());
}

2023-10-26T13:11:29.742+09:00  INFO 10884 --- [ctor-http-nio-3] c.h.i.s.n.s.inbound.rest.TestRouter      : reactor-http-nio-3
2023-10-26T13:11:29.742+09:00  INFO 10884 --- [ctor-http-nio-3] c.h.i.s.n.s.inbound.rest.TestRouter      : 1
2023-10-26T13:11:29.742+09:00  INFO 10884 --- [ctor-http-nio-3] c.h.i.s.n.s.inbound.rest.TestRouter      : 2&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;netty에서 이미 Scheduler를 지정했기 때문에 subscribeOn이 적용되지 못한 상황 예시&lt;/blockquote&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;Schedule 활용 예시&lt;/h3&gt;
&lt;h4 id=&quot;1.-Webflux-Request-handling-최적화&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;9508&quot; data-ke-size=&quot;size20&quot;&gt;1. Webflux Request handling 최적화&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1698400414070&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;ServerResponse&amp;gt; test(final ServerRequest serverRequest) {
  return serverRequest.bodyToMono(HashMap.class)
      .doOnNext(x -&amp;gt; wait1000(&quot;1&quot;))
      .doOnNext(x -&amp;gt; wait1000(&quot;2&quot;))
      .doOnNext(x -&amp;gt; wait1000(&quot;3&quot;))
      .flatMap(x -&amp;gt; ServerResponse.ok().build());
}

2023-10-26T13:51:49.133+09:00  INFO 19628 --- [ctor-http-nio-3] c.h.i.s.n.s.inbound.rest.TestRouter      : 1: reactor-http-nio-3
2023-10-26T13:51:50.144+09:00  INFO 19628 --- [ctor-http-nio-3] c.h.i.s.n.s.inbound.rest.TestRouter      : 2: reactor-http-nio-3
2023-10-26T13:51:51.160+09:00  INFO 19628 --- [ctor-http-nio-3] c.h.i.s.n.s.inbound.rest.TestRouter      : 3: reactor-http-nio-3&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1698400421267&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Scheduler 적용
public Mono&amp;lt;ServerResponse&amp;gt; test(final ServerRequest serverRequest) {
  return serverRequest.bodyToMono(HashMap.class)
      .publishOn(Schedulers.boundedElastic())
      .doOnNext(x -&amp;gt; wait1000(&quot;1&quot;))
      .doOnNext(x -&amp;gt; wait1000(&quot;2&quot;))
      .doOnNext(x -&amp;gt; wait1000(&quot;3&quot;))
      .flatMap(x -&amp;gt; ServerResponse.ok().build());
}

2023-10-26T13:53:37.177+09:00  INFO 24192 --- [oundedElastic-1] c.h.i.s.n.s.inbound.rest.TestRouter      : 1: boundedElastic-1
2023-10-26T13:53:38.187+09:00  INFO 24192 --- [oundedElastic-1] c.h.i.s.n.s.inbound.rest.TestRouter      : 2: boundedElastic-1
2023-10-26T13:53:39.200+09:00  INFO 24192 --- [oundedElastic-1] c.h.i.s.n.s.inbound.rest.TestRouter      : 3: boundedElastic-1&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Webflux에서 시간이 오래 소요 될 것 같은 작업은 publishOn을 통해 reactor Thread를 release&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 id=&quot;2.-여러-작업을-동시에-execute-할-때&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;11005&quot; data-ke-size=&quot;size20&quot;&gt;2. 여러 작업을 동시에 execute 할 때&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1698400455267&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;ServerResponse&amp;gt; test(final ServerRequest serverRequest) {
  return serverRequest.bodyToMono(HashMap.class)
      .doOnNext(x -&amp;gt; Mono.just(x)
          .subscribeOn(Schedulers.boundedElastic())
          .doOnNext(xx -&amp;gt; wait2000(&quot;1&quot;))
          .subscribe())
      .doOnNext(x -&amp;gt; Mono.just(x)
          .subscribeOn(Schedulers.boundedElastic())
          .doOnNext(xx -&amp;gt; wait1000(&quot;2&quot;))
          .subscribe())
      .doOnNext(x -&amp;gt; Mono.just(x)
          .subscribeOn(Schedulers.boundedElastic())
          .doOnNext(xx -&amp;gt; wait1000(&quot;3&quot;))
          .subscribe())
      .doOnNext(body -&amp;gt; log.info(&quot;last&quot;))
      .flatMap(x -&amp;gt; ServerResponse.ok().build());
}

2023-10-26T14:10:30.927+09:00  INFO 15004 --- [ctor-http-nio-3] c.h.i.s.n.s.inbound.rest.TestRouter      : last
2023-10-26T14:10:31.930+09:00  INFO 15004 --- [oundedElastic-6] c.h.i.s.n.s.inbound.rest.TestRouter      : 3: boundedElastic-6
2023-10-26T14:10:31.930+09:00  INFO 15004 --- [oundedElastic-1] c.h.i.s.n.s.inbound.rest.TestRouter      : 2: boundedElastic-1
2023-10-26T14:10:32.933+09:00  INFO 15004 --- [oundedElastic-4] c.h.i.s.n.s.inbound.rest.TestRouter      : 1: boundedElastic-4&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;별도의 scheduler를 설정해서 subscribe를 하는 경우&lt;br /&gt;메인 flow 외 별도의 flow로 consume을 하게 된다.&lt;br /&gt;이 상황에 Scheduler를 설정하면 손쉽게 해당 flow를 별도의 schedule로 동작하게 할 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 id=&quot;3.-전역-또는-Scoped에서-한번에-하나만-실행&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;12333&quot; data-ke-size=&quot;size20&quot;&gt;3. 전역 또는 Scoped에서 한번에 하나만 실행&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1698400484864&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Scheduled(&quot;...&quot;)
public void batch1_1() {
  Mono.just(&quot;1&quot;)
      .publishOn(Schedulers.single())
      .doOnNext(TestRouter::wait1000)
      .subscribe();
}
@Scheduled(&quot;...&quot;)
public void batch1_2() {
  Mono.just(&quot;2&quot;)
      .publishOn(Schedulers.single())
      .doOnNext(TestRouter::wait1000)
      .subscribe();
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위 상황처럼 배치가 여러개 돌아갈 때&lt;br /&gt;한번에 하나씩만 수행하도록 하고 싶다면, single() 스케쥴러를 통해 스케쥴링 하면 된다.&lt;br /&gt;이러면 batch1_1과 batch1_2가 동시에 수행되는 것을 방지할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;- Reactor진영에서는 Scheduler를 통해 로직이 실행 될 Thread 환경을 지정할 수 있다.&lt;br /&gt;&amp;nbsp;- 로직의 실행 Thread에 대한 주도권을 가질 땐 PublishOn, 로직을 subscribe할 때 default를 지정 할 목적이면 subscribeOn&lt;br /&gt;&amp;nbsp;- Webflux에서 I/O등 시간이 걸리는 작업은 scheduler를 지정하여(elastic 추천) 수행&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/blog/2019/12/13/flight-of-the-flux-3-hopping-threads-and-schedulers&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://spring.io/blog/2019/12/13/flight-of-the-flux-3-hopping-threads-and-schedulers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>spring</category>
      <category>spring webflux</category>
      <category>webflux</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/68</guid>
      <comments>https://p-bear.tistory.com/68#entry68comment</comments>
      <pubDate>Mon, 30 Oct 2023 19:56:02 +0900</pubDate>
    </item>
    <item>
      <title>Spring RSocket Code 분석</title>
      <link>https://p-bear.tistory.com/67</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5Akdc/btszkbfFe12/ocpKZ1JHITzzK7yJ7yEbF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5Akdc/btszkbfFe12/ocpKZ1JHITzzK7yJ7yEbF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5Akdc/btszkbfFe12/ocpKZ1JHITzzK7yJ7yEbF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5Akdc%2FbtszkbfFe12%2FocpKZ1JHITzzK7yJ7yEbF0%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;449&quot; height=&quot;112&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;112&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개요&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RSocket을 Spring에 Integration 하는 코드 분석&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RSocket은 Spring-Message 라이브러리를 통해 Spring Context에 Integration 되었다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ini Server&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1698399668606&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class AbstractMethodMessageHandler&amp;lt;T&amp;gt;
		implements ReactiveMessageHandler, ApplicationContextAware, InitializingBean, BeanNameAware {
		...
    @Override
	public void afterPropertiesSet() {
		List&amp;lt;? extends HandlerMethodArgumentResolver&amp;gt; resolvers = initArgumentResolvers();
		...
		this.invocableHelper.addArgumentResolvers(resolvers);
		List&amp;lt;? extends HandlerMethodReturnValueHandler&amp;gt; handlers = initReturnValueHandlers();
		...
		this.invocableHelper.addReturnValueHandlers(handlers);
		initHandlerMethods();
	}
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1698399674881&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class RSocketMessageHandler extends MessageMappingMessageHandler {
    ...
    @Override
	public void afterPropertiesSet() {
		// Add argument resolver before parent initializes argument resolution
		getArgumentResolverConfigurer().addCustomResolver(new RSocketRequesterMethodArgumentResolver());

		super.afterPropertiesSet();

		getHandlerMethods().forEach((composite, handler) -&amp;gt; {
			if (composite.getMessageConditions().contains(RSocketFrameTypeMessageCondition.CONNECT_CONDITION)) {
				MethodParameter returnType = handler.getReturnType();
				if (getCardinality(returnType) &amp;gt; 0) {
					throw new IllegalStateException(
							&quot;Invalid @ConnectMapping method. &quot; +
									&quot;Return type must be void or a void async type: &quot; + handler);
				}
			}
		});
	}
	...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;spring message에서 구현된 reactive 환경의 invoke handle method 관련 클래스&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;InitializingBean을 구현하여, spring init 시점에 handlerMethod들을 load 한다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;invocableHelper는 추후 payload를 handling할 때 사용&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Handle Payload&lt;/span&gt;&lt;/h2&gt;
&lt;pre id=&quot;code_1698399696662&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
@Override
public Mono&amp;lt;Payload&amp;gt; requestResponse(Payload payload) {
	return handleAndReply(payload, FrameType.REQUEST_RESPONSE, Flux.just(payload)).next();
}
...


private Flux&amp;lt;Payload&amp;gt; handleAndReply(Payload firstPayload, FrameType frameType, Flux&amp;lt;Payload&amp;gt; payloads) {
	...
	MessageHeaders headers = createHeaders(firstPayload, frameType, responseRef);
	...
	Flux&amp;lt;DataBuffer&amp;gt; buffers = payloads.map(this::retainDataAndReleasePayload).doOnSubscribe(s -&amp;gt; read.set(true));
	Message&amp;lt;Flux&amp;lt;DataBuffer&amp;gt;&amp;gt; message = MessageBuilder.createMessage(buffers, headers);
	return Mono.defer(() -&amp;gt; this.messageHandler.handleMessage(message))
			...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;RSocket 메시지를 파싱하고 fire&amp;amp;forget, request&amp;amp;response, request&amp;amp;stream 등 정보를 바탕으로 호출되는 메서드이다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. payload에서 header 파싱&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. payload에서 DataBuffer로 read&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. DataBuffer + header 로 Message 생성&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. messageHandler로 messsage handle&lt;/span&gt;&lt;/blockquote&gt;
&lt;h3 id=&quot;AbstractMethodMessageHandler-(spring-messaging)&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;2511&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;AbstractMethodMessageHandler (spring-messaging)&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1698399743041&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public Mono&amp;lt;Void&amp;gt; handleMessage(Message&amp;lt;?&amp;gt; message) throws MessagingException {
	...
	match = getHandlerMethod(message);
	...
	return handleMatch(match.mapping, match.handlereMthod, message);
}

protected Mono&amp;lt;Void&amp;gt; handleMatch(T mapping, HandlerMethod handlerMethod, Message&amp;lt;?&amp;gt; message) {
	handlerMethod = handlerMethod.createWithResolvedBean();
	return this.invocableHelper.handleMessage(handlerMethod, message);
}

public Mono&amp;lt;Void&amp;gt; handleMessage(HandlerMethod handlerMethod, Message&amp;lt;?&amp;gt; message) {
	InvocableHandlerMethod invocable = initMessageMappingMethod(handlerMethod);
	..
	return invocable.invoke(message)
			.switchIfEmpty(Mono.defer(() -&amp;gt; handleReturnValue(null, invocable, message)))
			.flatMap(returnValue -&amp;gt; handleReturnValue(returnValue, invocable, message))
			...
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1698399756190&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private Match&amp;lt;T&amp;gt; getHandlerMethod(Message&amp;lt;?&amp;gt; message) {
	List&amp;lt;Match&amp;lt;T&amp;gt;&amp;gt; matches = new ArrayList&amp;lt;&amp;gt;();

	RouteMatcher.Route destination = getDestination(message);
	List&amp;lt;T&amp;gt; mappingsByUrl = (destination != null ? this.destinationLookup.get(destination.value()) : null);
	if (mappingsByUrl != null) {
		addMatchesToCollection(mappingsByUrl, message, matches);
	}
	if (matches.isEmpty()) {
		// No direct hits, go through all mappings
		Set&amp;lt;T&amp;gt; allMappings = this.handlerMethods.keySet();
		addMatchesToCollection(allMappings, message, matches);
	}
	if (matches.isEmpty()) {
		handleNoMatch(destination, message);
		return null;
	}
	Comparator&amp;lt;Match&amp;lt;T&amp;gt;&amp;gt; comparator = new MatchComparator(getMappingComparator(message));
	matches.sort(comparator);
	if (logger.isTraceEnabled()) {
		logger.trace(&quot;Found &quot; + matches.size() + &quot; handler methods: &quot; + matches);
	}
	Match&amp;lt;T&amp;gt; bestMatch = matches.get(0);
	if (matches.size() &amp;gt; 1) {
		Match&amp;lt;T&amp;gt; secondBestMatch = matches.get(1);
		if (comparator.compare(bestMatch, secondBestMatch) == 0) {
			HandlerMethod m1 = bestMatch.handlerMethod;
			HandlerMethod m2 = secondBestMatch.handlerMethod;
			throw new IllegalStateException(&quot;Ambiguous handler methods mapped for destination '&quot; +
					(destination != null ? destination.value() : &quot;&quot;) + &quot;': {&quot; +
					m1.getShortLogMessage() + &quot;, &quot; + m2.getShortLogMessage() + &quot;}&quot;);
		}
	}
	return bestMatch;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000; text-align: left;&quot;&gt; handlerMethod &lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;spring-messaging에서 구현한 class이다. method invoke하기 위한 모든 정보를 맴버로 갖는다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Match&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;mapping 정보 + handlerMethod를 가진 spring-messageing의 private class&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. Message를 handle할 수 있는 method를 찾는다 (mapping 정보 등..)&lt;br /&gt;2. invoke가 가능한 method 프락시 객체 생성&lt;br /&gt;3. reflection으로 method 실행&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>CODE</category>
      <category>rsocket</category>
      <category>spring</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/67</guid>
      <comments>https://p-bear.tistory.com/67#entry67comment</comments>
      <pubDate>Sun, 29 Oct 2023 13:43:49 +0900</pubDate>
    </item>
    <item>
      <title>Spring Cloud Gateway Code 분석</title>
      <link>https://p-bear.tistory.com/66</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;237&quot; data-origin-height=&quot;213&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVaRj5/btszklvDUGR/QTcHq3OmCaZzzWE7KrNxA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVaRj5/btszklvDUGR/QTcHq3OmCaZzzWE7KrNxA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVaRj5/btszklvDUGR/QTcHq3OmCaZzzWE7KrNxA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVaRj5%2FbtszklvDUGR%2FQTcHq3OmCaZzzWE7KrNxA0%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;237&quot; height=&quot;213&quot; data-origin-width=&quot;237&quot; data-origin-height=&quot;213&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Cloud Gateway 동작 원리를 code를 통해 분석&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;Spring-Cloud-Gateway-동작-원리&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;42&quot; data-ke-size=&quot;size26&quot;&gt;Spring Cloud Gateway 동작 원리&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id=&quot;Predicates-and-filters-are-specific-to-routes&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;70&quot; data-ke-size=&quot;size23&quot;&gt;Predicates and filters are specific to routes&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Cloud Gateway의 핵심 아이디어인 &amp;ldquo;Routing 및 지역적 filter apply&amp;rdquo; 를 위해 HandlerMapping을 구현해서 추가했다.&lt;br /&gt;HandlerMapping은 Filter 이후 Exchange를 처리할 Handler를 찾는 클래스이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;RoutePredicateHandlerMapping으로 구현했으며, 구현된 핵심은&lt;br /&gt;exchange를 기반으로 Route를 찾아서 attribute에 route를 넣는 작업&lt;br /&gt;이후 FilteringWebHandler(webHandler 구현체)를 통해 작업을 진행&lt;br /&gt;&amp;nbsp;&lt;br /&gt;FilteringWebHandler는 exchange에서 Route를 가져와서 필터가 필요한지 확인한다.&lt;br /&gt;적용된 필터와, GlobalFilter랑 합쳐서 FilterChain을 만들어 처리한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;GatewayAutoConfiguration&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;536&quot; data-ke-size=&quot;size23&quot;&gt;GatewayAutoConfiguration&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1698399187178&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = &quot;spring.cloud.gateway.enabled&quot;, matchIfMissing = true)
@EnableConfigurationProperties
@AutoConfigureBefore({ HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class })
@AutoConfigureAfter({ GatewayReactiveLoadBalancerClientAutoConfiguration.class,
		GatewayClassPathWarningAutoConfiguration.class })
@ConditionalOnClass(DispatcherHandler.class)
public class GatewayAutoConfiguration {
  ...
  
  @Bean
  @ConditionalOnMissingBean
  public FilteringWebHandler filteringWebHandler(List&amp;lt;GlobalFilter&amp;gt; globalFilters) {
		return new FilteringWebHandler(globalFilters);
  }
	
  ...
  
  @Bean
  @ConditionalOnMissingBean
  public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHandler webHandler,
			RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties, Environment environment) {
		return new RoutePredicateHandlerMapping(webHandler, routeLocator, globalCorsProperties, environment);
  }
  
  ...
}&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;955&quot; data-origin-height=&quot;604&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9Lvh2/btszhofS16M/1QgxTogGyC8pxDXHxKk3Y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9Lvh2/btszhofS16M/1QgxTogGyC8pxDXHxKk3Y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9Lvh2/btszhofS16M/1QgxTogGyC8pxDXHxKk3Y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9Lvh2%2FbtszhofS16M%2F1QgxTogGyC8pxDXHxKk3Y0%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;955&quot; height=&quot;604&quot; data-origin-width=&quot;955&quot; data-origin-height=&quot;604&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 id=&quot;RoutePredicateHandlerMapping&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1581&quot; data-ke-size=&quot;size23&quot;&gt;RoutePredicateHandlerMapping&lt;/h3&gt;
&lt;pre id=&quot;code_1698399225886&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AbstractHandlerMapping
public Mono&amp;lt;Object&amp;gt; getHandler(ServerWebExchange exchange) {
  return this.getHandlerInternal(exchange).map((handler) -&amp;gt; {
    ...
    ServerHttpRequest request = exchange.getRequest();
    ...
    return handler;
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;HandlerMapping은 전략패턴으로 구성되었으며&lt;br /&gt;구현체의 getHandlerInternal()을 호출해 Mono로 Handler를 받아 Handler 공통 작업을 수행한다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1698399239569&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
protected Mono&amp;lt;?&amp;gt; getHandlerInternal(ServerWebExchange exchange) {
	...

	return Mono.deferContextual(contextView -&amp;gt; {
		...
		return lookupRoute(exchange)
				.flatMap((Function&amp;lt;Route, Mono&amp;lt;?&amp;gt;&amp;gt;) r -&amp;gt; {
					...

					exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
					return Mono.just(webHandler);
				}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -&amp;gt; {
					...
				})));
	});
}

protected Mono&amp;lt;Route&amp;gt; lookupRoute(ServerWebExchange exchange) {
	return this.routeLocator.getRoutes()
			.concatMap(route -&amp;gt; Mono.just(route).filterWhen(r -&amp;gt; {
				exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
				return r.getPredicate().apply(exchange);
			})
					...
			.next()
			.map(route -&amp;gt; {
				...
				validateRoute(route, exchange);
				return route;
			});
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;RoutePredicateHandlerMapping은 lookupRoute로 Predicate에 부합하는 Route 객체를 exchange에 attribute로 put 한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;FilteringWebHandler-(Gateway)&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;2953&quot; data-ke-size=&quot;size23&quot;&gt;FilteringWebHandler (Gateway)&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1698399291267&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public Mono&amp;lt;Void&amp;gt; handle(ServerWebExchange exchange) {
    Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
    List&amp;lt;GatewayFilter&amp;gt; gatewayFilters = route.getFilters();

    List&amp;lt;GatewayFilter&amp;gt; combined = new ArrayList&amp;lt;&amp;gt;(this.globalFilters);
    combined.addAll(gatewayFilters);
    AnnotationAwareOrderComparator.sort(combined);

    ...

    return new DefaultGatewayFilterChain(combined).filter(exchange);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;HandlerMapping의 getHandler() 는 최종적으로 &lt;br /&gt;return Mono.just(webHandler); 를 통해서 FilteringWebHandler를 반환한다.&lt;br /&gt;이 후 handlerAdapter에서 FilteringWebHandler의 handle()을 호출한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 id=&quot;Match-Exchange-with-Route&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3589&quot; data-ke-size=&quot;size26&quot;&gt;Match Exchange with Route&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id=&quot;lookupRoute&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;3616&quot; data-ke-size=&quot;size23&quot;&gt;lookupRoute&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;pre id=&quot;code_1698399319341&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// RoutePredicateHandlerMapping.java
protected Mono&amp;lt;Route&amp;gt; lookupRoute(ServerWebExchange exchange) {
	return this.routeLocator.getRoutes()
			.concatMap(route -&amp;gt; Mono.just(route).filterWhen(r -&amp;gt; {
				exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
				return r.getPredicate().apply(exchange);
			})
			...
			.next()
			.map(route -&amp;gt; {
				...
				validateRoute(route, exchange);
				return route;
			});
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1698399330711&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// RouteDefinitionRouteLocator
@Override
public Flux&amp;lt;Route&amp;gt; getRoutes() {
	return getRoutes(this.routeDefinitionLocator.getRouteDefinitions()); // RouteDefinitions from CacheFlux
}

private Flux&amp;lt;Route&amp;gt; getRoutes(Flux&amp;lt;RouteDefinition&amp;gt; routeDefinitions) {
	Flux&amp;lt;Route&amp;gt; routes = routeDefinitions.map(this::convertToRoute);
    ...
	return routes.map(route -&amp;gt; {
		...
		return route;
	});
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Cache로 부터 모든 Route 객체를 변환 받아 predicate를 돌린다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 id=&quot;번외:-Predicate-Route&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;4499&quot; data-ke-size=&quot;size23&quot;&gt;번외: Predicate Route&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #000000; color: #000000;&quot; data-panel-type=&quot;info&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Java Predicate를 reactior를 통해 Async하게 test할 수 있게 하는 Spring Cloud Gateway의 아이디어이다.&lt;br /&gt;사용자가 작성한 Predicate 전체를 sync하게 처리하지 않고, 모든 predicate를 async하게 바꿔 처리하기 위함으로 추정된다.&lt;br /&gt;delegate와 decorate 패턴을 활용한다.&lt;/blockquote&gt;
&lt;h4 id=&quot;Java-Predicate&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;4715&quot; data-ke-size=&quot;size20&quot;&gt;Java Predicate&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1698399386474&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FunctionalInterface
public interface Predicate&amp;lt;T&amp;gt; {
  boolean test(T var1);

  default Predicate&amp;lt;T&amp;gt; and(Predicate&amp;lt;? super T&amp;gt; other) {
    Objects.requireNonNull(other);
    return (t) -&amp;gt; {
      return this.test(t) &amp;amp;&amp;amp; other.test(t);
    };
  }

  default Predicate&amp;lt;T&amp;gt; negate() {
    return (t) -&amp;gt; {
      return !this.test(t);
    };
  }

  default Predicate&amp;lt;T&amp;gt; or(Predicate&amp;lt;? super T&amp;gt; other) {
    Objects.requireNonNull(other);
    return (t) -&amp;gt; {
      return this.test(t) || other.test(t);
    };
  }

  static &amp;lt;T&amp;gt; Predicate&amp;lt;T&amp;gt; isEqual(Object targetRef) {
    return null == targetRef ? Objects::isNull : (object) -&amp;gt; {
      return targetRef.equals(object);
    };
  }

  static &amp;lt;T&amp;gt; Predicate&amp;lt;T&amp;gt; not(Predicate&amp;lt;? super T&amp;gt; target) {
    Objects.requireNonNull(target);
    return target.negate();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Java Objects의 Functional 인테페이스이다.&lt;br /&gt;특정 Object의 조건을 서술한 Function을 사전에 정의하여&lt;br /&gt;다른 로직에서 해당 조건을 불러와 사용할 수 있다.&lt;br /&gt;또한&lt;br /&gt;and, or, negate, not, isEqual 메서드를 지원하여 function의 chaining을 할 수 있게 했다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1698399401620&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 심플한 사용 예
Predicate&amp;lt;Object&amp;gt; isString = o -&amp;gt; o instanceof String;
System.out.println(isString.test(&quot;&quot;)); // true
System.out.println(isString.test(1)); // false

// predicate Chain
Predicate&amp;lt;String&amp;gt; isStartWithA = s -&amp;gt; s.startsWith(&quot;A&quot;);
Predicate&amp;lt;String&amp;gt; isEndWithA = s -&amp;gt; s.endsWith(&quot;A&quot;);
System.out.println(isStartWithA.and(isEndWithA).test(&quot;ABB&quot;)); // false
System.out.println(isStartWithA.and(isEndWithA).test(&quot;ABA&quot;)); // true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;Predicate-with-Reactor&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;6155&quot; data-ke-size=&quot;size23&quot;&gt;Predicate with Reactor&lt;/h3&gt;
&lt;pre id=&quot;code_1698399417835&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface AsyncPredicate&amp;lt;T&amp;gt; extends Function&amp;lt;T, Publisher&amp;lt;Boolean&amp;gt;&amp;gt;, HasConfig {
  default AsyncPredicate&amp;lt;T&amp;gt; and(AsyncPredicate&amp;lt;? super T&amp;gt; other) {
	return new AndAsyncPredicate&amp;lt;&amp;gt;(this, other);
  }
  
  default AsyncPredicate&amp;lt;T&amp;gt; negate() {
		return new NegateAsyncPredicate&amp;lt;&amp;gt;(this);
  }
  
  default AsyncPredicate&amp;lt;T&amp;gt; not(AsyncPredicate&amp;lt;? super T&amp;gt; other) {
		return new NegateAsyncPredicate&amp;lt;&amp;gt;(other);
  }
  
  default AsyncPredicate&amp;lt;T&amp;gt; or(AsyncPredicate&amp;lt;? super T&amp;gt; other) {
		return new OrAsyncPredicate&amp;lt;&amp;gt;(this, other);
  }
	
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Predicate를 Reactive 환경에서 Async하게 처리하기 위해 구현된 파트 중 일부이다.&lt;br /&gt;Predicate의 체인을 만들고&lt;br /&gt;Java Function apply를 통해 모든 Predicate를 순서있는 비동기 처리를 구현했다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분석&lt;/h3&gt;
&lt;pre id=&quot;code_1698399437320&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Default
@Override
public Publisher&amp;lt;Boolean&amp;gt; apply(T t) {
	return Mono.just(delegate.test(t));
}

// Negate
@Override
public Publisher&amp;lt;Boolean&amp;gt; apply(T t) {
	return Mono.from(predicate.apply(t)).map(b -&amp;gt; !b);
}

// And
@Override
public Publisher&amp;lt;Boolean&amp;gt; apply(T t) {
	return Mono.from(left.apply(t)).flatMap(result -&amp;gt; !result ? Mono.just(false) : Mono.from(right.apply(t)));
}

// Or
@Override
public Publisher&amp;lt;Boolean&amp;gt; apply(T t) {
	return Mono.from(left.apply(t)).flatMap(result -&amp;gt; result ? Mono.just(true) : Mono.from(right.apply(t)));
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;default는 최초의 Predicate를 받아 delegate 패턴을 통해 predicate를 위임하는 기초 구현체이다.&lt;br /&gt;negate, and, or는 다른 AsyncPredicate를 생성자로 받아 apply 시 각각의 로직을 publisher에 append하는 구현체이다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1698399452662&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Simple Example
Predicate&amp;lt;String&amp;gt; isStartWithA = s -&amp;gt; s.startsWith(&quot;A&quot;);
AsyncPredicate.DefaultAsyncPredicate&amp;lt;String&amp;gt; predicate1 = new AsyncPredicate.DefaultAsyncPredicate&amp;lt;&amp;gt;(isStartWithA);

Predicate&amp;lt;String&amp;gt; isEndWithA = s -&amp;gt; s.endsWith(&quot;A&quot;);
AsyncPredicate.DefaultAsyncPredicate&amp;lt;String&amp;gt; predicate2 = new AsyncPredicate.DefaultAsyncPredicate&amp;lt;&amp;gt;(isEndWithA);

AsyncPredicate.OrAsyncPredicate&amp;lt;String&amp;gt; orPredicate = new AsyncPredicate.OrAsyncPredicate&amp;lt;&amp;gt;(predicate1, predicate2);

Flux.just(&quot;BBB&quot;, &quot;ABB&quot;, &quot;ABA&quot;)
    .flatMap(orPredicate)
    .doOnNext(System.out::println)
    .subscribe();

-&amp;gt; false, true, true&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;AsyncPredicate-in-Spring-Cloud-Gateway&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;8178&quot; data-ke-size=&quot;size23&quot;&gt;AsyncPredicate in Spring Cloud Gateway&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1698399479544&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// RouteDefinitionRouteLocator
private Route convertToRoute(RouteDefinition routeDefinition) {
	AsyncPredicate&amp;lt;ServerWebExchange&amp;gt; predicate = combinePredicates(routeDefinition);
	...
}

private AsyncPredicate&amp;lt;ServerWebExchange&amp;gt; combinePredicates(RouteDefinition routeDefinition) {
  ...
  AsyncPredicate&amp;lt;ServerWebExchange&amp;gt; predicate = lookup(routeDefinition, predicates.get(0));
  for (PredicateDefinition andPredicate : predicates.subList(1, predicates.size())) {
		AsyncPredicate&amp;lt;ServerWebExchange&amp;gt; found = lookup(routeDefinition, andPredicate);
		predicate = predicate.and(found);
  }
  return predicate;
}

// RoutePredicateHandlerMapping
protected Mono&amp;lt;Route&amp;gt; lookupRoute(ServerWebExchange exchange) {
	return this.routeLocator.getRoutes()
		.concatMap(route -&amp;gt; Mono.just(route).filterWhen(r -&amp;gt; {
			...
			return r.getPredicate().apply(exchange);
		})
		...
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;RouteDefinition: Library 사용자가 Route Rule에 대해 정의 할 수 있는 객체이다.&lt;br /&gt;Spring Cloud Gateway는 사용자가 정의한 RouteDefinition에서 predicate들을 flat한 AsyncPredicate로 만든다.&lt;br /&gt;이 후 RoutePredicateHandlerMapping 에서 predicate를 통해 route를 찾을 때 사용한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>CODE</category>
      <category>gateway</category>
      <category>spring</category>
      <category>Spring Cloud Gateway</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/66</guid>
      <comments>https://p-bear.tistory.com/66#entry66comment</comments>
      <pubDate>Sat, 28 Oct 2023 12:38:25 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux, HttpHandler code 분석</title>
      <link>https://p-bear.tistory.com/65</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfVeBe/btszj9aXm2u/x6vupU1cXPrGmV0Q2dpPMk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfVeBe/btszj9aXm2u/x6vupU1cXPrGmV0Q2dpPMk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfVeBe/btszj9aXm2u/x6vupU1cXPrGmV0Q2dpPMk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfVeBe%2Fbtszj9aXm2u%2Fx6vupU1cXPrGmV0Q2dpPMk%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;168&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;168&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Webflux가 http Request를 받아서 handler를 찾아 mapping, execute 및 response handle하는 과정에 대한 분석을 진행한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Webflux HttpHandler 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HttpHandler&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;139&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn8KnA/btszkSzPdO6/CsXheewSqrTlaysfoAiO1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn8KnA/btszkSzPdO6/CsXheewSqrTlaysfoAiO1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn8KnA/btszkSzPdO6/CsXheewSqrTlaysfoAiO1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn8KnA%2FbtszkSzPdO6%2FCsXheewSqrTlaysfoAiO1k%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;998&quot; height=&quot;139&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;139&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Http 요청에 대한 전반적인 처리를 담당하는 interface&lt;br /&gt;AutoConfiguration으로 Bean으로 만들어지며, Builder를 통해 생성된다. &lt;br /&gt;Builder는 생성된 Bean들을 기반으로 Handler를 생성한다. &lt;br /&gt;따라서 HttpHandler Bean 생성 시점은 매우 늦다 (order = -2147483638)&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;생성&amp;nbsp;Flow&amp;nbsp;요약&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. AutoConfig를 통해 Builder를 생성 (with applicationContext)&lt;br /&gt;2. Builder 내부에 webFliters, exceptionHandler, httpHandlerDecorator, sessionManager, codecConfigurer, contextResolver, forwardHeaderTransformer 할당 (load from bean)&lt;br /&gt;&amp;nbsp; a. HttpHandlerDecoratorFactory Bean을 통해서 httpHandlerDecorator를 load&lt;br /&gt;&amp;nbsp; b. Decorator 패턴으로 구현된 HttpHandlerDecorator를 Fuction으로 merge&lt;br /&gt;3. Builder를 통해 HttpHandler Build&lt;br /&gt;&amp;nbsp; a. DispatcherHandler를 FilteringWebHandler에 delegate 할당&lt;br /&gt;&amp;nbsp; b. FilteringWebHandler를 ExceptionHandlingWebHandler에 delegate 할당&lt;br /&gt;&amp;nbsp; c. ExceptionHandlingWebHandler를 HttpWebHandlerAdapter에 delegate 할당&lt;br /&gt;4. 기타 webflux 설정&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동작 Flow 요약&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kx0kW/btszluFqmAK/FcRhQYO4pPZ2pqT92v4OOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kx0kW/btszluFqmAK/FcRhQYO4pPZ2pqT92v4OOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kx0kW/btszluFqmAK/FcRhQYO4pPZ2pqT92v4OOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKx0kW%2FbtszluFqmAK%2FFcRhQYO4pPZ2pqT92v4OOK%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;366&quot; height=&quot;659&quot; data-origin-width=&quot;366&quot; data-origin-height=&quot;659&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. ServerHttpRequest 수신&lt;br /&gt;2. Exchange 생성&lt;br /&gt;3. ExceptionHandler 추가 (onErrorResume)&lt;br /&gt;&amp;nbsp; a. 추가 후 다음 handler 호출&lt;br /&gt;4. FilterHandler에서 filterChain으로 filter(exchange)&lt;br /&gt;&amp;nbsp; a. filter 후 다음 handler 호출&lt;br /&gt;5. DispatcherHandler에서 exchange Handling&lt;br /&gt;&amp;nbsp; a. HandlerMapping을 통해 target handler 찾기&lt;br /&gt;&amp;nbsp; b. handler를 support하는 adapter를 통해 handler(exchange)&lt;br /&gt;&amp;nbsp; c. handle 결과에 대해서 처리 (rest응답, viewModel 등&amp;hellip;)&lt;/blockquote&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1698396417025&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// HttpHandlerAutoConfiguration
@AutoConfiguration(
  after = {WebFluxAutoConfiguration.class}
)
@ConditionalOnClass({DispatcherHandler.class, HttpHandler.class})
@ConditionalOnWebApplication(
  type = Type.REACTIVE
)
@ConditionalOnMissingBean({HttpHandler.class})
@AutoConfigureOrder(-2147483638)
public class HttpHandlerAutoConfiguration {
  ...

  @Bean
  public HttpHandler httpHandler(ObjectProvider&amp;lt;WebFluxProperties&amp;gt; propsProvider) {
    HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(this.applicationContext).build();
    ...
  }

  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1698396439419&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// WebHttpHandlerBuilder
// builder 생성 시점에 Bean들을 load하여, build 시점에 HttpHandler에 적용한다.

// applicationContext를 통한 생성
public static WebHttpHandlerBuilder applicationContext(ApplicationContext context) {
  // webHandler(Dispatcher) 기반 Builder 생성
  WebHttpHandlerBuilder builder = new WebHttpHandlerBuilder((WebHandler)context.getBean(&quot;webHandler&quot;, WebHandler.class), context);
  
  // WebFilter Bean 모두 가져와서 정렬 후 추가
  List&amp;lt;WebFilter&amp;gt; webFilters = context.getBeanProvider(WebFilter.class).orderedStream().toList();
  builder.filters((filters) -&amp;gt; {
    filters.addAll(webFilters);
  });
  
  // exceptionHandler들 로드해서 정렬 후 추가
  List&amp;lt;WebExceptionHandler&amp;gt; exceptionHandlers = context.getBeanProvider(WebExceptionHandler.class).orderedStream().toList();
  builder.exceptionHandlers((handlers) -&amp;gt; {
    handlers.addAll(exceptionHandlers);
  });
  
  // Decorator 모두 가져와서 정렬 후 추가
  Stream var10000 = context.getBeanProvider(HttpHandlerDecoratorFactory.class).orderedStream();
  Objects.requireNonNull(builder);
  var10000.forEach(builder::httpHandlerDecorator);

  // sessionManager 로드
  try {
    builder.sessionManager((WebSessionManager)context.getBean(&quot;webSessionManager&quot;, WebSessionManager.class));
  } catch (NoSuchBeanDefinitionException var8) {
  }

  // codecConfigurer 로드
  try {
    builder.codecConfigurer((ServerCodecConfigurer)context.getBean(&quot;serverCodecConfigurer&quot;, ServerCodecConfigurer.class));
  } catch (NoSuchBeanDefinitionException var7) {
  }

  // localeContextResolver 로드
  try {
    builder.localeContextResolver((LocaleContextResolver)context.getBean(&quot;localeContextResolver&quot;, LocaleContextResolver.class));
  } catch (NoSuchBeanDefinitionException var6) {
  }

  // forwardedHeaderTransformer 로드
  try {
    builder.forwardedHeaderTransformer((ForwardedHeaderTransformer)context.getBean(&quot;forwardedHeaderTransformer&quot;, ForwardedHeaderTransformer.class));
  } catch (NoSuchBeanDefinitionException var5) {
  }

  return builder;
}

// HttpHandler build
public HttpHandler build() {
  // webHandler Merge (DispathcerHandler + ExceptionHandlingWebHandler + FilteringWebHandler)
  WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
  WebHandler decorated = new ExceptionHandlingWebHandler(decorated, this.exceptionHandlers);
  HttpWebHandlerAdapter adapted = new HttpWebHandlerAdapter(decorated);
  if (this.sessionManager != null) {
    adapted.setSessionManager(this.sessionManager);
  }

  if (this.codecConfigurer != null) {
    adapted.setCodecConfigurer(this.codecConfigurer);
  }

  if (this.localeContextResolver != null) {
    adapted.setLocaleContextResolver(this.localeContextResolver);
  }

  if (this.forwardedHeaderTransformer != null) {
    adapted.setForwardedHeaderTransformer(this.forwardedHeaderTransformer);
  }

  if (this.applicationContext != null) {
    adapted.setApplicationContext(this.applicationContext);
  }

  adapted.afterPropertiesSet();
  return (HttpHandler)(this.httpHandlerDecorator != null ? (HttpHandler)this.httpHandlerDecorator.apply(adapted) : adapted);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;HttpWebHandlerAdapter&amp;nbsp;(implements&amp;nbsp;HttpHandler)&lt;/h3&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;584&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfHd7K/btszgxcWPgg/jq7W66zjx07hywbqWkKMIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfHd7K/btszgxcWPgg/jq7W66zjx07hywbqWkKMIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfHd7K/btszgxcWPgg/jq7W66zjx07hywbqWkKMIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfHd7K%2FbtszgxcWPgg%2Fjq7W66zjx07hywbqWkKMIk%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;986&quot; height=&quot;584&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;584&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;blockquote data-pm-slice=&quot;0 0 []&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot; data-ke-style=&quot;style2&quot;&gt;Http Request 처리를 Decorate할 수 있는 Handler 구현체이다.&lt;br /&gt;핵심 메서드는 interface의 handle 메서드와 createExchange가 있다.&lt;br /&gt;1. request, response, filter 설정등을 바탕으로 exchange 생성&lt;br /&gt;2. (let delegate) handle exchange&lt;br /&gt;실제 exchange의 Handling 처리는 delegate를 통해 진행 &lt;br /&gt;default: ExceptionHandlingWebHandler &amp;rarr; FilteringWebHandler &amp;rarr; DispatcherHandler&lt;/blockquote&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;createExchange(&amp;hellip;)&lt;/h4&gt;
&lt;pre id=&quot;code_1698396603080&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) {
  return new DefaultServerWebExchange(request, response, this.sessionManager, this.getCodecConfigurer(), this.getLocaleContextResolver(), this.applicationContext);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-pm-slice=&quot;0 0 []&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot; data-ke-style=&quot;style3&quot;&gt;ServerHttpRequest + 기타 handler들 &amp;rarr; exchange 생성&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;handle(...)&lt;/h4&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;pre id=&quot;code_1698396657956&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;Void&amp;gt; handle(ServerHttpRequest request, ServerHttpResponse response) {
  if (this.forwardedHeaderTransformer != null) {
    try {
      request = this.forwardedHeaderTransformer.apply(request);
    } catch (Throwable var4) {
      ...
    }
  }

  ServerWebExchange exchange = this.createExchange(request, response);
  ...
  Mono var10000 = this.getDelegate().handle(exchange).doOnSuccess((aVoid) -&amp;gt; {
    this.logResponse(exchange);
  })
  ...
  return var10000.then(Mono.defer(response::setComplete));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. request의 Header 처리 (Transformer)&lt;br /&gt;2. exchange 생성&lt;br /&gt;3. reqeust logging&lt;br /&gt;4. delegate (Decorator)를 통한 exchange handle&lt;br /&gt;5. success / fail 처리 (response logging, etc)&lt;br /&gt;6. response complete&lt;/blockquote&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;h3 id=&quot;ExceptionHandlingWebHandler&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;6296&quot; data-ke-size=&quot;size23&quot;&gt;ExceptionHandlingWebHandler&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;delegate handle에서 발생한 error 시그날을&lt;br /&gt;onErrorResume으로 load 된 WebExceptionHandler 처리한다.&lt;/blockquote&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;h3 id=&quot;FilteringWebHandler&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;6411&quot; data-ke-size=&quot;size23&quot;&gt;FilteringWebHandler&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1698396760716&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private final DefaultWebFilterChain chain;

public Mono&amp;lt;Void&amp;gt; handle(ServerWebExchange exchange) {
  return this.chain.filter(exchange);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;FilterChain으로 exchange를 filter&lt;br /&gt;filter 후 delegate(DispatcherHandler)로 handle() 넘김&lt;/blockquote&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;h3 id=&quot;DispatcherHandler&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;6657&quot; data-ke-size=&quot;size23&quot;&gt;DispatcherHandler&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;div style=&quot;background-color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1698396790658&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// DispathcerHandler
public Mono&amp;lt;Void&amp;gt; handle(ServerWebExchange exchange) {
  if (this.handlerMappings == null) {
    return this.createNotFoundError();
  } else {
    return CorsUtils.isPreFlightRequest(exchange.getRequest()) ? this.handlePreFlight(exchange) : Flux.fromIterable(this.handlerMappings).concatMap((mapping) -&amp;gt; {
      return mapping.getHandler(exchange);
    })
    .next()
    ...
    }).flatMap((handler) -&amp;gt; {
      return this.handleRequestWith(exchange, handler);
    });
  }
}

private Mono&amp;lt;Void&amp;gt; handleRequestWith(ServerWebExchange exchange, Object handler) {
  if (ObjectUtils.nullSafeEquals(exchange.getResponse().getStatusCode(), HttpStatus.FORBIDDEN)) {
    return Mono.empty();
  } else {
    if (this.handlerAdapters != null) {
      Iterator var3 = this.handlerAdapters.iterator();

      while(var3.hasNext()) {
        HandlerAdapter adapter = (HandlerAdapter)var3.next();
        if (adapter.supports(handler)) {
          return adapter.handle(exchange, handler).flatMap((result) -&amp;gt; {
            return this.handleResult(exchange, result);
          });
        }
      }
    }

    return Mono.error(new IllegalStateException(&quot;No HandlerAdapter: &quot; + handler));
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-pm-slice=&quot;0 0 []&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot; data-ke-style=&quot;style2&quot;&gt;1. HandlerMapping을 통해 Handler를 찾아온다.&lt;br /&gt;&amp;nbsp;- 없으면 404&lt;br /&gt;2. 현재 handler를 support 하는 HandlerAdapter를 찾아와서 handle()&lt;br /&gt;3. handlerAdapter는 @RequestMapping, RouterFunction등으로 만들어진 Bean&lt;br /&gt;4. Adapter는 각 Handler의 handle()을 호출 &lt;br /&gt;&amp;nbsp;- exchange &amp;rarr; 각 handle()의 파라미터로&lt;br /&gt;5. Adapter는 handler의 return 값을 HandlerResult에 담아서 return&lt;br /&gt;6. resultHandler들 중에서 support하는 resultHandler를 찾아서 handlerResult&lt;/blockquote&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;&lt;/div&gt;
&lt;h2 id=&quot;httpHandler-구현-아이디어&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8246&quot; data-ke-size=&quot;size26&quot;&gt;Webflux HttpHandler 구현 아이디어&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;h3 id=&quot;목표-(추정)&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;8267&quot; data-ke-size=&quot;size23&quot;&gt;목표 (추정)&lt;/h3&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 여러 하위 로직을 합쳐서 하나의 handling 로직을 만들고 싶다&lt;br /&gt;2. handling 로직을 확장성 있게 가져가고 싶다. (기존 로직에 추가하거나, 다음 단계로 놓거나..)&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아이디어&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. Decorator 패턴을 이용해서 특정 로직에 기능을 추가할 수 있는 구조를 갖는다.&lt;br /&gt;2. Delegate 패턴을 이용해서 외부에서 위임 클래스 로직을 호출할 수 있게한다.&lt;br /&gt;3. Adapter 패턴을 통해 decorate된 클래스를 최종 클래스에서 사용한다.&lt;br /&gt;&amp;nbsp; &amp;nbsp;// 이 부분은 다른 프로젝트에서 만든 httpHandler에 WebHandler를 붙이기 위함&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 분석&lt;/h3&gt;
&lt;pre id=&quot;code_1698396940146&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface HttpHandler {
  Mono&amp;lt;Void&amp;gt; handle(ServerHttpRequest request, ServerHttpResponse response);
}

public interface WebHandler {
  Mono&amp;lt;Void&amp;gt; handle(ServerWebExchange exchange);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;조합 및 확장이 필요한 기능&lt;br /&gt;&amp;nbsp;- 여러 HttpHandler 모듈에서 exchange를 받아서 req 처리 &amp;rarr; res 처리&lt;/blockquote&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;h4 id=&quot;Decorator-패턴-적용&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;8876&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; color: #000000;&quot;&gt;Decorator 패턴 적용&lt;/span&gt;&lt;/h4&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1649&quot; data-origin-height=&quot;902&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0jdhk/btszgn2Jzoy/G4UHKftozCEO59zsPQkr50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0jdhk/btszgn2Jzoy/G4UHKftozCEO59zsPQkr50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0jdhk/btszgn2Jzoy/G4UHKftozCEO59zsPQkr50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0jdhk%2Fbtszgn2Jzoy%2FG4UHKftozCEO59zsPQkr50%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;1649&quot; height=&quot;902&quot; data-origin-width=&quot;1649&quot; data-origin-height=&quot;902&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;pre id=&quot;code_1698397030184&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public HttpHandler build() {
  WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
  WebHandler decorated = new ExceptionHandlingWebHandler(decorated, this.exceptionHandlers);
  ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;확장이 필요한 기능은 WebHandlerDecorator를 확장하여 handle() 메서드를 구현한다.&lt;br /&gt;Concrete는 DispatcherHandler에 해당되며, ExceptionHandlingWebHandler, FilteringWebHandler 등은 decorate class에 해당된다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Delegate 패턴 적용&lt;/h4&gt;
&lt;pre id=&quot;code_1698397077349&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// httpHandler에서 ExceptionHandler 호출
@Override
public Mono&amp;lt;Void&amp;gt; handle(ServerHttpRequest request, ServerHttpResponse response) {
	...
	return getDelegate().handle(exchange)
			.doOnSuccess(aVoid -&amp;gt; logResponse(exchange))
			.onErrorResume(ex -&amp;gt; handleUnresolvedError(exchange, ex))
			.then(Mono.defer(response::setComplete));
}

// ExceptionHandler에서 super(Decorator)를 통해 FilteringHandler 호출
@Override
public Mono&amp;lt;Void&amp;gt; handle(ServerWebExchange exchange) {
    ...
	completion = super.handle(exchange);
	...
}
@Override
public Mono&amp;lt;Void&amp;gt; handle(ServerWebExchange exchange) {
	return this.delegate.handle(exchange);
}

// FilteringHandler에서 DefaultWebFilterChain을 통해 DispatcherHandler 호출
@Override
public Mono&amp;lt;Void&amp;gt; handle(ServerWebExchange exchange) {
	return this.chain.filter(exchange);
}
@Override
public Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange) {
	return Mono.defer(() -&amp;gt;
			this.currentFilter != null &amp;amp;&amp;amp; this.chain != null ?
					invokeFilter(this.currentFilter, this.chain, exchange) :
					this.handler.handle(exchange));
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-pm-slice=&quot;0 0 []&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot; data-ke-style=&quot;style2&quot;&gt;Decorator class에 delegate가 있다.&lt;br /&gt;앞쪽 클래스에서 자신의 로직 처리 후 delegate를 통해 다음 webHandler 처리를 위임한다.&lt;/blockquote&gt;
&lt;h4 id=&quot;Adapter를-통한-WebHandler를-HttpHandler에-적용&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;10428&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Adapter를 통한 WebHandler를 HttpHandler에 적용&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-node-type=&quot;mediaSingle&quot; data-width-type=&quot;percentage&quot; data-layout=&quot;center&quot;&gt;
&lt;div&gt;
&lt;div data-alt=&quot;&quot; data-file-mime-type=&quot;image/png&quot; data-file-size=&quot;32887&quot; data-file-name=&quot;image-20230919-074310.png&quot; data-collection=&quot;contentId-256606260&quot; data-id=&quot;3ea6467b-0afe-423b-835e-6c97ce23387e&quot; data-height=&quot;317&quot; data-width=&quot;1230&quot; data-node-type=&quot;media&quot; data-type=&quot;file&quot; data-context-id=&quot;256606260&quot;&gt;
&lt;div id=&quot;newFileExperienceWrapper&quot; data-testid=&quot;media-card-view&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJwszE/btszk4AfY6U/fbYQPHy0ZLxurcI5MPxt91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJwszE/btszk4AfY6U/fbYQPHy0ZLxurcI5MPxt91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJwszE/btszk4AfY6U/fbYQPHy0ZLxurcI5MPxt91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJwszE%2Fbtszk4AfY6U%2FfbYQPHy0ZLxurcI5MPxt91%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;1230&quot; height=&quot;317&quot; data-origin-width=&quot;1230&quot; data-origin-height=&quot;317&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;WebHandler + HttpHandler&lt;/blockquote&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-pm-slice=&quot;0 0 []&quot; data-width=&quot;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&amp;lt;HttpHandler&amp;gt;&lt;br /&gt;HttpHandler는 Http Request를 처리하는 목적으로 만든 인터페이스이다.&lt;br /&gt;Spring Webflux는 httpHandler를 통해서 http request를 handling한다.&lt;br /&gt;HttpHandler를 확장하여 구현하면, 기본 제공하는 handler를 교체할 수 있다.&lt;br /&gt;&lt;br /&gt;&amp;lt;WebHandler&amp;gt;&lt;br /&gt;WebHandler는 web에서의 request 처리를 주 목적으로 만든 인터페이스이다.&lt;br /&gt;WebHandler를 구현한 핵심 클래스는 DispatcherHandler 이다.&lt;br /&gt;DispatcherHandler는 request dispatcher에 대한 기능을 구현했다.&lt;br /&gt;&lt;br /&gt;&amp;lt;HttpHander + WebHandler&amp;gt;&lt;br /&gt;두 인터페이스를 Adapter 패턴으로 합쳤다.&lt;br /&gt;HttpHandler에서 WebHandler의 인터페이스를 호출한다.&lt;br /&gt;&lt;br /&gt;&amp;lt;WebHandler 동작 구조&amp;gt;&lt;br /&gt;기본적으로 DispatcherHandler를 호출하게 되며&lt;br /&gt;dispatch 이외의 기능은 decorator 패턴과 delegate 패턴으로 확장했다.&lt;br /&gt;decorator 클래스에서 delegate로 dispatcher 또는 다른 decorator들을 가지고 있으며, delegate 호출 전 후로 기타 로직들을 넣었다.&lt;br /&gt;위의 Adapter또한 decorator로 다른 decorator를 delegate로 가지고 있다가 호출한다.&lt;br /&gt;&lt;br /&gt;&amp;lt;기본 flow&amp;gt;&lt;br /&gt;WehHandlerAdapter -&amp;gt; ExcpetionHandlingWebHandler -&amp;gt; FilteringWebHandler -&amp;gt; DispatcherHandler&lt;/blockquote&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Decorator, Delegate 패턴으로 기능의 확장을 확인할 수 있었다.&lt;br /&gt;http handle 과정에서 추가로직을 확장하기 쉬운 구조이다.&lt;/blockquote&gt;</description>
      <category>개발 일지</category>
      <category>CODE</category>
      <category>httpHandler</category>
      <category>spring</category>
      <category>webflux</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/65</guid>
      <comments>https://p-bear.tistory.com/65#entry65comment</comments>
      <pubDate>Fri, 27 Oct 2023 18:29:08 +0900</pubDate>
    </item>
    <item>
      <title>Spring Cloud Sleuth란 - 분산 Tracing</title>
      <link>https://p-bear.tistory.com/64</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;245&quot; data-origin-height=&quot;206&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/degL1K/btstx6ZIkem/DvPr7gEaZRCMfHzb97LkVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/degL1K/btstx6ZIkem/DvPr7gEaZRCMfHzb97LkVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/degL1K/btstx6ZIkem/DvPr7gEaZRCMfHzb97LkVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdegL1K%2Fbtstx6ZIkem%2FDvPr7gEaZRCMfHzb97LkVk%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;245&quot; height=&quot;206&quot; data-origin-width=&quot;245&quot; data-origin-height=&quot;206&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring&amp;nbsp;Cloud&amp;nbsp;Sleuth란 &lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring&amp;nbsp;Cloud의&amp;nbsp;분산&amp;nbsp;트레이싱&amp;nbsp;솔루션으로&amp;nbsp;개발&amp;nbsp;오픈소스&lt;/blockquote&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;Trace&amp;nbsp;And&amp;nbsp;Span&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Trace&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;전체 작업을 하나로 묶는 것&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;작업 당&lt;span&gt; 64-bit ID &lt;/span&gt;하나를 가진다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Span&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;분산 시스템에서 세부 작업&lt;span&gt;, &lt;/span&gt;다른 서비스&lt;span&gt;, &lt;/span&gt;다른 머신 또는 다른 쓰레드 등으로 나누어 질 수 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;각각&lt;span&gt; 64-bit ID&lt;/span&gt;를 가진다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;최초의&lt;span&gt; Span&lt;/span&gt;은&lt;span&gt; traceId&lt;/span&gt;와 동일하며&lt;span&gt;, root span&lt;/span&gt;으로 불린다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/71vgh/btstyhGwbJS/cgMD4HSDcU2FiMENlOm2q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/71vgh/btstyhGwbJS/cgMD4HSDcU2FiMENlOm2q0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/71vgh/btstyhGwbJS/cgMD4HSDcU2FiMENlOm2q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F71vgh%2FbtstyhGwbJS%2FcgMD4HSDcU2FiMENlOm2q0%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;890&quot; height=&quot;476&quot; data-origin-width=&quot;890&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;분산 시스템의 한 작업의 예이다.&lt;br /&gt;전체 작업을 하나의 Trace로 묶고, 세부 작업을 Span으로 표현한다.&lt;br /&gt;이를 통해 전체 프로세스를 한번에 tracing 할 수 있으며, 세부적인 작업에 대한 부분도 확인 가능하다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u3end/btstK0X6v1V/3JbTUBVZXqd35Oz8meElCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u3end/btstK0X6v1V/3JbTUBVZXqd35Oz8meElCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u3end/btstK0X6v1V/3JbTUBVZXqd35Oz8meElCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu3end%2FbtstK0X6v1V%2F3JbTUBVZXqd35Oz8meElCK%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;666&quot; height=&quot;212&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;전체 작업을 Span을 기준으로 tree 구조화 할 수 있다.&lt;br /&gt;이는 span을 기준으로 flow를 추적하기 좋은 구조이다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Log Sample&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1694421212182&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;service1.log:2016-02-26 11:15:47.561  INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application   : Hello from service1. Calling service2
service2.log:2016-02-26 11:15:47.710  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Hello from service2. Calling service3 and then service4
service3.log:2016-02-26 11:15:47.895  INFO [service3,2485ec27856c56f4,1210be13194bfe5,true] 68060 --- [nio-8083-exec-1] i.s.c.sleuth.docs.service3.Application   : Hello from service3
service2.log:2016-02-26 11:15:47.924  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Got response from service3 [Hello from service3]
service4.log:2016-02-26 11:15:48.134  INFO [service4,2485ec27856c56f4,1b1845262ffba49d,true] 68061 --- [nio-8084-exec-1] i.s.c.sleuth.docs.service4.Application   : Hello from service4
service2.log:2016-02-26 11:15:48.156  INFO [service2,2485ec27856c56f4,9aa10ee6fbde75fa,true] 68059 --- [nio-8082-exec-1] i.s.c.sleuth.docs.service2.Application   : Got response from service4 [Hello from service4]
service1.log:2016-02-26 11:15:48.182  INFO [service1,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] i.s.c.sleuth.docs.service1.Application   : Got response from service2 [Hello from service2, response from service3 [Hello from service3] and from service4 [Hello from service4]]&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Trace&amp;nbsp;Visualize&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ZipKin&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPh8Kr/btstHdclw5v/UoNPcH9qP6gb62jYDRwyl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPh8Kr/btstHdclw5v/UoNPcH9qP6gb62jYDRwyl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPh8Kr/btstHdclw5v/UoNPcH9qP6gb62jYDRwyl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPh8Kr%2FbtstHdclw5v%2FUoNPcH9qP6gb62jYDRwyl1%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;1700&quot; height=&quot;399&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;각 서비스에서 소요된 시간 및 호출정보를 한번에 파악할 수 있다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;1275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/D0ECX/btstCXAKSxG/FNenEM3uEZxSKYggULYtJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/D0ECX/btstCXAKSxG/FNenEM3uEZxSKYggULYtJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/D0ECX/btstCXAKSxG/FNenEM3uEZxSKYggULYtJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FD0ECX%2FbtstCXAKSxG%2FFNenEM3uEZxSKYggULYtJ0%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;1700&quot; height=&quot;1275&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;1275&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;427&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkwr47/btstHcxJd3Z/hjRMGXYUFZKfxSfVwJSI11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkwr47/btstHcxJd3Z/hjRMGXYUFZKfxSfVwJSI11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkwr47/btstHcxJd3Z/hjRMGXYUFZKfxSfVwJSI11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbkwr47%2FbtstHcxJd3Z%2FhjRMGXYUFZKfxSfVwJSI11%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;1700&quot; height=&quot;427&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;427&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;에러에 대한 부분도 한번에 파악할 수 있다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Kibana&amp;nbsp;with&amp;nbsp;logstash&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;750&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n1Olq/btstCGlANge/uwoUd46t7jzFIhl4hS5kMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n1Olq/btstCGlANge/uwoUd46t7jzFIhl4hS5kMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n1Olq/btstCGlANge/uwoUd46t7jzFIhl4hS5kMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn1Olq%2FbtstCGlANge%2FuwoUd46t7jzFIhl4hS5kMk%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;1700&quot; height=&quot;750&quot; data-origin-width=&quot;1700&quot; data-origin-height=&quot;750&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Logstash를 통해 collect된 정보를 기반으로 kibana연동&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동작 원리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Cloud Sleuth는 2.0.0버전부터 Brave 라이브러리를 도입했다.&lt;br /&gt;따라서 Tracing 및 ZipKin 통합 관련 동작은 Brave가 담당한다. &lt;br /&gt;Sleuth에서 Brave 라이브러리를 활용하여, MDC에 특정 key로 trace정보를 propagation한다. &lt;br /&gt;그 외 각 instrument 라이브러리에서 brave를 통해 관리되는 trace정보를 propagation할 tracing 객체들을 구현해 놓았다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 동작 (Brave)&lt;/h3&gt;
&lt;pre id=&quot;code_1694421378574&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Tracer(brave) 생성
new Tracer(...)

// Span 생성
tracer.newTrace().name(&quot;xxx&quot;).start(); // 인스턴스 span
tracer.startScopedSpan(&quot;xxx&quot;); // Threadlocal 기반 span

// 다음 span 생성
tracer.nextSpan();

// propagation (to other system)
tracing.propagation()
  .injector(Request::addHeader)
  .inject(span.context(), request);

// span 종료
span.finish();;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Tracer (Sleuth)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;353&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMOJrV/btstHcR37jn/MWLw0zyVRMvOJrVzwX2sW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMOJrV/btstHcR37jn/MWLw0zyVRMvOJrVzwX2sW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMOJrV/btstHcR37jn/MWLw0zyVRMvOJrVzwX2sW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMOJrV%2FbtstHcR37jn%2FMWLw0zyVRMvOJrVzwX2sW0%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;353&quot; height=&quot;230&quot; data-origin-width=&quot;353&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Cloud Sleuth에서 Tracing을 위해 사용하는 핵심 Interface&lt;br /&gt;Bean으로 만들어 다른 component들과 Integration한다.&lt;br /&gt;Sleuth에서 구현체로 BraveTracer를 기본적으로 사용하며, bridge 패턴으로 구성되어 있다.&lt;br /&gt;(내부적으로 brave 라이브러리의 Tracer를 가지고 Sleuth의 기능을 구현했다.)&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Tracer (Brave)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;505&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cE19mO/btstxieR2PZ/tC8bWkbE6hyTn0UCfzjkUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cE19mO/btstxieR2PZ/tC8bWkbE6hyTn0UCfzjkUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cE19mO/btstxieR2PZ/tC8bWkbE6hyTn0UCfzjkUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcE19mO%2FbtstxieR2PZ%2FtC8bWkbE6hyTn0UCfzjkUk%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;505&quot; height=&quot;582&quot; data-origin-width=&quot;505&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Brave에서 구현한 Tracer의 실제 동작을 담당하는 클래스&lt;br /&gt;newSpan, nextSpan, newTraceContext, newChild 등 Tracing에 필요한 객체를 생성하고 관리할 수 있다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Propagation&amp;nbsp;(brave)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxPrvm/btstxPKhrNS/sOuzwm0dORf8ankFuiizR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxPrvm/btstxPKhrNS/sOuzwm0dORf8ankFuiizR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxPrvm/btstxPKhrNS/sOuzwm0dORf8ankFuiizR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxPrvm%2FbtstxPKhrNS%2FsOuzwm0dORf8ankFuiizR0%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;311&quot; height=&quot;198&quot; data-origin-width=&quot;311&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;brave에서 정의한 trace propagation을 위한 interface&lt;br /&gt;일반적으로 Span을 다른 Object에 넘기기 위한 Injector와 다른 Object에서 Span을 추출하기 위한 Extractor interface와 함께 구현된다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1nWrX/btstCXgs0G3/HD2zMcrJfIhc5Mt6SMUoWk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1nWrX/btstCXgs0G3/HD2zMcrJfIhc5Mt6SMUoWk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1nWrX/btstCXgs0G3/HD2zMcrJfIhc5Mt6SMUoWk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1nWrX%2FbtstCXgs0G3%2FHD2zMcrJfIhc5Mt6SMUoWk%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;868&quot; height=&quot;252&quot; data-origin-width=&quot;868&quot; data-origin-height=&quot;252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Propagation의 구현체들&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;// B3 Propagation&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;분산 시스템 환경에서, 시스템 간 Tracing을 위한 스펙이 별도로 필요하다.&lt;br /&gt;B3 Propagation은 Tracing Propagation의 스펙 중 하나이며, 많은 Tracing system과 호환이 된다.&lt;br /&gt;주로 header에 키로 &amp;ldquo;X-B3-&amp;rdquo; 로 시작한다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;343&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cP6t34/btstHZSx8Zm/l4bXQH9ZknBArLJgYK9dkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cP6t34/btstHZSx8Zm/l4bXQH9ZknBArLJgYK9dkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cP6t34/btstHZSx8Zm/l4bXQH9ZknBArLJgYK9dkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcP6t34%2FbtstHZSx8Zm%2Fl4bXQH9ZknBArLJgYK9dkK%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;845&quot; height=&quot;343&quot; data-origin-width=&quot;845&quot; data-origin-height=&quot;343&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.5814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0464%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Mandatory(O / X)&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.2559%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;traceId&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0464%;&quot;&gt;&lt;span&gt;&lt;span&gt;O&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.2559%;&quot;&gt;&lt;span&gt;&lt;span&gt;64 &lt;/span&gt;또는&lt;span&gt; 128 bit&lt;/span&gt;의&lt;span&gt; traceId&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;spanId&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0464%;&quot;&gt;&lt;span&gt;&lt;span&gt;O&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.2559%;&quot;&gt;&lt;span&gt;&lt;span&gt;64 bit&lt;/span&gt;의&lt;span&gt; spanId&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;parentSpanId&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0464%;&quot;&gt;&lt;span&gt;&lt;span&gt;X&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.2559%;&quot;&gt;&lt;span&gt;현재&lt;span&gt; spanId&lt;/span&gt;의&lt;span&gt; parent&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;일반적으로&lt;span&gt; parent&lt;/span&gt;가 없는 경우&lt;span&gt; (traceId == spanId) &lt;/span&gt;생략한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;SamplelingState&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0464%;&quot;&gt;&lt;span&gt;&lt;span&gt;X&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 73.2559%;&quot;&gt;&lt;span&gt;현재 수신한&lt;span&gt; propagation&lt;/span&gt;을&lt;span&gt; Tracing System (ex. zipkin)&lt;/span&gt;에게 전파 할 지에 대한&lt;span&gt; Flag &lt;/span&gt;값이다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Defer: &lt;/span&gt;수신 시스템이 알아서 판단&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Deny: &lt;/span&gt;전파&lt;span&gt; X&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Accept: &lt;/span&gt;가능하면 전파&lt;span&gt; O&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;Debug: Debug&lt;/span&gt;모드로 전파&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Propagation&amp;nbsp;방식&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Multiple&amp;nbsp;Headers&lt;/h4&gt;
&lt;pre id=&quot;code_1694421598092&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// http header
x-b3-traceid:&quot;88666bfe25af4d12&quot;, 
x-b3-spanid:&quot;67ba768f6754994f&quot;,
x-b3-parentspanid:&quot;88666bfe25af4d12&quot;, 
x-b3-sampled:&quot;0&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Http header 같이 여러 값을 헤더에 넣기 좋은 경우 사용&lt;br /&gt;위 attributes 중 해당되는 값을 넣는다.&lt;br /&gt;// 위 예제의 smapled 0는 deny를 의미한다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Single&amp;nbsp;Header&lt;/h4&gt;
&lt;pre id=&quot;code_1694421622277&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// kafka header
b3: 3bce497d9c6833a4-e4efb90513184e29-0&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;b3={TraceId}-{SpanId}-{SamplingState}-{ParentSpanId}&lt;br /&gt;형식으로 전달한다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ParentSpanId를&amp;nbsp;전파하는&amp;nbsp;이유&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Tracing System에서 Tree 구조를 만들기 위해서 사용한다.&lt;br /&gt;Tracing System으로 전파된 정보의 위치를 결정하기 위함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기술 검증&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. Thread Switching에 따른 Tracing 동작 확인&lt;br /&gt;2. Rest 서버 간 Tracing 동작 확인 (WebClient)&lt;br /&gt;3. Kafka Publish, Subscribe의 동작 확인&lt;br /&gt;4. Rsocket Server / client 동작 확인&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;환경&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;JDK 11&lt;br /&gt;Spring boot 2.7.0&lt;br /&gt;Sring cloud Sleuth 3.1.9&lt;br /&gt;Spring webflux&lt;br /&gt;Spring data r2dbc&lt;br /&gt;reactor-kafka 1.2.7.RELEASE&lt;/blockquote&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;1.&amp;nbsp;Webflux&amp;nbsp;(Reactor&amp;nbsp;Netty)&amp;nbsp;Tracing&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본 Tracing 동작 확인&lt;br /&gt;Reactor의 Thread 변경에 따른 Tracing 확인&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;관련 코드&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;관련 라이브러리를 import만 하면 Tracing 동작을 한다.&lt;br /&gt;MDC를 통해 logback으로 연결할 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1694421749225&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
implementation 'org.springframework.cloud:spring-cloud-starter-sleuth:3.1.9'

implementation 'org.springframework.boot:spring-boot-starter-webflux'
// Netty Reactor내 Tracing을 위한 라이브러리
implementation 'io.projectreactor.netty:reactor-netty-http-brave:1.1.9'
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;RestRouter&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694421763026&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
@Bean
public RouterFunction&amp;lt;ServerResponse&amp;gt; traceRouterFunc() {
  return route(GET(&quot;/trace/service&quot;), traceHandler::traceService)
      .andRoute(GET(&quot;/trace/r2dbc&quot;), traceHandler::traceR2dbc)
      .andRoute(GET(&quot;/trace/webclient&quot;), traceHandler::traceWebClient)
      .andRoute(GET(&quot;/trace/produce&quot;), traceHandler::traceKafkaProduce)
      .andRoute(GET(&quot;/trace/rsocket&quot;), traceHandler::traceRsocket);
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;TraceService&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694421775354&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public Mono&amp;lt;ServerResponse&amp;gt; traceR2dbc(final ServerRequest serverRequest) {
  log.info(&quot;traceService() start&quot;);
  return this.r2dbcService.r2dbcSelectCall()
      .flatMap(sampleData -&amp;gt; ServerResponse.ok().bodyValue(sampleData))
      .doOnNext(serverResponse -&amp;gt; log.info(&quot;traceService() end&quot;));
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;R2dbcService&lt;/p&gt;
&lt;pre id=&quot;code_1694421794785&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
public Mono&amp;lt;SampleData&amp;gt; r2dbcSelectCall() {
  return this.sampleRepo.findById(8)
      .doOnNext(sampleData -&amp;gt; log.info(&quot;r2dbcSelectCall()&quot;));
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시나리오&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. Http Request&lt;br /&gt;2. R2DBC MariaDB Query&lt;br /&gt;3. Http Response&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log&lt;/p&gt;
&lt;pre id=&quot;code_1694421862841&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2023-09-04 16:57:45.216  INFO [sleuthTest,20dc9c663477eddc,20dc9c663477eddc] 24344 --- [ctor-http-nio-4] c.e.l.sample.webflux.TraceHandler        : traceService() start
2023-09-04 16:57:45.440  INFO [sleuthTest,20dc9c663477eddc,20dc9c663477eddc] 24344 --- [actor-tcp-nio-2] c.e.l.sample.r2dbc.R2dbcService          : r2dbcSelectCall()
2023-09-04 16:57:45.444  INFO [sleuthTest,20dc9c663477eddc,20dc9c663477eddc] 24344 --- [actor-tcp-nio-2] c.e.l.sample.webflux.TraceHandler        : traceService() end&lt;/code&gt;&lt;/pre&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;2.&amp;nbsp;Rest&amp;nbsp;Servers&amp;nbsp;Tracing&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Http 서버 간 Tracing 동작 확인&lt;br /&gt;WebClient TraceId, SpanId 등 Header injection 동작 확인&lt;br /&gt;Create SpanId 동작 확인&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;관련 코드&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;주의 사항 !&lt;br /&gt;&amp;nbsp;- WebClient는 Bean으로 관리되고 있어야 Header Injection이 동작&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;WebClientConfig&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694422108771&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public WebClient webClient() {
  return WebClient.builder()
      .build();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;WebClientService&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694422114587&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;HashMap&amp;gt; traceWebClient() {
  return webClient
      .get()
      .uri(&quot;http://xxxx&quot;)
      .retrieve()
      .bodyToMono(HashMap.class)
      .doOnNext(hashMap -&amp;gt; log.info(&quot;qweasd&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버2&lt;/p&gt;
&lt;pre id=&quot;code_1694422122415&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
@GetMapping(&quot;/test&quot;)
public Mono&amp;lt;Map&amp;lt;String, String&amp;gt;&amp;gt; cccc(@RequestHeader Map&amp;lt;String, String&amp;gt; headers) {
  log.info(&quot;GET /test start&quot;);
  headers
      .forEach((key, value) -&amp;gt; log.info(key + &quot; : &quot; + value));
  return Mono.just(Map.of(&quot;test&quot;, &quot;success&quot;));
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시나리오&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 사용자 &amp;rarr; 서버1 REST 요청&lt;br /&gt;2. 서버1 &amp;rarr; 서버2 REST 요청 (WebClient)&lt;br /&gt;3. 서버2 &amp;rarr; 서버1 응답&lt;br /&gt;4. 서버1 &amp;rarr; 사용자 응답&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log&lt;/p&gt;
&lt;pre id=&quot;code_1694422157874&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 서버1 (before Request)
2023-09-04 17:20:04.372  INFO [sleuthTest,f988661d4e75a625,f988661d4e75a625] 17692 --- [ctor-http-nio-4] c.e.l.sample.webflux.TraceHandler        : traceService() start

// 서버2
2023-09-04 17:20:04.494  INFO [,f988661d4e75a625,e72c2850246440e5] 18248 --- [ctor-http-nio-3] c.e.r.TestRestController                 : GET /test start
2023-09-04 17:20:04.495  INFO [,f988661d4e75a625,e72c2850246440e5] 18248 --- [ctor-http-nio-3] c.e.r.TestRestController                 : accept-encoding : gzip
2023-09-04 17:20:04.495  INFO [,f988661d4e75a625,e72c2850246440e5] 18248 --- [ctor-http-nio-3] c.e.r.TestRestController                 : user-agent : ReactorNetty/1.0.19
2023-09-04 17:20:04.495  INFO [,f988661d4e75a625,e72c2850246440e5] 18248 --- [ctor-http-nio-3] c.e.r.TestRestController                 : host : localhost:8080
2023-09-04 17:20:04.495  INFO [,f988661d4e75a625,e72c2850246440e5] 18248 --- [ctor-http-nio-3] c.e.r.TestRestController                 : accept : */*
2023-09-04 17:20:04.495  INFO [,f988661d4e75a625,e72c2850246440e5] 18248 --- [ctor-http-nio-3] c.e.r.TestRestController                 : X-B3-TraceId : f988661d4e75a625
2023-09-04 17:20:04.495  INFO [,f988661d4e75a625,e72c2850246440e5] 18248 --- [ctor-http-nio-3] c.e.r.TestRestController                 : X-B3-SpanId : e72c2850246440e5
2023-09-04 17:20:04.495  INFO [,f988661d4e75a625,e72c2850246440e5] 18248 --- [ctor-http-nio-3] c.e.r.TestRestController                 : X-B3-ParentSpanId : f988661d4e75a625
2023-09-04 17:20:04.495  INFO [,f988661d4e75a625,e72c2850246440e5] 18248 --- [ctor-http-nio-3] c.e.r.TestRestController                 : X-B3-Sampled : 0

// 서버1 (after Request)
2023-09-04 17:20:04.552  INFO [sleuthTest,f988661d4e75a625,f988661d4e75a625] 17692 --- [ctor-http-nio-4] c.e.l.sample.webClient.WebClientService  : qweasd
2023-09-04 17:20:04.554  INFO [sleuthTest,f988661d4e75a625,f988661d4e75a625] 17692 --- [ctor-http-nio-4] c.e.l.sample.webflux.TraceHandler        : traceService() end&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;3.-Kafka-Pub-/-Sub-Tracing&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;13001&quot; data-ke-size=&quot;size23&quot;&gt;3. Kafka Pub / Sub Tracing&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka Producer의 header injection 확인&lt;br /&gt;Kafka Consumer의 message header로 부터 traceId 연계 확인&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;관련 코드&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Reactive환경이기에 reactor kafka를 대상으로 테스트&lt;br /&gt;ProducerFactory를 extend한 TracingKafkaProducerFactory를 제공한다.&lt;br /&gt;KafkaReceiver를 implements한 TracingKafkaReceiver를 제공한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1694422221323&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
implementation 'io.projectreactor.kafka:reactor-kafka:1.2.7.RELEASE'
implementation 'org.springframework.kafka:spring-kafka'
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;KafkaConfig&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694422227279&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public KafkaSender&amp;lt;String, String&amp;gt; kafkaSender(final BeanFactory beanFactory) {
  Map&amp;lt;String, Object&amp;gt; senderProps = new HashMap&amp;lt;&amp;gt;();
  senderProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;xxxx&quot;);
  senderProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
  senderProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

  SenderOptions&amp;lt;String, String&amp;gt; senderOptions = SenderOptions.create(senderProps);
  return KafkaSender.create(new TracingKafkaProducerFactory(beanFactory), senderOptions);
}

@Bean
public KafkaReceiver&amp;lt;String, String&amp;gt; kafkaReceiver(ReactiveKafkaTracingPropagator reactiveKafkaTracingPropagator) {
  Map&amp;lt;String, Object&amp;gt; consumerConfig = new HashMap&amp;lt;&amp;gt;();
  consumerConfig.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;xxxx&quot;);
  consumerConfig.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;test-1&quot;);
  consumerConfig.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
  consumerConfig.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
  consumerConfig.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, OffsetResetStrategy.LATEST.name().toLowerCase());
  consumerConfig.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 10);

  ReceiverOptions&amp;lt;String, String&amp;gt; receiverOptions = ReceiverOptions.create(consumerConfig);

  return TracingKafkaReceiver
      .create(reactiveKafkaTracingPropagator, receiverOptions
          .atmostOnceCommitAheadSize(20)
          .subscription(Pattern.compile(&quot;xxxx&quot;)));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;Producer&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694422233636&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Flux&amp;lt;Integer&amp;gt; sendMessage(final int count) {
  return this.kafkaSender
      .send(Flux
          .range(1, count)
          .map(i -&amp;gt; SenderRecord.create(&quot;chchoi.test.1&quot;, null, new Date().getTime(), null, i + &quot;-&quot; + UUID.randomUUID(), i)))
      .doOnError(e -&amp;gt; log.error(&quot;send error&quot;, e))
      .doOnNext(response -&amp;gt; log.info(&quot;response: cm: {}, rm: {}&quot;, response.correlationMetadata(), response.recordMetadata()))
      .map(SenderResult::correlationMetadata);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;Consumer&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694422239577&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
public void onApplicationEvent(final ApplicationReadyEvent event) {
  this.kafkaReceiver
      .receiveAutoAck()
      .concatMap(r -&amp;gt; r)
      .doOnNext(stringStringConsumerRecord -&amp;gt; {
        log.info(&quot;next....&quot;);
      })
      .onErrorContinue((e, o) -&amp;gt; log.error(&quot;error on consume&quot;, e))
      .subscribe(record -&amp;gt; log.info(&quot;topic: {}, key: {}, value: {}&quot;, record.topic(), record.key(), record.value()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시나리오&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. REST 요청&lt;br /&gt;2. Topic Messsage Produce&lt;br /&gt;3. Consume Topic Message&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log&lt;/p&gt;
&lt;pre id=&quot;code_1694422286408&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// REST 요청 받음
2023-09-04 17:33:53.615  INFO [sleuthTest,3bce497d9c6833a4,3bce497d9c6833a4] 7340 --- [ctor-http-nio-4] c.e.l.sample.webflux.TraceHandler        : traceService() start

// init Producer
2023-09-04 17:33:53.626  INFO [sleuthTest,3bce497d9c6833a4,3bce497d9c6833a4] 7340 --- [ctor-http-nio-4] o.a.k.clients.producer.ProducerConfig    : ProducerConfig values: 
	acks = -1
	batch.size = 16384
	...
2023-09-04 17:33:53.632  INFO [sleuthTest,3bce497d9c6833a4,3bce497d9c6833a4] 7340 --- [ctor-http-nio-4] o.a.k.clients.producer.KafkaProducer     : [Producer clientId=producer-1] Instantiated an idempotent producer.
2023-09-04 17:33:53.643  INFO [sleuthTest,3bce497d9c6833a4,3bce497d9c6833a4] 7340 --- [ctor-http-nio-4] o.a.kafka.common.utils.AppInfoParser     : Kafka version: 3.1.1

// produce start
2023-09-04 17:33:53.644  INFO [sleuthTest,3bce497d9c6833a4,3bce497d9c6833a4] 7340 --- [ctor-http-nio-4] o.a.kafka.common.utils.AppInfoParser     : Kafka commitId: 97671528ba54a138
2023-09-04 17:33:53.644  INFO [sleuthTest,3bce497d9c6833a4,3bce497d9c6833a4] 7340 --- [ctor-http-nio-4] o.a.kafka.common.utils.AppInfoParser     : Kafka startTimeMs: 1693816433643
...
2023-09-04 17:33:53.687  INFO [sleuthTest,3bce497d9c6833a4,3bce497d9c6833a4] 7340 --- [       single-1] c.e.l.sample.kafka.KafkaService          : response: cm: 1, rm: chchoi.test.1-0@53
2023-09-04 17:33:53.687  INFO [sleuthTest,3bce497d9c6833a4,3bce497d9c6833a4] 7340 --- [       single-1] c.e.l.sample.kafka.KafkaService          : response: cm: 2, rm: chchoi.test.1-0@54
2023-09-04 17:33:53.687  INFO [sleuthTest,3bce497d9c6833a4,3bce497d9c6833a4] 7340 --- [       single-1] c.e.l.sample.kafka.KafkaService          : response: cm: 3, rm: chchoi.test.1-0@55
// produce end

// consume start
2023-09-04 17:33:53.746  INFO [sleuthTest,3bce497d9c6833a4,2f0175bf3ca03167] 7340 --- [     parallel-3] c.e.l.sample.kafka.ReadyListener         : next....
2023-09-04 17:33:53.746  INFO [sleuthTest,3bce497d9c6833a4,2f0175bf3ca03167] 7340 --- [     parallel-3] c.e.l.sample.kafka.ReadyListener         : topic: chchoi.test.1, key: null, value: 1-9ccf744f-052e-497b-bf92-556d8a501768
2023-09-04 17:33:53.746  INFO [sleuthTest,3bce497d9c6833a4,7435a497d71b90ab] 7340 --- [     parallel-3] c.e.l.sample.kafka.ReadyListener         : next....
2023-09-04 17:33:53.746  INFO [sleuthTest,3bce497d9c6833a4,7435a497d71b90ab] 7340 --- [     parallel-3] c.e.l.sample.kafka.ReadyListener         : topic: chchoi.test.1, key: null, value: 2-6c5ecc32-12d9-416b-8df3-79a6a892e411
2023-09-04 17:33:53.747  INFO [sleuthTest,3bce497d9c6833a4,ca941de8f65d2d36] 7340 --- [     parallel-3] c.e.l.sample.kafka.ReadyListener         : next....
2023-09-04 17:33:53.747  INFO [sleuthTest,3bce497d9c6833a4,ca941de8f65d2d36] 7340 --- [     parallel-3] c.e.l.sample.kafka.ReadyListener         : topic: chchoi.test.1, key: null, value: 3-72b7d2fc-5baf-4897-a65c-4748465706da
// consume end

// rest end
2023-09-04 17:33:53.747  INFO [sleuthTest,3bce497d9c6833a4,3bce497d9c6833a4] 7340 --- [       single-1] c.e.l.sample.webflux.TraceHandler        : traceService() end&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;821&quot; data-origin-height=&quot;175&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTGl3q/btstM2gF9Jc/rUbFWgs0vmWzKFKIz5KZGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTGl3q/btstM2gF9Jc/rUbFWgs0vmWzKFKIz5KZGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTGl3q/btstM2gF9Jc/rUbFWgs0vmWzKFKIz5KZGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTGl3q%2FbtstM2gF9Jc%2FrUbFWgs0vmWzKFKIz5KZGK%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;821&quot; height=&quot;175&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;175&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;h2 id=&quot;4.-RSocket-Server-/-Client-Tracing&quot; style=&quot;color: #000000;&quot; data-renderer-start-pos=&quot;19166&quot; data-ke-size=&quot;size26&quot;&gt;4. RSocket Server / Client Tracing&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;RSocket Server의 요청 받을 때 Tracing 동작 확인&lt;br /&gt;RSocket Client의 요청할 때 traceId관련 injection 확인&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;관련 코드&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;RSocket 서버는 별도의 설정 없이 import 및 enable 만으로 Tracing 관련 로직이 동작한다.&lt;br /&gt;client는 RSocketConnectorConfigurer의 구현체인 TracingRSocketConnectorConfigurer를 설정해줘야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle&lt;/p&gt;
&lt;div style=&quot;background-color: #000000;&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1694422364921&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
implementation 'org.springframework.boot:spring-boot-starter-rsocket'
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;RSocketServer&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694422370100&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@MessageMapping(&quot;/test&quot;)
@NewSpan(&quot;testSSS&quot;)
public Mono&amp;lt;A&amp;gt; traceService() {
  return Mono.just(new A(&quot;testResponseData&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;RSocketClientConfig&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694422382878&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
public RSocketRequester rSocketRequester(Propagator propagator, Tracer tracer) {
  return RSocketRequester.builder()
      .rsocketStrategies(RSocketStrategies.builder()
          .decoder(new Jackson2JsonDecoder())
          .build())
      .rsocketConnector(new TracingRSocketConnectorConfigurer(propagator, tracer, true))
      .dataMimeType(MimeType.valueOf(&quot;application/json&quot;))
      .transport(TcpClientTransport.create(&quot;localhost&quot;, 8000));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;RSocketClient&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1694422389196&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;A&amp;gt; traceRSocket() {
  log.info(&quot;traceRSocket() start&quot;);
  return this.rSocketRequester
      .route(&quot;/test&quot;)
      .data(&quot;qweasd&quot;)
      .retrieveMono(A.class)
      .doOnNext(a -&amp;gt; log.info(&quot;receiveFromRSocket: {}&quot;, a.getA()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시나리오&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. REST 요청&lt;br /&gt;2. 서버1 &amp;rarr; 서버2 RSocket 요청&lt;br /&gt;3. RSocket 응답&lt;br /&gt;4. REST 응답&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;log&lt;/p&gt;
&lt;pre id=&quot;code_1694422432880&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 서버1
2023-09-04 17:45:46.148  INFO [sleuthTest,95ab4dfd5ea0968b,95ab4dfd5ea0968b] 7340 --- [ctor-http-nio-4] c.e.l.sample.webflux.TraceHandler        : traceService() start
2023-09-04 17:45:46.154  INFO [sleuthTest,95ab4dfd5ea0968b,95ab4dfd5ea0968b] 7340 --- [ctor-http-nio-4] c.e.l.sample.rsocket.RSocketService      : traceRSocket() start
2023-09-04 17:45:46.253 DEBUG [sleuthTest,95ab4dfd5ea0968b,95ab4dfd5ea0968b] 7340 --- [actor-tcp-nio-3] io.rsocket.FrameLogger                   : sending -&amp;gt; 
Frame =&amp;gt; Stream ID: 0 Type: SETUP Flags: 0b0 Length: 75
Data:

2023-09-04 17:45:46.280 DEBUG [sleuthTest,95ab4dfd5ea0968b,95ab4dfd5ea0968b] 7340 --- [actor-tcp-nio-3] io.rsocket.FrameLogger                   : sending -&amp;gt; 
Frame =&amp;gt; Stream ID: 1 Type: REQUEST_RESPONSE Flags: 0b100000000 Length: 95
Metadata:
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| fe 00 00 06 05 2f 74 65 73 74 fd 00 00 19 94 95 |...../test......|
|00000010| ab 4d fd 5e a0 96 8b 1f 0e e7 1a 2e 31 9c ce 95 |.M.^........1...|
|00000020| ab 4d fd 5e a0 96 8b 01 62 33 00 00 23 39 35 61 |.M.^....b3..#95a|
|00000030| 62 34 64 66 64 35 65 61 30 39 36 38 62 2d 31 66 |b4dfd5ea0968b-1f|
|00000040| 30 65 65 37 31 61 32 65 33 31 39 63 63 65 2d 30 |0ee71a2e319cce-0|
+--------+-------------------------------------------------+----------------+
Data:
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 71 77 65 61 73 64                               |qweasd          |
+--------+-------------------------------------------------+----------------+
2023-09-04 17:45:46.396 DEBUG [sleuthTest,95ab4dfd5ea0968b,95ab4dfd5ea0968b] 7340 --- [actor-tcp-nio-3] io.rsocket.FrameLogger                   : receiving -&amp;gt; 
Frame =&amp;gt; Stream ID: 1 Type: NEXT_COMPLETE Flags: 0b1100000 Length: 30
Data:
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 7b 22 61 22 3a 22 74 65 73 74 52 65 73 70 6f 6e |{&quot;a&quot;:&quot;testRespon|
|00000010| 73 65 44 61 74 61 22 7d                         |seData&quot;}        |
+--------+-------------------------------------------------+----------------+
2023-09-04 17:45:46.404  INFO [sleuthTest,95ab4dfd5ea0968b,95ab4dfd5ea0968b] 7340 --- [actor-tcp-nio-3] c.e.l.sample.rsocket.RSocketService      : receiveFromRSocket: testResponseData
2023-09-04 17:45:46.404  INFO [sleuthTest,95ab4dfd5ea0968b,95ab4dfd5ea0968b] 7340 --- [actor-tcp-nio-3] c.e.l.sample.webflux.TraceHandler        : traceService() end



// 서버2
2023-09-04 17:45:46.288 DEBUG [,,] 18248 --- [ctor-http-nio-4] io.rsocket.FrameLogger                   : receiving -&amp;gt; 
Frame =&amp;gt; Stream ID: 0 Type: SETUP Flags: 0b0 Length: 75
Data:

2023-09-04 17:45:46.315 DEBUG [,,] 18248 --- [ctor-http-nio-4] io.rsocket.FrameLogger                   : receiving -&amp;gt; 
Frame =&amp;gt; Stream ID: 1 Type: REQUEST_RESPONSE Flags: 0b100000000 Length: 95
Metadata:
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| fe 00 00 06 05 2f 74 65 73 74 fd 00 00 19 94 95 |...../test......|
|00000010| ab 4d fd 5e a0 96 8b 1f 0e e7 1a 2e 31 9c ce 95 |.M.^........1...|
|00000020| ab 4d fd 5e a0 96 8b 01 62 33 00 00 23 39 35 61 |.M.^....b3..#95a|
|00000030| 62 34 64 66 64 35 65 61 30 39 36 38 62 2d 31 66 |b4dfd5ea0968b-1f|
|00000040| 30 65 65 37 31 61 32 65 33 31 39 63 63 65 2d 30 |0ee71a2e319cce-0|
+--------+-------------------------------------------------+----------------+
Data:
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 71 77 65 61 73 64                               |qweasd          |
+--------+-------------------------------------------------+----------------+
2023-09-04 17:45:46.387 DEBUG [,95ab4dfd5ea0968b,acab5cb6dc4c8625] 18248 --- [ctor-http-nio-4] io.rsocket.FrameLogger                   : sending -&amp;gt; 
Frame =&amp;gt; Stream ID: 1 Type: NEXT_COMPLETE Flags: 0b1100000 Length: 30
Data:
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 7b 22 61 22 3a 22 74 65 73 74 52 65 73 70 6f 6e |{&quot;a&quot;:&quot;testRespon|
|00000010| 73 65 44 61 74 61 22 7d                         |seData&quot;}        |
+--------+-------------------------------------------------+----------------+&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;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;간편하게 tracing 관련 연계를 구성할 수 있다.&lt;br /&gt;설정이 조금 까다롭지만, 스펙도 명확하고, 역할도 명확해서 도입에 문제가 없을 것으로 판단된다.&lt;br /&gt;error 처리 및 tracing의 시각화는 더 조사해 보아야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docs &lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-sleuth/docs/2.2.9.BUILD-SNAPSHOT/reference/html/#introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-cloud-sleuth/docs/2.2.9.BUILD-SNAPSHOT/reference/html/#introduction&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/integrations.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/integrations.html&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-sleuth/docs/2.2.9.BUILD-SNAPSHOT/reference/html/appendix.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-cloud-sleuth/docs/2.2.9.BUILD-SNAPSHOT/reference/html/appendix.html&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;netty&amp;nbsp;example &lt;br /&gt;&lt;a href=&quot;https://github.com/btkelly/Sleuth-Netty-Example/blob/master/build.gradle&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/btkelly/Sleuth-Netty-Example/blob/master/build.gradle&lt;/a&gt;&lt;br /&gt;producer&amp;nbsp;example &lt;br /&gt;&lt;a href=&quot;https://github.com/spring-cloud-samples/spring-cloud-sleuth-samples/tree/main/kafka-reactive-producer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/spring-cloud-samples/spring-cloud-sleuth-samples/tree/main/kafka-reactive-producer&lt;/a&gt;&lt;br /&gt;consumer&amp;nbsp;example &lt;br /&gt;&lt;a href=&quot;https://github.com/spring-cloud-samples/spring-cloud-sleuth-samples/tree/main/kafka-reactive-consumer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/spring-cloud-samples/spring-cloud-sleuth-samples/tree/main/kafka-reactive-consumer&lt;/a&gt;&lt;br /&gt;rsocket &lt;br /&gt;&lt;a href=&quot;https://github.com/rsocket/rsocket/blob/master/Extensions/WellKnownMimeTypes.md&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/rsocket/rsocket/blob/master/Extensions/WellKnownMimeTypes.md&lt;/a&gt;&lt;br /&gt;org.springframework.cloud.sleuth.instrument.rsocket.TracingRSocketConnectorConfigurer &lt;br /&gt;&lt;a href=&quot;https://micrometer.io/docs/tracing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://micrometer.io/docs/tracing&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div style=&quot;background-color: #000000;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>java</category>
      <category>log tracing</category>
      <category>MSA</category>
      <category>spring</category>
      <category>spring cloud sleuth</category>
      <category>spring-cloud-sleuth</category>
      <category>trace</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/64</guid>
      <comments>https://p-bear.tistory.com/64#entry64comment</comments>
      <pubDate>Mon, 11 Sep 2023 17:57:21 +0900</pubDate>
    </item>
    <item>
      <title>#2 Session Connector Server 구현</title>
      <link>https://p-bear.tistory.com/63</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WhaxG/btspUnXo0yU/WhkAEwJHg6Xbzr7oTbSK40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WhaxG/btspUnXo0yU/WhkAEwJHg6Xbzr7oTbSK40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WhaxG/btspUnXo0yU/WhkAEwJHg6Xbzr7oTbSK40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWhaxG%2FbtspUnXo0yU%2FWhkAEwJHg6Xbzr7oTbSK40%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;286&quot; height=&quot;176&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;176&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;FlagWar 관련 개발을 한동안 바빠서 소흘했었네요...&lt;br /&gt;이제 슬슬 다시 재개하려 합니다.&lt;br /&gt;이번 구현한 사항은 client와 session을 유지하며, 메시지를 보낼 client를 찾아서 서버 push를 보낼 수 있는 서버입니다.&lt;br /&gt;EX) A, B가 X라는 reference로 서버와 session을 유지하고 있을 때, X로 향하는 메시지가 A, B 모두에게 전달될 수 있어야 함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요구사항 요약&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 서버는 Client와의 Session을 유지할 수 있어야 한다.&lt;br /&gt;2. 서버에 연결된 Client는 자신을 식별할 수 있는 TAG 정보를 서버에 저장 및 삭제할 수 있어야 한다.&lt;br /&gt;3. 서버는 메시지를 수신하여 TAG 기반으로 Client에게 메시지를 전달 할 수 있어야 한다.&lt;br /&gt;4. 서버는 Scale-out이 용이한 구조를 가진다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Infra Structure&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;782&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lwacf/btspPaksEIv/ylPc7t6kOd73CtLqxt5zPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lwacf/btspPaksEIv/ylPc7t6kOd73CtLqxt5zPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lwacf/btspPaksEIv/ylPc7t6kOd73CtLqxt5zPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flwacf%2FbtspPaksEIv%2FylPc7t6kOd73CtLqxt5zPK%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;496&quot; height=&quot;782&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;782&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본적으로 Server &amp;lt;-&amp;gt; Client는 Nginx를 통해 연결된다.&lt;br /&gt;세션 및 서버 PUSH를 위한 프로토콜은 Websocket을 사용한다.&lt;br /&gt;Client가 Message를 받을 TAG CRUD는 REST, Websocket Message를 사용한다.&lt;br /&gt;Server는 Kafka에서 Message 관련 Topic을 구독한다.&lt;br /&gt;Message 전파를 위해서, Tag를 포함한 Message를 해당 Topic에 Publish 한다.&lt;br /&gt;&lt;br /&gt;Kafka를 사용한 이유는 Scale-out에 용이한 구조를 가지기 위함이며, 모든 message 전달은 kafka를 통해서 전달하여 session server는 자신의 session에 해당되는 Tag를 기반으로 메시지를 전달한다.&lt;br /&gt;// kafka 관련 인프라가 이미 구축되어 있기에 다른 메시지 큐 도입은 안하기로 결정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zYZyR/btspRKeO79Q/QmdUgKsKsZ5M5KwETOsFqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zYZyR/btspRKeO79Q/QmdUgKsKsZ5M5KwETOsFqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zYZyR/btspRKeO79Q/QmdUgKsKsZ5M5KwETOsFqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzYZyR%2FbtspRKeO79Q%2FQmdUgKsKsZ5M5KwETOsFqK%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;1474&quot; height=&quot;728&quot; data-origin-width=&quot;1474&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;- Kotlin 기반&lt;br /&gt;- Spring Webflux&lt;br /&gt;- Spring Websocket&lt;br /&gt;- Reactor-Kafka&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;SessionManager: Session을 관리하는 클래스, websocket session을 wrapping한 객체를 가지고 있으며, 해당 객체는 websocket session 외 referenceTag 등을 추가로 가진다.&lt;br /&gt;WebsocketConifig는 StringWebsocketHandler를 등록하며, 해당 handler에서 세션처리 및 메시지 처리를 한다.&lt;br /&gt;SessionController는 REST 인터페이스를 제공&lt;br /&gt;KafkaConsumeManager는 특정 Topic을 Consume 하며, SessionManager를 통해 websocket session에 message를 전달한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Session 기능 (Websocket)&lt;/h3&gt;
&lt;pre id=&quot;code_1691045469461&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSocket
class WebsocketConfig(val stringWebsocketHandler: StringWebsocketHandler): WebSocketConfigurer {
    override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
        registry
            .addHandler(stringWebsocketHandler, &quot;/connect&quot;).setAllowedOrigins(&quot;*&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Websocket Enable 및 handler, resource 설정&lt;br /&gt;CORS를 위한 origin 설정&lt;/blockquote&gt;
&lt;pre id=&quot;code_1691045740533&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override fun afterConnectionEstablished(session: WebSocketSession) {
    val sessionWrapper = SessionWrapper(id = session.id, webSocketSession = session)
    session.uri?.rawQuery?.let { addReferenceTags(it, sessionWrapper) }
    this.sessionManager.addSession(sessionWrapper)
}

override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
    this.sessionManager.removeSession(session.id)
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Websocket connection 연결/끊기 관련 코드&lt;br /&gt;establish시 메모리 상 session을 추가&lt;br /&gt;close 시 session 제거&lt;/blockquote&gt;
&lt;pre id=&quot;code_1691045833327&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SessionWrapper(
    val id: String,
    var referenceTagSet: MutableSet&amp;lt;String&amp;gt; = HashSet(),
    val timestamp: Long = Date().time,
    @JsonIgnore
    val webSocketSession: WebSocketSession
)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;메모리상의 Session Wrapper 객체&lt;br /&gt;고유 id를 가지며, referenceTag를 Set으로 가지고 있는다.&lt;br /&gt;referenceTag는 중복없는 단순 String 값이다.&lt;/blockquote&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;Reference 관련 기능&lt;/h3&gt;
&lt;pre id=&quot;code_1691046294311&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun addReferenceTag(sessionId: String, referenceTag: String) =
        this.sessionMap[sessionId]?.referenceTagSet?.add(referenceTag)

fun removeReferenceTag(sessionId: String, referenceTag: String) =
    this.sessionMap[sessionId]?.referenceTagSet?.remove(referenceTag)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Reference를 추가/제거하는 메서드&lt;br /&gt;websocket message handle 및 REST Controller에서 호출된다.&lt;/blockquote&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;SendMessage 기능&lt;/h3&gt;
&lt;pre id=&quot;code_1691046416333&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun sendMessage(referenceTags: Set&amp;lt;String&amp;gt;, message: String) {
    val failList = this.getSessionsByReferenceTags(referenceTags)
        .map {
            log.info(&quot;send message, id: ${it.id}&quot;)
            try {
                it.webSocketSession.sendMessage(TextMessage(message))
                null
            } catch (e: Exception) {
                it
            }
        }
        .filterNotNull()
        .toList()
    failList.forEach { this.removeSession(it.id) }
}

fun getSessionsByReferenceTags(target: Set&amp;lt;String&amp;gt;): Set&amp;lt;SessionWrapper&amp;gt; {
    return this.sessionMap.entries
        .map { it.value }
        .filter { target.intersect(it.referenceTagSet).isNotEmpty() }
        .toSet()
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;message를 전달할 대상은 referenceTag를 기반으로 하며&lt;br /&gt;메모리상의 session들에게서 referenceTag가 매칭되는 객체를 찾아서, websocket session에 메시지를 보낸다.&lt;br /&gt;sendMessage에 실패하는 경우, 비활성화된 세션으로 간주하여, session에서 remove하고, close 처리를 한다.&lt;/blockquote&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;Consume Kafka Topic&lt;/h3&gt;
&lt;pre id=&quot;code_1691046594245&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostConstruct
fun init() {
    val consumerConfig = HashMap&amp;lt;String, Any&amp;gt;();
    consumerConfig[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = bootstrapServer
    consumerConfig[ConsumerConfig.GROUP_ID_CONFIG] = UUID.randomUUID().toString()
    consumerConfig[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java
    consumerConfig[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java
    consumerConfig[ConsumerConfig.AUTO_OFFSET_RESET_CONFIG] =
        OffsetResetStrategy.LATEST.name.lowercase(Locale.getDefault())
    consumerConfig[ConsumerConfig.MAX_POLL_RECORDS_CONFIG] = 10

    this.receiverOptions = ReceiverOptions.create(consumerConfig)
    this.connectorMessageReceiver = KafkaReceiver
        .create(
            this.receiverOptions
                .commitInterval(Duration.of(10, ChronoUnit.MILLIS))
                .commitBatchSize(1)
                .subscription(listOf(&quot;connector.message&quot;))
        )

    this.consumeConnectorMessage()
}

fun consumeConnectorMessage() {
    this.connectorMessageReceiver
        .receiveAutoAck()
        .concatMap { it }
        .onErrorContinue { t, _ -&amp;gt; t.printStackTrace() }
        .subscribe { record -&amp;gt;
            try {
                val commonConnectorMessage = this.gson.fromJson(record.value(), CommonConnectorMessage::class.java)
                this.sessionManager.sendMessage(
                    commonConnectorMessage.referenceTags.toSet(),
                    this.gson.toJson(commonConnectorMessage.message)
                )
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Server는 상시 message Topic을 구독하고 있으며&lt;br /&gt;유효한 메시지를 기반으로 message 전파를 호출한다.&lt;br /&gt;이미 지나간 메시지는 실시간 메시지 전파와 맞지 않기에, offset 전략은 latest&lt;br /&gt;reactor 설정인 offset commit은 autoAck(비동기)로 설정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;최소 기능만 충족하도록 구현했으며&lt;br /&gt;추후 기능추가 및 리펙토링이 진행될 것으로 예상된다.&lt;br /&gt;&lt;br /&gt;지금은 핵심 기능은 게임 엔진 설계에 집중하고 있다.&lt;br /&gt;목표는 기물을 기반으로 한 보드게임을 어느정도 커버할 수 있는 엔진을 만들고&lt;br /&gt;해당 엔진으로 서버 interface를 구성한 뒤&lt;br /&gt;web 또는 app을 구현하려 한다.&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;</description>
      <category>[완] 개인서버 개발/FlagWar(중단)</category>
      <category>Kafka</category>
      <category>kotlin</category>
      <category>session</category>
      <category>websocket</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/63</guid>
      <comments>https://p-bear.tistory.com/63#entry63comment</comments>
      <pubDate>Thu, 3 Aug 2023 18:00:29 +0900</pubDate>
    </item>
    <item>
      <title>API Documentation 정리 (oas,swagger,asyncAPI)</title>
      <link>https://p-bear.tistory.com/62</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCOA8g/btspPaJYPJu/w2uOlI1i28XkRk5kjSUhZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCOA8g/btspPaJYPJu/w2uOlI1i28XkRk5kjSUhZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCOA8g/btspPaJYPJu/w2uOlI1i28XkRk5kjSUhZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCOA8g%2FbtspPaJYPJu%2Fw2uOlI1i28XkRk5kjSUhZk%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;558&quot; height=&quot;439&quot; data-origin-width=&quot;558&quot; data-origin-height=&quot;439&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;개발 문서 중 API Document를 작성할 방안에 대한 조사 진행&lt;br /&gt;목표는 신뢰성 있으며, 유지보수에 용이한 API 문서 작성이다.&lt;br /&gt;Target API 프로토콜로 REST, RSocket, Kafka Message, Websocket Message 이다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;신뢰성: 문서와 실제 구현상의 간극이 최소화 되는 것을 말함&lt;br /&gt;유지보수 용이: Document 작성을 위한 시간을 최소화 하여, 생산성을 높이는 것, 또한 여러 타입의 문서로 재생산 가능한 것도 포함한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OpenAPI Specification 이란&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;OAS란 OpenAPI Specification의 약자로 REST API를 json 또는 yaml로 표현하는 방식에 대한 스펙을 의미한다. &lt;br /&gt;2010년대 초 Tam Wordnik이 개발할 당시에는 Swagger 스펙으로 불리었지만 &lt;br /&gt;2015년 즈음 SmartBear에 인수되고, OpenAPI Initiative에 기부되면서 OpenAPI Specification으로 변경 됨&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;OpenAPI: REST를 정의할 수 있는 스펙&lt;br /&gt;Swagger: OpenAPI 문서를 만들 수 있는 툴 (자동화 목적)&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;특징&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;REST API를 문서로 정의 및 생성 할 수 있다. &lt;br /&gt;방대한 생태계 서포트를 통해, REST 문서 관리에 이점이 있다. &lt;br /&gt;스펙 작성을 위한 공식 툴이 존재한다. &lt;br /&gt;스펙을 기반으로 SDK / 코드 생성이 가능한다.&lt;br /&gt;스펙을 기반으로 API 테스트도 가능하다. &lt;br /&gt;오픈 라이센스이다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OAS 생태계&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Swagger UI: OAS를 Web UI로 볼 수 있는 어플리케이션&lt;br /&gt;Swagger Editor: OAS 스펙 편집기, 브라우저 기반&lt;br /&gt;Swagger Codegen: OAS를 기반으로 Server, Client Code 생성&lt;br /&gt;Swagger Spring Integration: Spring 코드에서 OAS 문서를 생성해주는 라이브러리&lt;br /&gt;OpenAPI IDE Plugins: OAS를 위한 IDE 서포트&lt;br /&gt;Swagger Hub: OAS Document를 통한 협업을 손쉽게 진행할 수 있는 서비스&lt;br /&gt;&amp;hellip;&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 스펙 설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;전체 스펙: &lt;/span&gt;&lt;a href=&quot;https://swagger.io/specification/&quot;&gt;https://swagger.io/specification/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1690949452800&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;OpenAPI Specification - Version 3.0.3 | Swagger&quot; data-og-description=&quot;OpenAPI Specification Version 3.1.0 The key words &amp;quot;MUST&amp;quot;, &amp;quot;MUST NOT&amp;quot;, &amp;quot;REQUIRED&amp;quot;, &amp;quot;SHALL&amp;quot;, &amp;quot;SHALL NOT&amp;quot;, &amp;quot;SHOULD&amp;quot;, &amp;quot;SHOULD NOT&amp;quot;, &amp;quot;RECOMMENDED&amp;quot;, &amp;quot;NOT RECOMMENDED&amp;quot;, &amp;quot;MAY&amp;quot;, and &amp;quot;OPTIONAL&amp;quot; in this document are to be interpreted as described in BCP 14 RFC2119 RF&quot; data-og-host=&quot;swagger.io&quot; data-og-source-url=&quot;https://swagger.io/specification/&quot; data-og-url=&quot;https://swagger.io/specification/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://swagger.io/specification/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://swagger.io/specification/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;OpenAPI Specification - Version 3.0.3 | Swagger&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;OpenAPI Specification Version 3.1.0 The key words &quot;MUST&quot;, &quot;MUST NOT&quot;, &quot;REQUIRED&quot;, &quot;SHALL&quot;, &quot;SHALL NOT&quot;, &quot;SHOULD&quot;, &quot;SHOULD NOT&quot;, &quot;RECOMMENDED&quot;, &quot;NOT RECOMMENDED&quot;, &quot;MAY&quot;, and &quot;OPTIONAL&quot; in this document are to be interpreted as described in BCP 14 RFC2119 RF&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;swagger.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Field Name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Description&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;openapi&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;REQUIRED&lt;/span&gt;&lt;/b&gt;&lt;span&gt;. &lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;OAS&lt;/span&gt;의 스펙 버전&lt;span&gt;, &lt;/span&gt;문서 파싱할 때 참조하기 위한 값&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;info&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://swagger.io/specification/#info-object&quot;&gt;Info Object&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;REQUIRED&lt;/span&gt;&lt;/b&gt;&lt;span&gt;. &lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;본 문서를 설명하는&lt;span&gt; metadata&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;servers&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;[&lt;a href=&quot;https://swagger.io/specification/#server-object&quot;&gt;Server Object&lt;/a&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;테스트가 가능한 서버에 대한 정의&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;paths&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td data-highlight-colour=&quot;#abf5d1&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://swagger.io/specification/#paths-object&quot;&gt;&lt;b&gt;&lt;span&gt;Paths Object&lt;/span&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td data-highlight-colour=&quot;#abf5d1&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;REST Spec&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;에 대한 정의&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;webhooks&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Map[&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;, &lt;a href=&quot;https://swagger.io/specification/#path-item-object&quot;&gt;Path Item Object&lt;/a&gt; | &lt;a href=&quot;https://swagger.io/specification/#reference-object&quot;&gt;Reference Object&lt;/a&gt;] ]&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;콜백을 정의 할 수 있는 파트&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;components&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td data-highlight-colour=&quot;#abf5d1&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://swagger.io/specification/#components-object&quot;&gt;&lt;b&gt;&lt;span&gt;Components Object&lt;/span&gt;&lt;/b&gt;&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td data-highlight-colour=&quot;#abf5d1&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;REST Spec&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;에서 참조하는 객체 정의&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;security&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;[&lt;a href=&quot;https://swagger.io/specification/#security-requirement-object&quot;&gt;Security Requirement Object&lt;/a&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;인증 및 보안에 관한 스펙&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;tags&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;[&lt;a href=&quot;https://swagger.io/specification/#tag-object&quot;&gt;Tag Object&lt;/a&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;API&lt;/span&gt;를 묶을 수 있는&lt;span&gt; tag&lt;/span&gt;를 정의&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;externalDocs&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;https://swagger.io/specification/#external-documentation-object&quot;&gt;External Documentation Object&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;외부 문서 링크 등&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;paths Example&lt;/h4&gt;
&lt;pre id=&quot;code_1690949536824&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;paths:
  /pet/findByStatus:
    get:
      tags:
        - pet
      summary: Finds Pets by status
      description: Multiple status values can be provided with comma separated strings
      operationId: findPetsByStatus
      parameters:
        - name: status
          in: query
          description: Status values that need to be considered for filter
          required: false
          explode: true
          schema:
            type: string
            default: available
            enum:
              - available
              - pending
              - sold
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Pet'          
            application/xml:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Pet'
        '400':
          description: Invalid status value
      security:
        - petstore_auth:
            - write:pets
            - read:pets&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1690949553766&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;paths:
  /pet:
    post:
      tags:
        - pet
      summary: Add a new pet to the store
      description: Add a new pet to the store
      operationId: addPet
      requestBody:
        description: Create a new pet in the store
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
          application/xml:
            schema:
              $ref: '#/components/schemas/Pet'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/Pet'
        required: true
      responses:
        '200':
          description: Successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'          
            application/xml:
              schema:
                $ref: '#/components/schemas/Pet'
        '405':
          description: Invalid input
      security:
        - petstore_auth:
            - write:pets
            - read:pets&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Components Example&lt;/h4&gt;
&lt;pre id=&quot;code_1690950037116&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;components:
  schemas:
    Pet:
      required:
        - name
        - photoUrls
      type: object
      properties:
        id:
          type: integer
          format: int64
          example: 10
        name:
          type: string
          example: doggie
        category:
          $ref: '#/components/schemas/Category'
        photoUrls:
          type: array
          xml:
            wrapped: true
          items:
            type: string
            xml:
              name: photoUrl
        tags:
          type: array
          xml:
            wrapped: true
          items:
            $ref: '#/components/schemas/Tag'
        status:
          type: string
          description: pet status in the store
          enum:
            - available
            - pending
            - sold
      xml:
        name: pet&lt;/code&gt;&lt;/pre&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;Spring With Swagger (OAS)&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring 프로젝트에서 OAS 문서를 만들어 주는 서포트 라이브러리가 있다.&lt;br /&gt;org.springdoc:springdoc-openapi-ui&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sample Code&lt;/h4&gt;
&lt;pre id=&quot;code_1690950177628&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class Config {
    @Bean
    public OpenAPI openAPI() {
        return new OpenAPI()
                .info(new Info()
                  .title(&quot;...&quot;)
                  .description(&quot;...&quot;)
                  .externalDocs(new ExternalDocumentation()
                    .description(&quot;....&quot;)
                    .url(&quot;http://...&quot;));
    }
}

@OpenAPIDefinition(info = @io.swagger.v3.oas.annotations.info.Info(title = &quot;TEST API&quot;, version = &quot;v1&quot;))
@SecurityScheme(
        name = &quot;bearerAuth&quot;,
        type = SecuritySchemeType.HTTP,
        bearerFormat = &quot;JWT&quot;,
        scheme = &quot;bearer&quot;
)

@Tag(name = &quot;TestAPI&quot;, description = &quot;test&quot;)

@Operation(summary =  &quot;테스트 기능1, 인증 필요&quot;, security = @SecurityRequirement(name = &quot;bearerAuth&quot;))&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;h2 data-ke-size=&quot;size26&quot;&gt;OAS 시각화&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Swagger UI&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1485&quot; data-origin-height=&quot;401&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y6vpV/btspCmFoL9V/iZTJ18DoEuT7YW5prfae80/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y6vpV/btspCmFoL9V/iZTJ18DoEuT7YW5prfae80/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y6vpV/btspCmFoL9V/iZTJ18DoEuT7YW5prfae80/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy6vpV%2FbtspCmFoL9V%2FiZTJ18DoEuT7YW5prfae80%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;1485&quot; height=&quot;401&quot; data-origin-width=&quot;1485&quot; data-origin-height=&quot;401&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Swagger to Postman&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;799&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKbhKc/btspNgcMkTU/EvtHDtDCYNkkGyCazc6tqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKbhKc/btspNgcMkTU/EvtHDtDCYNkkGyCazc6tqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKbhKc/btspNgcMkTU/EvtHDtDCYNkkGyCazc6tqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKbhKc%2FbtspNgcMkTU%2FEvtHDtDCYNkkGyCazc6tqk%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;799&quot; height=&quot;618&quot; data-origin-width=&quot;799&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Swagger to Confluence&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1169&quot; data-origin-height=&quot;627&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cF6N14/btspFlzsQiY/8GukPH2vrCK8zZc7aIgcK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cF6N14/btspFlzsQiY/8GukPH2vrCK8zZc7aIgcK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cF6N14/btspFlzsQiY/8GukPH2vrCK8zZc7aIgcK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcF6N14%2FbtspFlzsQiY%2F8GukPH2vrCK8zZc7aIgcK0%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;1169&quot; height=&quot;627&quot; data-origin-width=&quot;1169&quot; data-origin-height=&quot;627&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Swagger2Markup&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;819&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9etSy/btspRJ6z9v6/b7E4swOodkC1YQ8iZOvqS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9etSy/btspRJ6z9v6/b7E4swOodkC1YQ8iZOvqS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9etSy/btspRJ6z9v6/b7E4swOodkC1YQ8iZOvqS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9etSy%2FbtspRJ6z9v6%2Fb7E4swOodkC1YQ8iZOvqS1%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;914&quot; height=&quot;819&quot; data-origin-width=&quot;914&quot; data-origin-height=&quot;819&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;관련 리서치&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;신뢰성 있는 API 문서란&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;카카오페이, 토스에서 채택한 방식으로&lt;br /&gt;Integration Test를 통과한 API만 문서가 작성되는 Spring REST Docs를 통해 문서를 생성하여 문서 신뢰도를 확보하고&lt;br /&gt;OAS(Swagger)를 통해 외부 공개 및 API Test 지원&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pdOLo/btspTJFeNeL/goMBeaZTd76toeVk01Ony1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pdOLo/btspTJFeNeL/goMBeaZTd76toeVk01Ony1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pdOLo/btspTJFeNeL/goMBeaZTd76toeVk01Ony1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpdOLo%2FbtspTJFeNeL%2FgoMBeaZTd76toeVk01Ony1%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;639&quot; height=&quot;284&quot; data-origin-width=&quot;639&quot; data-origin-height=&quot;284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;메인 아이디어&lt;br /&gt;- restdocs-api-spec 라이브러리를 통해, Rest Docs를 OAS Document로 변환&lt;br /&gt;- 이 후 프로세스는 동일&lt;/blockquote&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;MSA 환경에서 문서 배포 자동화&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;토스의 MSA 환경에서, Swagger를 활용한 문서 자동화 프로세스 정리&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1239&quot; data-origin-height=&quot;574&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9OdqS/btspFkHqjkW/FGwzuy5tcsUgjXN3LkihV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9OdqS/btspFkHqjkW/FGwzuy5tcsUgjXN3LkihV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9OdqS/btspFkHqjkW/FGwzuy5tcsUgjXN3LkihV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9OdqS%2FbtspFkHqjkW%2FFGwzuy5tcsUgjXN3LkihV1%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;1239&quot; height=&quot;574&quot; data-origin-width=&quot;1239&quot; data-origin-height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kUuJH/btspBADOdK8/dI7vsK7YEwFfm7PUsRkL8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kUuJH/btspBADOdK8/dI7vsK7YEwFfm7PUsRkL8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kUuJH/btspBADOdK8/dI7vsK7YEwFfm7PUsRkL8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkUuJH%2FbtspBADOdK8%2FdI7vsK7YEwFfm7PUsRkL8K%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;1174&quot; height=&quot;522&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;522&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;메인 아이디어&lt;br /&gt;- Github의 소스코드의 변경이 트리거링 되어, 젠킨스에서 안시블을 통해 문서 자동 생성 및 배포 되는 프로세스&lt;br /&gt;- 개발자는 코드 기반 Docs만 관리하며, 문서 배포가 자동적으로 될 수 있는 기본 프로세스로 보인다.&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;LINE에서 체택한 방식으로&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;다른 언어, 다른API 유형의 문서를 통합하여 관리 및 게시&lt;/b&gt;&lt;/span&gt;하기 위한 방식&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;목적&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;출력 형식이 바뀌어도 쉽게 적응할 수 있어야 한다.&lt;br /&gt;작성하는 사람이 뭘 써야 하는지 쉽게 알 수 있어야 한다.&lt;br /&gt;다양한 프로그래밍 언어와 API 유형을 통합할 수 있어야 한다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LMa2S/btspMjnqBVP/1SkrVPzKTa20sJwD21WkG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LMa2S/btspMjnqBVP/1SkrVPzKTa20sJwD21WkG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LMa2S/btspMjnqBVP/1SkrVPzKTa20sJwD21WkG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLMa2S%2FbtspMjnqBVP%2F1SkrVPzKTa20sJwD21WkG0%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;651&quot; height=&quot;383&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. Tool 또는 수기로 API를 작성합니다.&lt;br /&gt;2. 각각의 프로젝트에서 생성된 API를 하나의 Markup 템플릿으로 자동 컨버팅할 수 있도록 합니다.&lt;br /&gt;3. 해당 템플릿으로 여러 타입의 문서를 generate 합니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;메인 아이디어&lt;br /&gt;- 언어 및 API 유형이 다르면, API 문서를 관리하는 라이브러리 및 Tool이 다를 가능성이 높다.&lt;br /&gt;- 각 API 유형별로 문서가 생성될 수 있다. (같은 기능에 rest와 rsocket으로 제공한다면)&lt;br /&gt;- 이를 해결하기 위해 중간 template을 두고, 최종 문서와 자동 생성되는 문서의 의존성은 낮췄다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;문제점&lt;br /&gt;- 초기 구현 및 검증에 필요한 리소스가 많이 소요될 것으로 예상된다.&lt;br /&gt;- 문서에 필요한 항목 (최종 결과물)에 대한 정의에 리소스가 소요된다.&lt;br /&gt;- 문서 유지관리에 리소스가 소모된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AsyncAPI&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;675&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pYjiS/btspH09yy5O/VwXfpFqItUohCAiM3Zm5d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pYjiS/btspH09yy5O/VwXfpFqItUohCAiM3Zm5d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pYjiS/btspH09yy5O/VwXfpFqItUohCAiM3Zm5d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpYjiS%2FbtspH09yy5O%2FVwXfpFqItUohCAiM3Zm5d0%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;1241&quot; height=&quot;675&quot; data-origin-width=&quot;1241&quot; data-origin-height=&quot;675&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;OAS와 유사한 이벤트 드리븐 시스템을 위한 API 스펙&lt;br /&gt;YAML 기반 API Describe 문서&lt;br /&gt;이벤트 드리븐 아키텍쳐를 설명하기 위한 스펙이다.&lt;br /&gt;Protocol에 종속적이지 않다.&lt;br /&gt;Pub / Sub 구조를 정의하기 적합한 스펙&lt;/blockquote&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;Library Support&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Springwolf&lt;/h4&gt;
&lt;pre id=&quot;code_1690953380989&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Consumer
@KafkaListener
@AsyncListener(operation = @AsyncOperation(
        channelName = &quot;example-consumer-topic&quot;,
        description = &quot;Optional. Customer uploaded an example payload&quot;,
        headers = @AsyncOperation.Headers(
                schemaName = &quot;SpringKafkaDefaultHeaders&quot;,
                values = {
                        @AsyncOperation.Headers.Header(
                                name = DEFAULT_CLASSID_FIELD_NAME,
                                description = &quot;Spring Type Id Header&quot;,
                                value = &quot;io.github.stavshamir.springwolf.example.dtos.ExamplePayloadDto&quot;
                        ),
                }
        )
))
@KafkaAsyncOperationBinding
public void receiveMessage(ExamplePayloadDto msg) {
    // process
}

// Producer
@AsyncPublisher(operation = @AsyncOperation(
        channelName = &quot;example-producer-topic&quot;,
        description = &quot;Optional. Customer uploaded an example payload&quot;,
        headers = @AsyncOperation.Headers(
                schemaName = &quot;SpringKafkaDefaultHeaders&quot;,
                values = {
                        @AsyncOperation.Headers.Header(
                                name = DEFAULT_CLASSID_FIELD_NAME,
                                description = &quot;Spring Type Id Header&quot;,
                                value = &quot;io.github.stavshamir.springwolf.example.dtos.ExamplePayloadDto&quot;
                        ),
                }
        )
))
@KafkaAsyncOperationBinding
public void sendMessage(ExamplePayloadDto msg) {
    // send
}

// Message
@AsyncPublisher(operation = @AsyncOperation(
        channelName = &quot;example-producer-topic&quot;,
        description = &quot;Optional. Customer uploaded an example payload&quot;,
        message = @AsyncMessage(
                messageId = &quot;my-unique-id&quot;,
                name = &quot;ExamplePayloadDto&quot;,
                schemaFormat = &quot;application/schema+json;version=draft-07&quot;,
                description = &quot;Example payload model for sending messages&quot;
        )
))
public void sendMessage(ExamplePayloadDto msg) {
    // send
}&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;h2 data-ke-size=&quot;size26&quot;&gt;요약&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. OpenAPI Specification, Swagger는 REST 프로토콜에 종속적인 API Document 스펙이다.&lt;br /&gt;2. OAS는 REST 프로토콜을 정의하고 활용하기에 좋은 생태계를 가지고 있다.&lt;br /&gt;3. AsyncAPI는 Event-Driven 시스템을 정의할 수 있는 스펙이다.&lt;br /&gt;4. 문서 자동화를 달성 위해서는 파일 기반 Documentation이 효과적이다.&lt;br /&gt;5. json, yaml로 잘 정의된 스펙은 확장성이 뛰어나다.&lt;br /&gt;6. 문서 신뢰성을 위해서는 테스트 기반 문서 생성 프로세스가 필요하다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://swagger.io/specification/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://swagger.io/specification/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://plugins.jetbrains.com/plugin/8433-openapi-generator&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://plugins.jetbrains.com/plugin/8433-openapi-generator&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/document-engineering-api-documentation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://engineering.linecorp.com/ko/blog/document-engineering-api-documentation&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://tech.kakaopay.com/post/openapi-documentation/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://tech.kakaopay.com/post/openapi-documentation/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://rlxuc0ppd.toastcdn.net/presentation/%5BNHN%20FORWARD%202020%5DMSA%20%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%20API%20%EB%AC%B8%EC%84%9C%20%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://rlxuc0ppd.toastcdn.net/presentation/%5BNHN%20FORWARD%202020%5DMSA%20%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%20API%20%EB%AC%B8%EC%84%9C%20%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0.pdf&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.asyncapi.com/docs/tools/generator/asyncapi-document&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.asyncapi.com/docs/tools/generator/asyncapi-document&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.asyncapi.com/tools&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.asyncapi.com/tools&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://springwolf.github.io/docs/configuration/documenting-producers/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://springwolf.github.io/docs/configuration/documenting-producers/&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>AsyncAPI</category>
      <category>Documentation</category>
      <category>OAS</category>
      <category>openapi specification</category>
      <category>S</category>
      <category>SWAGGER</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/62</guid>
      <comments>https://p-bear.tistory.com/62#entry62comment</comments>
      <pubDate>Wed, 2 Aug 2023 18:18:28 +0900</pubDate>
    </item>
    <item>
      <title>mysql-connector with mariaDB 버전 호환성 이슈</title>
      <link>https://p-bear.tistory.com/61</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;249&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6ADJg/btso6MXcdzA/bRYzicv0rIOvkNFMn3w5mk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6ADJg/btso6MXcdzA/bRYzicv0rIOvkNFMn3w5mk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6ADJg/btso6MXcdzA/bRYzicv0rIOvkNFMn3w5mk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6ADJg%2Fbtso6MXcdzA%2FbRYzicv0rIOvkNFMn3w5mk%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;249&quot; height=&quot;203&quot; data-origin-width=&quot;249&quot; data-origin-height=&quot;203&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;보안 이슈로 인하여, OS 버전업을 하게되면서, DB 버전을 올리는 일이 발생했다.&lt;br /&gt;기존 코드는 mysql jdbc connector를 사용하여 mariaDB에 붙고 있었고&lt;br /&gt;mariaDB의 버전이 올라가는 상황이다.&lt;br /&gt;버전 업 과정에서 Connection 생성을 못하는 상황이 발생했으며, 해당 이슈 해결과정을 기록으로 남긴다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;변경 사항&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;전&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;후&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;mariaDB version&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;10.1.25&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;11.0.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;mysql-connector-java version&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;8.0.21&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%;&quot;&gt;8.0.21&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이슈 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예외 발생&lt;/h3&gt;
&lt;pre id=&quot;code_1690355475010&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Caused by: java.sql.SQLException: Unknown system variable 'transaction_isolation'
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
	at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:836)
	at com.mysql.cj.jdbc.ConnectionImpl.&amp;lt;init&amp;gt;(ConnectionImpl.java:456)
	at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:246)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;transaction_isolation이란 DB의 variable을 찾을 수 없는 예외 발생&lt;br /&gt;해당 예외로 인하여 connectionImpl 인스턴스 생성 불가&lt;/blockquote&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;DB variable 확인&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;383&quot; data-origin-height=&quot;117&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c78hU1/btso6i29sKb/OoPoeK2IPc1ya1XiR3tYv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c78hU1/btso6i29sKb/OoPoeK2IPc1ya1XiR3tYv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c78hU1/btso6i29sKb/OoPoeK2IPc1ya1XiR3tYv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc78hU1%2Fbtso6i29sKb%2FOoPoeK2IPc1ya1XiR3tYv1%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;383&quot; height=&quot;117&quot; data-origin-width=&quot;383&quot; data-origin-height=&quot;117&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;transaction_isolation이 아닌 tx_isolation이란 variable로 세팅되어있는 상황 확인&lt;/blockquote&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;926&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ILtmI/btso4xNhbsb/4OsvyztnkuSAX00DIlArak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ILtmI/btso4xNhbsb/4OsvyztnkuSAX00DIlArak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ILtmI/btso4xNhbsb/4OsvyztnkuSAX00DIlArak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FILtmI%2Fbtso4xNhbsb%2F4OsvyztnkuSAX00DIlArak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;926&quot; height=&quot;244&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;895&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4qXpc/btso5W0ngho/657tFK7q5KIX354ybLvuw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4qXpc/btso5W0ngho/657tFK7q5KIX354ybLvuw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4qXpc/btso5W0ngho/657tFK7q5KIX354ybLvuw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4qXpc%2Fbtso5W0ngho%2F657tFK7q5KIX354ybLvuw1%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;895&quot; height=&quot;261&quot; data-origin-width=&quot;895&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;공식 문서에 따르면 &lt;br /&gt;MariaDB 11.1 이전에는 tx_isolation 값을 사용하고, 11.1 이후에는 transaction_isolation으로 사용한다. &lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;즉 현재 mysql-connector가 비 정상적인 버전으로 인식하고 있는 상황&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&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;pre id=&quot;code_1690355721104&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// com.mysql.cj.NativeSession 클래스

// public void loadServerVariables(Object syncMutex, String version) 메서드
...
if (!this.versionMeetsMinimum(8, 0, 3) &amp;amp;&amp;amp; (!this.versionMeetsMinimum(5, 7, 20) || this.versionMeetsMinimum(8, 0, 0))) {
  queryBuf.append(&quot;, @@tx_isolation AS transaction_isolation&quot;);
} else {
  queryBuf.append(&quot;, @@transaction_isolation AS transaction_isolation&quot;);
}
...&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;mysql-connector:8.0.21 라이브러리의 관련 로직이다&lt;br /&gt;this.versionMeetsMinimum() 메서드의 경우 파라미터보다 낮거나 같으면 true를 반환한다.&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;런 타임 상 MariaDB의 버전을 11.0.2로 인식을 하기에, mysql의 버전체계와 비교하면 맞지 않게 된다.&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;최종적으로 구현체에서 transaction_isolation으로 설정 값을 찾아와 한다고 인식 &lt;br /&gt;결론적으로 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;mysql connector 구현체를 통해 mariaDB를 사용하기에 발생한 이슈&lt;/b&gt;&lt;/span&gt;이다&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;참고사항: 기존 mariaDB 10.1.25 버전에서는 mariaDB의 버전 추출 값이 5.5.5(MySQL에 대응되는 버전 값)로 파싱이 되기 때문에, 문제가 없었다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방안 모색&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;2가지 해결방안이 있을 것으로 판단된다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.&amp;nbsp;MariaDB&amp;nbsp;버전을&amp;nbsp;11.1&amp;nbsp;버전&amp;nbsp;이상으로&amp;nbsp;업그레이드&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;mariaDB를 11.1 버전으로 올려서 변수를 transaction_isolation으로 변수를 통일하여 이슈 해결하는 방안&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. mysql-connector 라이브러리 mariaDB-connector로 교체&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;현 시점 최신버전은 8.0.33 버전은 Mysql 버전 기준으로 버전 파싱 및 비교를 한다. &lt;br /&gt;따라서 mysql-connector의 버전 업이 아닌 mariadb connector로 변경해야 한다. &lt;br /&gt;+ jdbc 구현체 라이브러리가 변경되면서 소스의 드라이버 관련 코드를 모두 변경해야 한다. &lt;br /&gt;&amp;rarr; mariadb-java-client:3.1.4 버전으로 교체 및 코드 변경&lt;/blockquote&gt;
&lt;pre id=&quot;code_1690356661091&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// org.mariadb.jdbc
// public class Connection implements java.sql.Connection

...

public int getTransactionIsolation() throws SQLException {
    String sql = &quot;SELECT @@tx_isolation&quot;;
    if (!this.client.getContext().getVersion().isMariaDBServer() &amp;amp;&amp;amp; (this.client.getContext().getVersion().getMajorVersion() &amp;gt;= 8 &amp;amp;&amp;amp; this.client.getContext().getVersion().versionGreaterOrEqual(8, 0, 3) || this.client.getContext().getVersion().getMajorVersion() &amp;lt; 8 &amp;amp;&amp;amp; this.client.getContext().getVersion().versionGreaterOrEqual(5, 7, 20))) {
      sql = &quot;SELECT @@transaction_isolation&quot;;
    }

...&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;mariaDB에서 제공하는 jdbc를 보면&lt;br /&gt;mariaDB server인지 확인 후, mysql일 경우 버전 비교를 통해 transaction_isolation을 적용하여 기동하도록 되어있다.&lt;br /&gt;connector 라이브러리는 아직 11.1+버전에서 추가된 transaction_isolation에 대한 부분은 반영하지 못한 것으로 추정된다.&lt;/blockquote&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;blockquote data-ke-style=&quot;style3&quot;&gt;DB와 JDBC를 맞추는 방향이, 사이드 이펙트를 최소화 할 수 있는 방안이며&lt;br /&gt;추후 업데이트되는 측면에서 유리하다고 판단된다.&lt;br /&gt;따라서 해당 해결 방안을 채택해서 적용하였다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jira.mariadb.org/browse/MDEV-21921&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jira.mariadb.org/browse/MDEV-21921&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mariadb.com/kb/en/server-system-variables/#transaction_isolation&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://mariadb.com/kb/en/server-system-variables/#transaction_isolation&lt;/a&gt;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>db version</category>
      <category>JDBC</category>
      <category>MariaDB</category>
      <category>MySQL</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/61</guid>
      <comments>https://p-bear.tistory.com/61#entry61comment</comments>
      <pubDate>Wed, 26 Jul 2023 19:22:05 +0900</pubDate>
    </item>
    <item>
      <title>#1 FlagWar 기획</title>
      <link>https://p-bear.tistory.com/60</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;軍旗(군기)라는 중국 보드 게임이 있습니다.&lt;br /&gt;상대방의 깃발을 뺏는 전략 보드게임으로 상당히 재밌게 했던 기억이 있습니다.&lt;br /&gt;해당 게임을 크로스 플랫폼으로 적절히 번역해서 개발하는것이 목표&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요구사항 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;일반 2인 FlagWar 게임만 구현
&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;simple 매칭 시스템 구현 (매칭룰 없음 FIFO)&lt;/li&gt;
&lt;li&gt;게임 플레이 관련 구현&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;게임 resume을 염두한 설계&lt;/li&gt;
&lt;li&gt;기능적 확장성 고려&lt;/li&gt;
&lt;li&gt;인프라 확장성 및 가용성 고려&lt;/li&gt;
&lt;li&gt;반응성 고려&lt;/li&gt;
&lt;li&gt;핵심기능만 우선 구현 (추후 기능 추가개발)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;게임 룰 정의&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보드와 기물&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;1. 보드&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY0rPt/btsnj9abnxr/h2ft1HdvjmpPrQbdKVcmt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY0rPt/btsnj9abnxr/h2ft1HdvjmpPrQbdKVcmt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY0rPt/btsnj9abnxr/h2ft1HdvjmpPrQbdKVcmt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY0rPt%2Fbtsnj9abnxr%2Fh2ft1HdvjmpPrQbdKVcmt1%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;521&quot; height=&quot;720&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;5 X 12 게임판&lt;br /&gt;기물을 배치할 수 있는 위치는 총 60개 이며, 위치는 포인트로 지칭한다. 총 3종류의 포인트가 있다.&lt;br /&gt;기물이 움직이는 경로가 있으며, 경로는 총 2가지 있다.&lt;br /&gt;&lt;br /&gt;포인트 설명&lt;br /&gt;&amp;nbsp;- 본진(大本營): 본진의 의미를 가지며, 해당 위치에는 목표물에 해당하는 &quot;깃&quot;이 배치되어야 한다. 해당 위치에 올라간 기물은 움직일 수 없다.&lt;br /&gt;&amp;nbsp;- 거점(行營): 거점의 의미를 가지며, 배치 단계에서 기물을 배치할 수 없다. 게임 시작 후 기물을 거점으로 이동할 수 있으며, 해당 포인트의 기물은 무적이며 적에게 공격당할 수 없다.&lt;br /&gt;&amp;nbsp;- 초소(兵點): 초소의 의미를 가지며, 일반적인 배치 포인트 및 이동 포인트이다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;경로 설명&lt;br /&gt;&amp;nbsp;- 한줄 경로: 일반적인 경로이며, 한턴에 한칸씩 갈 수 있음을 의미한다.&lt;br /&gt;&amp;nbsp;- 2줄경로: 철도를 의미하며, 직선이동을 할 수 있다. 모든 기물은 다른 기물이 막지 않는 범위에서 여러칸 이동을 할 수 있다. 특수 기물인 공병대의 경우 철도로 연결된 모든경로를 대상으로 움직일 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 기물&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;621&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PYx1d/btsnjatmRIC/dRdKKTtjYIQdNnwCtoRY11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PYx1d/btsnjatmRIC/dRdKKTtjYIQdNnwCtoRY11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PYx1d/btsnjatmRIC/dRdKKTtjYIQdNnwCtoRY11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPYx1d%2FbtsnjatmRIC%2FdRdKKTtjYIQdNnwCtoRY11%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;720&quot; height=&quot;621&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;621&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;총 25개의 기물로 구성된다.&lt;br /&gt;일반 기물과 특수 기물로 나누어지며 각 기물은 고유의 등급, 고유능력을 가진다.&lt;br /&gt;자신보다 등급이 높은 기물에게 잡아먹힌다.&lt;br /&gt;번역보다는 한국 군 체계를 따라서 정의한다.&lt;br /&gt;&lt;br /&gt;일반 기물 / 갯 수&lt;br /&gt;&amp;nbsp;- 본부(司令): 1&lt;br /&gt;&amp;nbsp;-&amp;nbsp;군단(軍長): 1&lt;br /&gt;&amp;nbsp;-&amp;nbsp;사단(師長): 2&lt;br /&gt;&amp;nbsp;-&amp;nbsp;여단(旅長): 2&lt;br /&gt;&amp;nbsp;-&amp;nbsp;연대(團長): 2&lt;br /&gt;&amp;nbsp;-&amp;nbsp;대대(營長): 2&lt;br /&gt;&amp;nbsp;-&amp;nbsp;중대(蓮長): 3&lt;br /&gt;&amp;nbsp;-&amp;nbsp;분대(排長): 3&lt;br /&gt;일반 기물의 등급은 내림차순으로&lt;br /&gt;본부 -&amp;gt; 군단 -&amp;gt; 사단 -&amp;gt; 여단 -&amp;gt; 연대 -&amp;gt; 대대 -&amp;gt; 중대 -&amp;gt; 분대&lt;br /&gt;&lt;br /&gt;특수 기물 / 갯 수 / 고유능력&lt;br /&gt;&amp;nbsp;- 깃(軍旗): 1, 게임의 목표가 되는 기물로 항상 본진 포인트에 배치되어야하며, 움직일 수 없다. 지뢰가 3개 모두 제거되면, 기물이 상대방에게 노출된다. 등급은 최하로 모든 기물에게 먹힌다.&lt;br /&gt;&amp;nbsp;- 폭탄(炸彈): 2, 등급은 없으며, 모든 기물과 만나면 같이 죽는 자폭 기물이다. 일반 룰에서는 각 진영의 최 전방 1라인에 배치될 수 없다.&lt;br /&gt;&amp;nbsp;- 지뢰(地雷): 3, 등급은 최상급으로 공병을 제외한 모든 기물을 이긴다. 하지만 배치 후 움직일 수 없다. 일반 룰에서는 최 후방 2 라인에만 배치될 수 있다.&lt;br /&gt;&amp;nbsp;- 공병(工兵): 3, 등급은 최하급으로 오직 지뢰만 제거 가능한 기물이다. 철도가 연결되며 막히지 않은 모든 지점을 이동할 수 있다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 게임 룰 및 진행&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;배치 단계, 게임 단계로 총 2가지 단계로 구성된다.&lt;br /&gt;배치 단계는 게임 시작 전 기물을 배치하는 단계로 룰에 따라 각 기물들을 배치한다. 모든 기물을 배치하는 것이 원칙이다.&lt;br /&gt;게임 단계는 각 기물을 움직여서 승리 조건을 채우는 단계이다.&lt;br /&gt;턴 방식으로 각 기물을 움직일 수 있다.&lt;br /&gt;상대방 기물과 겹치면, 등급과 능력에 따라서 승패가 결정되며, 승리한 기물이 남고 패배한 기물은 제거된다. 비기면 둘다 사라진다.&lt;br /&gt;&lt;br /&gt;승리 조건&lt;br /&gt;&amp;nbsp;- 상대방의 깃을 공격한다.&lt;br /&gt;&amp;nbsp;- 상대방의 모든 기물이 움직일 수 없는 상태로 만든다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;설계 및 구현 난이도가 쫌 있을것으로 예상되며&lt;br /&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;</description>
      <category>[완] 개인서버 개발/FlagWar(중단)</category>
      <category>FlagWar</category>
      <category>게임</category>
      <category>기획</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/60</guid>
      <comments>https://p-bear.tistory.com/60#entry60comment</comments>
      <pubDate>Wed, 12 Jul 2023 16:58:40 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Consumer #4 Offset Commit Strategy</title>
      <link>https://p-bear.tistory.com/59</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/msgud/btskJsoDlGP/cVhqk19KA0SQNil0luiVTK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/msgud/btskJsoDlGP/cVhqk19KA0SQNil0luiVTK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/msgud/btskJsoDlGP/cVhqk19KA0SQNil0luiVTK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmsgud%2FbtskJsoDlGP%2FcVhqk19KA0SQNil0luiVTK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;390&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;390&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Kafka Consumer의 Commit 설정 및 전략에 대한 조사&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka Commit Config&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Default&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;enable.auto.commit&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Auto-Commit &lt;/span&gt;사용 여부&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;auto.commit.interval.ms&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Auto-Commit &lt;/span&gt;사용 시&lt;span&gt; Commit&lt;/span&gt;을 수행할 시간&lt;span&gt; (ms)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;5000&lt;/span&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AUTO-Commit&lt;/h2&gt;
&lt;pre id=&quot;code_1687244072026&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;enable.auto.commit : commit 여부
auto.commit.interval.ms : commit interval 시간&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1687244077398&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void maybeAutoCommitOffsetsAsync(long now) {
    if (autoCommitEnabled) {
        nextAutoCommitTimer.update(now);
        if (nextAutoCommitTimer.isExpired()) {
            nextAutoCommitTimer.reset(autoCommitIntervalMs);
            autoCommitOffsetsAsync();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;auto commit이 enable true 인 경우&lt;br /&gt;poll() 호출 시 마다 ConsumerCoordinator의 poll() 이 호출되고&lt;br /&gt;poll() 내부적으로 autoCommit설정 시 동작하는 maybeAutoCommitOffsetsAsync()가 수행된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Confluent Client Sample Code&lt;/h2&gt;
&lt;pre id=&quot;code_1687244105653&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;final Consumer&amp;lt;String, DataRecord&amp;gt; consumer = new KafkaConsumer&amp;lt;String, DataRecord&amp;gt;(props);
consumer.subscribe(Arrays.asList(topic));

Long total_count = 0L;

try {
  while (true) {
    ConsumerRecords&amp;lt;String, DataRecord&amp;gt; records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord&amp;lt;String, DataRecord&amp;gt; record : records) {
      String key = record.key();
      DataRecord value = record.value();
      total_count += value.getCount();
      System.out.printf(&quot;Consumed record with key %s and value %s, and updated total count to %d%n&quot;, key, value, total_count);
    }
  }
} finally {
  consumer.close();
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;일반 Client의 Sample은 Data를 순회하면서 처리한다.&lt;br /&gt;&lt;br /&gt;문제:&lt;br /&gt;&amp;nbsp;- 비동기 프로그램으로 record 처리 시, commit에 대한 문제가 발생&lt;br /&gt;&amp;nbsp;- record들이 각각 비동기적으로 처리 될 시, commit 시점에 대한 고려가 필요함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring&amp;nbsp;with&amp;nbsp;Kafka&amp;nbsp;(Commit&amp;nbsp;Strategy)&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring에서 Kafka를 Integration된 library의 interface / configuration 분석&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring은 내부적으로 Spring Configuration을 통해 KafkaConsumer 객체를 생성 후 관리를 한다.&lt;br /&gt;Spring은 Commit을 AckMode로 불린다.&lt;br /&gt;Spring은 KafkaConsumer를 사용하기 위한 Listener를 제공한다.&lt;br /&gt;Listener는 MessageListener와 BatchMessageListener 2가지를 제공하며, 차이점은 한 번에 처리할 Record가 1개 또는 다수 일지 이다.&lt;br /&gt;자세한 사항은 후술&lt;/blockquote&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;Spring Kafka AckMode&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.8837%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;AckMode&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.8837%;&quot;&gt;&lt;span&gt;&lt;span&gt;RECORD&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85%;&quot;&gt;&lt;span&gt;&lt;span&gt;Commit the offset after each record is processed by the listener.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;한번에 하나의 처리된 레코드 커밋&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.8837%;&quot;&gt;&lt;span&gt;&lt;span&gt;BATCH&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85%;&quot;&gt;&lt;span&gt;&lt;span&gt;Commit the offsets of all records returned by the previous poll after they all have been processed by the listener.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;이전&lt;span&gt; poll()&lt;/span&gt;된&lt;span&gt; record&lt;/span&gt;가 모두 처리된 후 커밋&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.8837%;&quot;&gt;&lt;span&gt;&lt;span&gt;TIME&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85%;&quot;&gt;&lt;span&gt;&lt;span&gt;Commit pending offsets after&lt;br /&gt;{@link ContainerProperties#setAckTime(long) ackTime} has elapsed.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;actTime &lt;/span&gt;이후&lt;span&gt; pending&lt;/span&gt;된&lt;span&gt; record &lt;/span&gt;커밋&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.8837%;&quot;&gt;&lt;span&gt;&lt;span&gt;COUNT&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85%;&quot;&gt;&lt;span&gt;&lt;span&gt;Commit pending offsets after&lt;br /&gt;{@link ContainerProperties#setAckCount(int) ackCount} has been exceeded.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;AckCount &lt;/span&gt;만큼&lt;span&gt; record &lt;/span&gt;처리 후 커밋&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.8837%;&quot;&gt;&lt;span&gt;&lt;span&gt;COUNT_TIME&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85%;&quot;&gt;&lt;span&gt;&lt;span&gt;Commit pending offsets after&lt;br /&gt;{@link ContainerProperties#setAckCount(int) ackCount} has been exceeded or after {@link ContainerProperties#setAckTime(long) ackTime} has elapsed.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;TIME or COUNT &lt;/span&gt;조건 만족 시 커밋&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.8837%;&quot;&gt;&lt;span&gt;&lt;span&gt;MANUAL&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85%;&quot;&gt;&lt;span&gt;&lt;span&gt;Listener is responsible for acking - use a&lt;br /&gt;{@link org.springframework.kafka.listener.AcknowledgingMessageListener}; acks will be queued and offsets will be committed when all the records returned by the previous poll have been processed by the listener.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;AcknowledgingMessageListener&lt;/span&gt;를 통해서 커밋을 큐잉한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;Acknowledgement.acknowledge()&lt;/span&gt;가 호출되면 다음번&lt;span&gt; poll()&lt;/span&gt;때 이전&lt;span&gt; Records&lt;/span&gt;들을&lt;span&gt; commit &lt;/span&gt;한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 14.8837%;&quot;&gt;&lt;span&gt;&lt;span&gt;MANUAL_IMMEDIATE&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 85%;&quot;&gt;&lt;span&gt;&lt;span&gt;Listener is responsible for acking - use a&lt;br /&gt;{@link org.springframework.kafka.listener.AcknowledgingMessageListener}; the commit will be performed immediately if the {@code Acknowledgment} is acknowledged on the calling consumer thread; otherwise, the acks will be queued and offsets will be committed when all the records returned by the previous poll have been processed by the listener; results will be indeterminate if you sometimes acknowledge on the calling thread and sometimes not.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;AcknowledgingMessageListener&lt;/span&gt;를 통해 즉시 커밋을 호출한다&lt;span&gt;.&lt;/span&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Quarkus with Kafka (Commit Strategy)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고:&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Quarkus는 MSA를 구성하기 위해 만들어진 Java 기반 Framework이다.&lt;br /&gt;Red-hat의 주도로 만들어졌으며, 주요 이점은 다음과 같다&lt;br /&gt;&amp;nbsp;- 개발자 친화적&lt;br /&gt;&amp;nbsp;- 컨테이너(쿠버네티스)에 Java를 최적화 (GraalVM 최적화)&lt;br /&gt;&amp;nbsp;- common한 framework와 결합&lt;br /&gt;&amp;nbsp;- &lt;b&gt;Reactive한 code flow 지원&lt;/b&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Quarkus의 Kafka 비동기 프로그래밍 Commit 전략&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 131px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.8372%; height: 17px;&quot;&gt;&lt;b&gt;Strategy&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 81.1628%; height: 17px;&quot;&gt;&lt;b&gt;Desc&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 80px;&quot;&gt;
&lt;td style=&quot;width: 18.8372%; height: 80px;&quot;&gt;Throttled&lt;/td&gt;
&lt;td style=&quot;width: 81.1628%; height: 80px;&quot;&gt;&lt;span&gt;제한 전략&lt;/span&gt;&lt;br /&gt;&lt;span&gt;모든 비동기 처리에 대한 ack를 관리하고&lt;/span&gt;&lt;br /&gt;&lt;span&gt;특정 위치의 이전 ack가 모두 성공한 상황에서 commit 실행&lt;/span&gt;&lt;br /&gt;&lt;span&gt;영원히 오지 않을 수 있는 ack에 대해서는 별도의 max-age 속성을 통해 시간 체크 후 무시&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.8372%; height: 17px;&quot;&gt;Ignore&lt;/td&gt;
&lt;td style=&quot;width: 81.1628%; height: 17px;&quot;&gt;&lt;span&gt;무시 전략&lt;/span&gt;&lt;br /&gt;&lt;span&gt;commit에 관여하지 않는다.&lt;/span&gt;&lt;br /&gt;&lt;span&gt;gracefully shutdown시 또는 rebalance 시에만 commit 동작&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 18.8372%; height: 17px;&quot;&gt;Latest&lt;/td&gt;
&lt;td style=&quot;width: 81.1628%; height: 17px;&quot;&gt;&lt;span&gt;최신 전략&lt;/span&gt;&lt;br /&gt;&lt;span&gt;일정 주기로 비동기 처리의 최신 ack의 offset을 commit&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reactive Kafka (Commit Strategy)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ACK Mode Summary&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.5116%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;AckMode&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.3721%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.5116%;&quot;&gt;&lt;span&gt;&lt;span&gt;AUTO_ACK&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.3721%;&quot;&gt;&lt;span&gt;&lt;span&gt;AUTO_ACK &lt;/span&gt;모드로&lt;span&gt; Flux &lt;/span&gt;생성 시&lt;span&gt; KafkaConsumer&lt;/span&gt;에서&lt;span&gt; poll()&lt;/span&gt;된&lt;span&gt; records&lt;/span&gt;들을&lt;span&gt; Flux&lt;/span&gt;로 받는다&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;전달받은&lt;span&gt; Flux (poll &lt;/span&gt;된&lt;span&gt; records)&lt;/span&gt;가 종료되면 자동으로&lt;span&gt; commit &lt;/span&gt;처리된다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;최대&lt;span&gt; poll() &lt;/span&gt;된 크기 단위로&lt;span&gt; Message &lt;/span&gt;중복이 발생할 수 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.5116%;&quot;&gt;&lt;span&gt;&lt;span&gt;MANUAL_ACK&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.3721%;&quot;&gt;&lt;span&gt;수동으로&lt;span&gt; offset &lt;/span&gt;조작을 하기 위한 모드&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;전달받은&lt;span&gt; Record&lt;/span&gt;의&lt;span&gt; offset&lt;/span&gt;을 기반으로 하여 수동으로&lt;span&gt; commit &lt;/span&gt;수행&lt;span&gt; (&lt;/span&gt;내부적으로&lt;span&gt; commit &lt;/span&gt;된&lt;span&gt; offset &lt;/span&gt;보다 작으면 무시&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;offset&lt;/span&gt;이 기존보다 크면&lt;span&gt;, commit scheduler&lt;/span&gt;에&lt;span&gt; commit&lt;/span&gt;할&lt;span&gt; offset &lt;/span&gt;등록&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.5116%;&quot;&gt;&lt;span&gt;&lt;span&gt;ATMOST_ONCE&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.3721%;&quot;&gt;&lt;span&gt;중복 처리를 방지하는 커밋 모드&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;poll() &lt;/span&gt;이 후&lt;span&gt; flux &lt;/span&gt;생성 전 동기적으로&lt;span&gt; poll()&lt;/span&gt;한&lt;span&gt; record&lt;/span&gt;에 대한&lt;span&gt; commit&lt;/span&gt;을 진행&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;commit&lt;/span&gt;이 동기적으로 수행되기에 실제&lt;span&gt; record&lt;/span&gt;를 받는데 비용이 발생&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;사실 상&lt;span&gt; record&lt;/span&gt;를 받은 시점에서&lt;span&gt; commit &lt;/span&gt;하는것으로 볼 수 있으며&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;commit &lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;설정에 따라서&lt;span&gt; Message &lt;/span&gt;손실이 발생할 수 있다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.5116%;&quot;&gt;&lt;span&gt;&lt;span&gt;EXACTLY_ONCE&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.3721%;&quot;&gt;&lt;span&gt;&lt;span&gt;consum &amp;rarr; process &amp;rarr; produce &lt;/span&gt;의&lt;span&gt; transaction&lt;/span&gt;을 보장해주는 모드&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;produce&lt;/span&gt;의&lt;span&gt; transaction&lt;/span&gt;을 받아서&lt;span&gt; commit()&lt;/span&gt;을 실행한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #000000; text-align: left;&quot;&gt;AUTO_ACK vs ATMOST_ONCE 성능&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;AUTO_ACK 모드는 asyncCommit()을 이용하기 때문에, commit 메시지가 유실될 수 있어, 메시지 중복처리가 발생할 수 있다.&lt;br /&gt;ATMOST_ONCE는 syncCommit()을 이용하기 때문에, commit 메시지가 유실되지 않으며, 미리 commit하기 때문에 중복처리가 없지만, 손실이 발생할 수 있다.&lt;br /&gt;따라서 성능적으로 asyncCommit()을 사용하는 AUTO_ACK 모드가 syncCommit()을 사용하는 ATMOST_ONCE보다 좋다&lt;/blockquote&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;Reactive Kafka Commit Config&lt;/h3&gt;
&lt;pre id=&quot;code_1687244530593&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Map&amp;lt;String, Object&amp;gt; consumerConfig = new HashMap&amp;lt;&amp;gt;();
consumerConfig.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, &quot;192.168.2.89:29091&quot;);
consumerConfig.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;test-1&quot;);
consumerConfig.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
consumerConfig.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
consumerConfig.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, OffsetResetStrategy.EARLIEST.name().toLowerCase());
consumerConfig.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 10);
consumerConfig.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, List.of(&quot;LoggingCommitInterceptor&quot;));

ReceiverOptions&amp;lt;String, String&amp;gt; receiverOptions = ReceiverOptions.create(consumerConfig);

this.kafkaReceiver = KafkaReceiver
    .create(receiverOptions
        .commitInterval(Duration.ZERO)
        .commitBatchSize(1)
        .subscription(List.of(&quot;commit-test-1&quot;)));&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;ConsumerConfig (Kafka Client Config)를 기반으로 ReceiverOptions 생성 후&lt;br /&gt;Reactive에서 정의된 option들을 추가적으로 설정한다.&lt;br /&gt;KafkaReceiver는 ReceiverOptions 기반으로 생성한다.&lt;br /&gt;내부적으로 KafkaClient는 ConsumerConfig를 기반으로 동작하고, ConsumerHandler가 전체적인 Config를 종합하여 동작한다.&lt;br /&gt;&lt;b&gt;※ KafkaReceiver는 기본적으로 Commit을 ACK_MODE에 따라서 자체적으로 관리한다. 따라서 ConsumerConfig의 Auto-commit 옵션은 무시된다고 보면 된다.&lt;/b&gt;&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3721%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.5116%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3721%;&quot;&gt;&lt;span&gt;&lt;span&gt;commitInterval(Duration commitInterval)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.5116%;&quot;&gt;&lt;span&gt;&lt;span&gt;Commit&lt;/span&gt;의&lt;span&gt; interval &lt;/span&gt;시간&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;값 설정 시&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;batchSize&lt;/span&gt;에 도달 안해도 시간에 따라서&lt;span&gt; commit &lt;/span&gt;요청&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;으로 설정 시&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;batchSize&lt;/span&gt;가 설정 되어있을 시&lt;span&gt; batchSize &lt;/span&gt;설정 만으로&lt;span&gt; commit &lt;/span&gt;동작&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;batchSzie&lt;/span&gt;도&lt;span&gt; 0&lt;/span&gt;일 시 수동으로&lt;span&gt; commit&lt;/span&gt;에 대한&lt;span&gt; handling &lt;/span&gt;필요&lt;span&gt; (ReceiverOffset.commit())&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3721%;&quot;&gt;&lt;span&gt;&lt;span&gt;commitBatchSize(int commitBatchSize)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.5116%;&quot;&gt;&lt;span&gt;&lt;span&gt;commit&lt;/span&gt;시 필요한&lt;span&gt; record&lt;/span&gt;의 최소 단위 설정&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;값 설정시&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;값에 도달하면&lt;span&gt; commit&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;으로 설정 시&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;interval &lt;/span&gt;설정에 의해서만&lt;span&gt; commit &lt;/span&gt;동작&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;interval &lt;/span&gt;또한&lt;span&gt; 0&lt;/span&gt;이면 수동으로&lt;span&gt; commit&lt;/span&gt;에 대한&lt;span&gt; handling &lt;/span&gt;필요&lt;span&gt; (ReceiverOffset.commit())&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3721%;&quot;&gt;&lt;span&gt;&lt;span&gt;atmostOnceCommitAheadSize(int commitAheadSize)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.5116%;&quot;&gt;&lt;span&gt;&lt;span&gt;ATMOST_ONCE&lt;/span&gt;에서 보장하는 최대로 손실될&lt;span&gt; size&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;값 설정시&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;매번&lt;span&gt; record &lt;/span&gt;발행 전&lt;span&gt; commit&lt;/span&gt;을 확인하여 발행할&lt;span&gt; records&lt;/span&gt;가 현재&lt;span&gt; commit&lt;/span&gt;을 넘으면&lt;span&gt;, size + 1 &lt;/span&gt;만큼&lt;span&gt; syncCommit()&lt;/span&gt;을 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;따라서 최대로 손실 될 수 있는&lt;span&gt; message &lt;/span&gt;갯수는&lt;span&gt; size + 1 &lt;/span&gt;이다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;0&lt;/span&gt;으로 설정 시&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;middot;&lt;span&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;record &lt;/span&gt;발행 시마다 미리&lt;span&gt; syncCommit()&lt;/span&gt;을 한다&lt;span&gt;.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3721%;&quot;&gt;&lt;span&gt;&lt;span&gt;maxCommitAttempts(int maxAttempts)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.5116%;&quot;&gt;&lt;span&gt;&lt;span&gt;commit &lt;/span&gt;최대 시도 횟수&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3721%;&quot;&gt;&lt;span&gt;&lt;span&gt;commitRetryInterval(Duration commitRetryInterval)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.5116%;&quot;&gt;&lt;span&gt;&lt;span&gt;commit retry &lt;/span&gt;시간&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3721%;&quot;&gt;&lt;span&gt;&lt;span&gt;maxDeferredCommits(int maxDeferred)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.5116%;&quot;&gt;&lt;span&gt;지연된&lt;span&gt; commit&lt;/span&gt;의 최대 갯수&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;AUTO_ACK&lt;/h3&gt;
&lt;pre id=&quot;code_1687244616466&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default Flux&amp;lt;Flux&amp;lt;ConsumerRecord&amp;lt;K, V&amp;gt;&amp;gt;&amp;gt; receiveAutoAck() {
    return receiveAutoAck(null);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;poll() 된 records단위로 flux가 생성된다.&lt;br /&gt;flux 단위로 commit이 처리된다.&lt;br /&gt;따라서, 서버가 갑자기 다운되는 경우 마지막으로 처리된 poll() records가 commit 되어있어 중복 처리가 발생&lt;/blockquote&gt;
&lt;pre id=&quot;code_1687244632172&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;this.kafkaReceiver = KafkaReceiver
    .create(receiverOptions
        .commitInterval(Duration.of(10, ChronoUnit.MILLIS))
        .commitBatchSize(1)
        .subscription(List.of(&quot;commit-test-1&quot;)));

...

private static void subscribeAutoAck() {
  KafkaReceiverManager kafkaReceiverManager = new KafkaReceiverManager();

  kafkaReceiverManager.getReceiver()
      .receiveAutoAck()
      .concatMap(r -&amp;gt; r)
      .doOnNext(record -&amp;gt; errorByPrefix(&quot;55&quot;, record.value()))
      .onErrorContinue((e, o) -&amp;gt; {
        log.error(&quot;error on consume&quot;, e);
        System.exit(1);
      })
      .subscribe(record -&amp;gt; log.info(&quot;key: {}, value: {}&quot;, record.key(), record.value()));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;// concatMap&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;391&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ddvgd/btskBKEBWnx/HK2va4XYvb0R8WOnQI58PK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ddvgd/btskBKEBWnx/HK2va4XYvb0R8WOnQI58PK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ddvgd/btskBKEBWnx/HK2va4XYvb0R8WOnQI58PK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDdvgd%2FbtskBKEBWnx%2FHK2va4XYvb0R8WOnQI58PK%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;496&quot; height=&quot;391&quot; data-origin-width=&quot;496&quot; data-origin-height=&quot;391&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Test log&lt;/h4&gt;
&lt;pre id=&quot;code_1687244668009&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 10개 씩 records Poll()
14:15:46.014 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onConsume: commit-test-1-0-2740,commit-test-1-0-2741,commit-test-1-0-2742,commit-test-1-0-2743,commit-test-1-0-2744,commit-test-1-0-2745,commit-test-1-0-2746,commit-test-1-0-2747,commit-test-1-0-2748,commit-test-1-0-2749

// Emit
14:15:46.014 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Emitting 10 records, requested now 0

// 10개 정상 처리
14:15:46.014 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 41-1260010f-ded8-4c5e-b8db-8006d2491bff
14:15:46.014 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 42-04dbe9e7-b21f-445e-8d3f-09a582dbe3b9
14:15:46.014 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 43-14d5c8e4-e1b5-4610-be9d-edac51cf9f6c
14:15:46.014 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 44-161c3aae-6d60-49bd-bc4a-95a811988d57
14:15:46.014 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 45-c7690625-9ae5-4987-9519-e8b6dfb82831
14:15:46.014 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 46-9f9e68b5-a199-4b25-abca-263742e6cb33
14:15:46.014 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 47-474d1bfa-c755-4ca8-ab57-771cb4d718e2
14:15:46.014 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 48-0ab63698-8ff2-4369-bc93-e9c763c4b152
14:15:46.014 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 49-cb46f2a2-79a7-4949-bda3-8c62719113da
14:15:46.014 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 50-7bc420c8-1a00-4c41-9f63-599bf49a80d0
14:15:46.014 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- onRequest.toAdd 1, paused false

// Async Commit (check by ConsumerEventLoop)
14:15:46.014 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Async committing: {commit-test-1-0=OffsetAndMetadata{offset=2750, leaderEpoch=null, metadata=''}}

// KafkaConsumer로 commit 호출
14:15:46.014 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.consumer.KafkaConsumer -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Committing offsets: {commit-test-1-0=OffsetAndMetadata{offset=2750, leaderEpoch=null, metadata=''}}

// NetworkClient로 commit 호출, Get Reponse From Broker
14:15:46.014 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.NetworkClient -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Sending OFFSET_COMMIT request with header RequestHeader(apiKey=OFFSET_COMMIT, apiVersion=8, clientId=consumer-test-1-1, correlationId=35) and timeout 30000 to node 2147483646: OffsetCommitRequestData(groupId='test-1', generationId=90, memberId='consumer-test-1-1-97557ddd-e091-4e03-9db5-c687955bd175', groupInstanceId=null, retentionTimeMs=-1, topics=[OffsetCommitRequestTopic(name='commit-test-1', partitions=[OffsetCommitRequestPartition(partitionIndex=0, committedOffset=2750, committedLeaderEpoch=-1, commitTimestamp=-1, committedMetadata='')])])
14:15:46.015 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.NetworkClient -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Received OFFSET_COMMIT response from node 2147483646 for request with header RequestHeader(apiKey=OFFSET_COMMIT, apiVersion=8, clientId=consumer-test-1-1, correlationId=33): OffsetCommitResponseData(throttleTimeMs=0, topics=[OffsetCommitResponseTopic(name='commit-test-1', partitions=[OffsetCommitResponsePartition(partitionIndex=0, errorCode=0)])])

// 10개 씩 Poll()
14:15:46.015 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onConsume: commit-test-1-0-2750,commit-test-1-0-2751,commit-test-1-0-2752,commit-test-1-0-2753,commit-test-1-0-2754,commit-test-1-0-2755,commit-test-1-0-2756,commit-test-1-0-2757,commit-test-1-0-2758,commit-test-1-0-2759

// Emit
14:15:46.015 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Emitting 10 records, requested now 0

// 54까지 정상, 55에서 Error, System.exit(1)
14:15:46.015 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 51-22044176-c4bf-4f77-bbfa-1ba66f638db6
14:15:46.015 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 52-c830bc30-c4b1-4155-9916-982c8d2fba6d
14:15:46.015 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 53-aefd0b98-9309-440c-998f-c2f6c27acb92
14:15:46.015 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 54-4262041c-9ace-45ea-b87d-cb993c10922c
14:15:46.017 [reactive-kafka-test-1-1] ERROR TestMain -- error on consume
java.lang.RuntimeException: prefix error !!
	at TestMain.errorByPrefix(TestMain.java:40)
	at TestMain.lambda$subscribeAutoAck$1(TestMain.java:22)
...&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;1159&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NdUK1/btskEoVcMof/xKzK4FBnFxOP7dyT4YbNKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NdUK1/btskEoVcMof/xKzK4FBnFxOP7dyT4YbNKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NdUK1/btskEoVcMof/xKzK4FBnFxOP7dyT4YbNKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNdUK1%2FbtskEoVcMof%2FxKzK4FBnFxOP7dyT4YbNKk%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;1159&quot; height=&quot;181&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;181&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;기본적으로 polling된 Records의 단위로 동작을 하기 때문에, 메시지의 중복 처리가 발생할 수 있다.&lt;br /&gt;Commit이 별도의 Async로 동작하기에, CommitInterval 설정과 BatchSize를 적절하게 세팅해야한다.&lt;br /&gt;경우에 따라서 중복 처리되는 메시지가 늘어날 수 있다.&lt;br /&gt;&amp;nbsp;- commitAsync 요청이 NetworkClient 안에서 큐잉이 발생했을 때 Application이 강제 종료되면 마지막으로 보내진 Commit 이후 메시지들이 중복처리됨&lt;/blockquote&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;ATMOST_ONCE&lt;/h3&gt;
&lt;pre id=&quot;code_1687244792568&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;default Flux&amp;lt;ConsumerRecord&amp;lt;K, V&amp;gt;&amp;gt; receiveAtmostOnce() {
    return receiveAtmostOnce(null);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1687244805313&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;this.kafkaReceiver = KafkaReceiver
        .create(receiverOptions
            .atmostOnceCommitAheadSize(20)
            .subscription(List.of(&quot;commit-test-1&quot;)));

private static void subscribeAtMostOnce() {
  KafkaReceiverManager kafkaReceiverManager = new KafkaReceiverManager();

  kafkaReceiverManager.getReceiver()
      .receiveAtmostOnce()
      .doOnNext(record -&amp;gt; errorByPrefix(&quot;55&quot;, record.value()))
      .onErrorContinue((e, o) -&amp;gt; {
        log.error(&quot;error on consume&quot;, e);
        System.exit(1);
      })
      .subscribe(record -&amp;gt; {
        log.info(&quot;key: {}, value: {}&quot;, record.key(), record.value());
      });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Test Log&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;size = 0&lt;/p&gt;
&lt;pre id=&quot;code_1687244840338&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
16:29:08.387 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 52-9f1921f1-9eee-4d50-a42d-f008687aada2
16:29:08.387 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Sync committing: {commit-test-1-0=OffsetAndMetadata{offset=2953, leaderEpoch=null, metadata=''}}
16:29:08.387 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.NetworkClient -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Sending OFFSET_COMMIT request with header RequestHeader(apiKey=OFFSET_COMMIT, apiVersion=8, clientId=consumer-test-1-1, correlationId=62) and timeout 30000 to node 2147483646: OffsetCommitRequestData(groupId='test-1', generationId=94, memberId='consumer-test-1-1-2c2a4598-c0ca-463a-a31a-40c591c40daa', groupInstanceId=null, retentionTimeMs=-1, topics=[OffsetCommitRequestTopic(name='commit-test-1', partitions=[OffsetCommitRequestPartition(partitionIndex=0, committedOffset=2953, committedLeaderEpoch=-1, commitTimestamp=-1, committedMetadata='')])])
16:29:08.388 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.NetworkClient -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Received OFFSET_COMMIT response from node 2147483646 for request with header RequestHeader(apiKey=OFFSET_COMMIT, apiVersion=8, clientId=consumer-test-1-1, correlationId=62): OffsetCommitResponseData(throttleTimeMs=0, topics=[OffsetCommitResponseTopic(name='commit-test-1', partitions=[OffsetCommitResponsePartition(partitionIndex=0, errorCode=0)])])
16:29:08.389 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.consumer.internals.ConsumerCoordinator -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Committed offset 2953 for partition commit-test-1-0
16:29:08.389 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onCommit: commit-test-1-0-2953
16:29:08.389 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 53-fd5794b0-0252-4345-a110-cd1e5eab7dbc
16:29:08.389 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onConsume: commit-test-1-0-2990,commit-test-1-0-2991,commit-test-1-0-2992,commit-test-1-0-2993,commit-test-1-0-2994,commit-test-1-0-2995,commit-test-1-0-2996,commit-test-1-0-2997,commit-test-1-0-2998,commit-test-1-0-2999
16:29:08.389 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Emitting 10 records, requested now 10
16:29:08.389 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Sync committing: {commit-test-1-0=OffsetAndMetadata{offset=2954, leaderEpoch=null, metadata=''}}
16:29:08.389 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.NetworkClient -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Sending OFFSET_COMMIT request with header RequestHeader(apiKey=OFFSET_COMMIT, apiVersion=8, clientId=consumer-test-1-1, correlationId=63) and timeout 30000 to node 2147483646: OffsetCommitRequestData(groupId='test-1', generationId=94, memberId='consumer-test-1-1-2c2a4598-c0ca-463a-a31a-40c591c40daa', groupInstanceId=null, retentionTimeMs=-1, topics=[OffsetCommitRequestTopic(name='commit-test-1', partitions=[OffsetCommitRequestPartition(partitionIndex=0, committedOffset=2954, committedLeaderEpoch=-1, commitTimestamp=-1, committedMetadata='')])])
16:29:08.390 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.NetworkClient -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Received OFFSET_COMMIT response from node 2147483646 for request with header RequestHeader(apiKey=OFFSET_COMMIT, apiVersion=8, clientId=consumer-test-1-1, correlationId=63): OffsetCommitResponseData(throttleTimeMs=0, topics=[OffsetCommitResponseTopic(name='commit-test-1', partitions=[OffsetCommitResponsePartition(partitionIndex=0, errorCode=0)])])
16:29:08.391 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.consumer.internals.ConsumerCoordinator -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Committed offset 2954 for partition commit-test-1-0
16:29:08.391 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onCommit: commit-test-1-0-2954
16:29:08.391 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 54-d5942adc-529f-4af9-99d7-5c8df55bb1aa
16:29:08.391 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Sync committing: {commit-test-1-0=OffsetAndMetadata{offset=2955, leaderEpoch=null, metadata=''}}
16:29:08.391 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.NetworkClient -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Sending OFFSET_COMMIT request with header RequestHeader(apiKey=OFFSET_COMMIT, apiVersion=8, clientId=consumer-test-1-1, correlationId=64) and timeout 30000 to node 2147483646: OffsetCommitRequestData(groupId='test-1', generationId=94, memberId='consumer-test-1-1-2c2a4598-c0ca-463a-a31a-40c591c40daa', groupInstanceId=null, retentionTimeMs=-1, topics=[OffsetCommitRequestTopic(name='commit-test-1', partitions=[OffsetCommitRequestPartition(partitionIndex=0, committedOffset=2955, committedLeaderEpoch=-1, commitTimestamp=-1, committedMetadata='')])])
16:29:08.392 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.NetworkClient -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Received OFFSET_COMMIT response from node 2147483646 for request with header RequestHeader(apiKey=OFFSET_COMMIT, apiVersion=8, clientId=consumer-test-1-1, correlationId=64): OffsetCommitResponseData(throttleTimeMs=0, topics=[OffsetCommitResponseTopic(name='commit-test-1', partitions=[OffsetCommitResponsePartition(partitionIndex=0, errorCode=0)])])
16:29:08.392 [reactive-kafka-test-1-1] DEBUG org.apache.kafka.clients.consumer.internals.ConsumerCoordinator -- [Consumer clientId=consumer-test-1-1, groupId=test-1] Committed offset 2955 for partition commit-test-1-0
16:29:08.392 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onCommit: commit-test-1-0-2955
16:29:08.395 [reactive-kafka-test-1-1] ERROR TestMain -- error on consume
java.lang.RuntimeException: prefix error !!
	at TestMain.errorByPrefix(TestMain.java:51)
...&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;1161&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwUK5h/btskIQjract/xy9sL3cF6ngALjfDG7BND0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwUK5h/btskIQjract/xy9sL3cF6ngALjfDG7BND0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwUK5h/btskIQjract/xy9sL3cF6ngALjfDG7BND0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwUK5h%2FbtskIQjract%2Fxy9sL3cF6ngALjfDG7BND0%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;1161&quot; height=&quot;179&quot; data-origin-width=&quot;1161&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;size = 20&lt;/p&gt;
&lt;pre id=&quot;code_1687244863993&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...
// poll 10
08:36:58.748 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onConsume: commit-test-1-0-3800,commit-test-1-0-3801,commit-test-1-0-3802,commit-test-1-0-3803,commit-test-1-0-3804,commit-test-1-0-3805,commit-test-1-0-3806,commit-test-1-0-3807,commit-test-1-0-3808,commit-test-1-0-3809
...
// poll: 10, commit: 0 -&amp;gt; commit: 21
08:36:58.757 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onCommit: commit-test-1-0-3821
08:36:58.757 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 1-99908b38-4cce-43c9-8e1e-2bdadfc8918a
// poll: 10, commit: 21 -&amp;gt; poll: 20
08:36:58.758 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onConsume: commit-test-1-0-3810,commit-test-1-0-3811,commit-test-1-0-3812,commit-test-1-0-3813,commit-test-1-0-3814,commit-test-1-0-3815,commit-test-1-0-3816,commit-test-1-0-3817,commit-test-1-0-3818,commit-test-1-0-3819
...
// poll: 20, commit: 21 -&amp;gt; commit: 22
08:36:58.760 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onCommit: commit-test-1-0-3822
08:36:58.760 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 2-f156abbd-16b5-4f5e-a951-1936df768fff
...
08:36:58.761 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 10-46020f53-66bb-4c4e-9a8a-00d646d781d0
08:36:58.761 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 11-6000179f-5bea-44ba-86ff-c1dcb044cbb3
...
08:36:58.762 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 20-ab35889b-aa01-4b71-a47c-42389d5f3173
...
// poll: 20, commit:22 -&amp;gt; commit: 32
08:36:58.764 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onCommit: commit-test-1-0-3832
// poll: 20, commit:32 -&amp;gt; poll: 30
08:36:58.765 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onConsume: commit-test-1-0-3820,commit-test-1-0-3821,commit-test-1-0-3822,commit-test-1-0-3823,commit-test-1-0-3824,commit-test-1-0-3825,commit-test-1-0-3826,commit-test-1-0-3827,commit-test-1-0-3828,commit-test-1-0-3829
08:36:58.765 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Emitting 10 records, requested now 0
08:36:58.765 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 21-e78a711a-02d3-4615-88a4-46a9fc4e001f
...
08:36:58.765 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 30-1af38ef1-1ea5-4147-874a-465aa9a49151
...
// poll: 30, commit: 32 -&amp;gt; commit: 43
08:36:58.767 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onCommit: commit-test-1-0-3843
// poll:30, commit: 43 -&amp;gt; poll: 40
08:36:58.767 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onConsume: commit-test-1-0-3830,commit-test-1-0-3831,commit-test-1-0-3832,commit-test-1-0-3833,commit-test-1-0-3834,commit-test-1-0-3835,commit-test-1-0-3836,commit-test-1-0-3837,commit-test-1-0-3838,commit-test-1-0-3839
08:36:58.767 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Emitting 10 records, requested now 0
08:36:58.768 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 31-7a4b04d2-eb7c-4b59-a241-ca5d4055b2b4
...
08:36:58.768 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 40-3b6b0504-695f-44c3-9182-8daa1b3406c3
...
// poll: 40, commit: 43 -&amp;gt; commit: 54
08:36:58.770 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onCommit: commit-test-1-0-3854
// poll: 40, commit: 54 -&amp;gt; commit: 50
08:36:58.770 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onConsume: commit-test-1-0-3840,commit-test-1-0-3841,commit-test-1-0-3842,commit-test-1-0-3843,commit-test-1-0-3844,commit-test-1-0-3845,commit-test-1-0-3846,commit-test-1-0-3847,commit-test-1-0-3848,commit-test-1-0-3849
08:36:58.770 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Emitting 10 records, requested now 0
08:36:58.770 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- onRequest.toAdd 1, paused false
08:36:58.770 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 41-7dbe2748-ff08-4035-bd4f-c183f3e15018
...
08:36:58.771 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 50-f350dc4b-70b6-4746-b625-653103dfd6f6
...
// poll: 50, commit: 54 -&amp;gt; commit: 65
08:36:58.772 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onCommit: commit-test-1-0-3865
// poll: 50, commit: 65 -&amp;gt; poll: 60
08:36:58.773 [reactive-kafka-test-1-1] INFO LoggingCommitInterceptor -- onConsume: commit-test-1-0-3850,commit-test-1-0-3851,commit-test-1-0-3852,commit-test-1-0-3853,commit-test-1-0-3854,commit-test-1-0-3855,commit-test-1-0-3856,commit-test-1-0-3857,commit-test-1-0-3858,commit-test-1-0-3859
08:36:58.773 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- Emitting 10 records, requested now 0
08:36:58.773 [reactive-kafka-test-1-1] DEBUG reactor.kafka.receiver.internals.ConsumerEventLoop -- onRequest.toAdd 1, paused false
08:36:58.773 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 51-3aabfb11-860a-419e-b712-88fd9d268e73
08:36:58.773 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 52-0257c5c5-5d9f-4f09-a49f-7a9e71fd6bff
08:36:58.773 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 53-a28607e5-bf88-4434-b41a-273335ad257c
08:36:58.773 [reactive-kafka-test-1-1] INFO TestMain -- key: null, value: 54-9d74c852-2a00-41d4-82c8-78565b1d52fb
08:36:58.775 [reactive-kafka-test-1-1] ERROR TestMain -- error on consume
java.lang.RuntimeException: prefix error !!
	at TestMain.errorByPrefix(TestMain.java:51)
	at TestMain.lambda$subscribeAtMostOnce$4(TestMain.java:39)
...&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;1157&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJpGK1/btskIcmC2Xz/yJkX5R2371vP6fMDsqN650/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJpGK1/btskIcmC2Xz/yJkX5R2371vP6fMDsqN650/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJpGK1/btskIcmC2Xz/yJkX5R2371vP6fMDsqN650/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJpGK1%2FbtskIcmC2Xz%2FyJkX5R2371vP6fMDsqN650%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;1157&quot; height=&quot;169&quot; data-origin-width=&quot;1157&quot; data-origin-height=&quot;169&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;a href=&quot;https://github.com/apache/kafka/tree/3.4&quot;&gt;https://github.com/apache/kafka/tree/3.4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;</description>
      <category>개발 일지</category>
      <category>Kafka</category>
      <category>kafka consumer</category>
      <category>kafka offset commit</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/59</guid>
      <comments>https://p-bear.tistory.com/59#entry59comment</comments>
      <pubDate>Thu, 22 Jun 2023 18:08:42 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Consumer #3 Consume 로직</title>
      <link>https://p-bear.tistory.com/58</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CPjD9/btskDjsNjCc/jwtbbuZBykdZHlj5CeG8EK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CPjD9/btskDjsNjCc/jwtbbuZBykdZHlj5CeG8EK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CPjD9/btskDjsNjCc/jwtbbuZBykdZHlj5CeG8EK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCPjD9%2FbtskDjsNjCc%2FjwtbbuZBykdZHlj5CeG8EK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;390&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;390&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;289&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IGqDD/btskJGNJCDH/406BBc9kkqYqD3QjK82aP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IGqDD/btskJGNJCDH/406BBc9kkqYqD3QjK82aP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IGqDD/btskJGNJCDH/406BBc9kkqYqD3QjK82aP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIGqDD%2FbtskJGNJCDH%2F406BBc9kkqYqD3QjK82aP0%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;612&quot; height=&quot;289&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;289&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dJd1nN/btskCvUysqJ/8pZkzLd8K05F8xmnqoLqi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dJd1nN/btskCvUysqJ/8pZkzLd8K05F8xmnqoLqi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dJd1nN/btskCvUysqJ/8pZkzLd8K05F8xmnqoLqi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdJd1nN%2FbtskCvUysqJ%2F8pZkzLd8K05F8xmnqoLqi0%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;788&quot; height=&quot;361&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;361&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Consum 과정은 polling(fetch) + commit offset으로 볼 수 있다. &lt;br /&gt;Broker로부터 Date를 가져와서 처리 후 처리된 offset만큼 commit하는 과정이다. &lt;br /&gt;&amp;nbsp;- kafkaConsumer가 initialize 하면서 Broker로 부터 커밋된 offset 정보를 받아온다. &lt;br /&gt;&amp;nbsp;- polling을 통해 Data를 받는다. &lt;br /&gt;&amp;nbsp;- commit을 통해 offset을 올린다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Fetch Offset&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;1045&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G3Ak7/btskJisVPgx/yLOglKVkxBxVRGI1APyBrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G3Ak7/btskJisVPgx/yLOglKVkxBxVRGI1APyBrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G3Ak7/btskJisVPgx/yLOglKVkxBxVRGI1APyBrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG3Ak7%2FbtskJisVPgx%2FyLOglKVkxBxVRGI1APyBrk%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;1112&quot; height=&quot;1045&quot; data-origin-width=&quot;1112&quot; data-origin-height=&quot;1045&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. poll 호출을 받으면 우선 모든 구독된 토픽/파티션에 offset이 있는지 확인한다.&lt;br /&gt;2. 만약 하나라도 offset 정보가 없다면 resfresh offset 로직이 동작한다.&lt;br /&gt;3. ConsumerCoordinator는 OffsetFetch Reuqest를 만들고, future에 대한 handler를 등록한다.&lt;br /&gt;4. Request 객체로 ConsumerNetworkClient에게 send 요청을 한다.&lt;br /&gt;5. 그 후 ConsumerCoordinator에게 리턴하고, ConsumerCoordinator는 NetworkClient의 response가 올 때 까지 block 됩니다.&lt;br /&gt;6. networkClient는 Broker와 통신을 하고 응답을 등록된 handler를 통해 future 객체에 offset을 세팅합니다.&lt;br /&gt;7. coordinator는 future에서 offset을 받아서 구독 정보에 해당 offset 정보를 세팅하고 리턴합니다.&lt;br /&gt;8. 이후 polling 프로세스로 넘어갑니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OffsetFetchRequestData&amp;nbsp;(SCHEMA_8)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;groups&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;List&amp;lt;Group&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Each group we would like to fetch offsets for&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;group.group_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The group ID.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;group.topics&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;List&amp;lt;Topic&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Each topic we would like to fetch offsets for, or null to fetch offsets for all topics.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Topic.name&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;String&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The topic name.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Topic.partition_indexes&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;List&amp;lt;Integer&amp;gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The partition indexes we would like to fetch offsets for.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;require_stable&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Whether broker should hold on returning unstable offsets but set a retriable error code for the partitions.&lt;/span&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-1. Polling&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4VxGV/btskEVytza9/DVog0LzgLj2ptYlK45OVq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4VxGV/btskEVytza9/DVog0LzgLj2ptYlK45OVq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4VxGV/btskEVytza9/DVog0LzgLj2ptYlK45OVq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4VxGV%2FbtskEVytza9%2FDVog0LzgLj2ptYlK45OVq1%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;797&quot; height=&quot;966&quot; data-origin-width=&quot;797&quot; data-origin-height=&quot;966&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. kafkaConsumer는 poll이 호출되면, Fetcher에게 Fetch된 데이터를 달라고 한다.&lt;br /&gt;2. Fetcher는 이미 Fetch 된 데이터를 maxPollRecords 만큼 반환한다.&lt;br /&gt;&amp;nbsp; - 부족하거나 없다면 Empty 반환&lt;br /&gt;3. kafkaConsumer는 리턴된 Fetch를 확인하여, empty인 경우 Fetcher에게 Fetch(sendFetch)를 호출한다.&lt;br /&gt;4. Fetcher는 sendFetch가 호출되면, 설정 및 파티션 할당 정보로 Fetch Reuquest를 구성하여 ConsumernetworkClient의 send()를 호출&lt;br /&gt;&amp;nbsp; - Fetcher는 send()호출을 하면서, future에 success 및 fail응답에 대한 listener를 작성&lt;br /&gt;&amp;nbsp; - KafkaConsumer는 ConsumerNetworkClient에게 poll()을 호출하여, 응답을 받을 때 까지 Block&lt;br /&gt;5. ConsumerNetworkClient는 Broker에게 Fetch request 요청&lt;br /&gt;&amp;nbsp; - ConsumerNetworkClient는 요청 후 fetchRequest(network)가 실행되는 쓰레드가 block 됨&lt;br /&gt;6. Broker에게 응답을 받으면, Fetcher가 작성한 handler가 실행 됨&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2-2. Fetch&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 옵션&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Option&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;fetch.min.bytes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;fetch &lt;/span&gt;시 브로커에게 최소한&lt;span&gt; fetch.min.bytes &lt;/span&gt;값만큼 데이터를 요청&lt;span&gt;&lt;br /&gt;&lt;/span&gt;반환할 만큼 데이터가 충분하지 않다면 브로커는 데이터가 누적되길 기다린다&lt;span&gt;.&lt;br /&gt;default 1&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;&lt;a href=&quot;http://fetch.max.wait.ms&quot;&gt;fetch.max.wait.ms&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;브로커가&lt;span&gt; Fetch API &lt;/span&gt;요청을 받았을 때&lt;span&gt; fetch.min.bytes &lt;/span&gt;값만큼 데이터가 없는 경우 응답을 주기까지 최대로 기다릴 시간&lt;span&gt;&lt;br /&gt;default 500ms&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;fetch.max.bytes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Fetch API &lt;/span&gt;요청에 대해 브로커가 반환해야 하는 최대 데이터 크기이다&lt;span&gt;.&lt;br /&gt;&lt;/span&gt;다만 첫 번째 파티션의 &lt;b&gt;&lt;span&gt;첫 번째 메시지가 이 값보다 크다&lt;/span&gt;&lt;/b&gt;면 컨슈머가 계속 진행될 수 있도록 데이터가 반환된다&lt;span&gt;.&lt;br /&gt;&lt;/span&gt;브로커가 허용하는 최대 메시지 크기는&lt;span&gt; message.max.bytes&lt;/span&gt;와&lt;span&gt; max.message.bytes&lt;/span&gt;를 통해 설정한다&lt;span&gt;.&lt;br /&gt;default 52428800(50MiB)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FetchRequestData&amp;nbsp;(SCHEMA_13)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 204px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 34px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 34px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 34px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;cluster_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;COMPACT_NULLABLE_STRING&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;The clusterId if known. This is used to validate metadata fetches prior to broker registration.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;replica_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;The broker ID of the follower, of -1 if this request is from a consumer.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;max_wait_ms&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The maximum time in milliseconds to wait for the response.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;min_bytes&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The minimum bytes to accumulate in the response.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;max_bytes&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The maximum bytes to fetch. See KIP-74 for cases where this limit may not be honored.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;isolation_level&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;INT8&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;This setting controls the visibility of transactional records. Using READ_UNCOMMITTED (isolation_level = 0) makes all records visible. With READ_COMMITTED (isolation_level = 1), non-transactional and COMMITTED transactional records are visible. To be more concrete, READ_COMMITTED returns all data from offsets smaller than the current LSO (last stable offset), and enables the inclusion of the list of aborted transactions in the result, which allows consumers to discard ABORTED transactional records&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;session_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;The fetch session ID.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;session_epoch&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;The fetch session epoch&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;topics&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;[{topic_id, partitionDatas}, &amp;hellip;]&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;The topics to fetch.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;forgotten_topics_data&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;[{topic_id, partitionDatas}, &amp;hellip;]&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;In an incremental fetch request the partitions to remove.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;rack_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;COMPACT_STRING&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;Rack ID of the consumer making this request&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;partitionData.partition&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;The partition index.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;partitionData.current_leader_epoch&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;The current leader epoch of the partition.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;partitionData.fetch_offset&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;INT64&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The message offset.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;partitionData.last_fetched_epoch&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;The epoch of the last fetched record or -1 if there is none&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;partitionData.log_start_offset&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;INT64&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;The earliest available offset of the follower replica. The field is only used when the request is sent by the follower.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;width: 19.1861%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;partitionData.partition_max_bytes&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.0697%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.628%; height: 10px;&quot;&gt;&lt;span&gt;&lt;span&gt;The maximum bytes to fetch from this partition. See KIP-74 for cases where this limit may not be honored.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FetchResponseData&amp;nbsp;(SCHEMA_13)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0466%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.2326%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0466%;&quot;&gt;&lt;span&gt;&lt;span&gt;throttle_time_ms&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%;&quot;&gt;&lt;span&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.2326%;&quot;&gt;&lt;span&gt;&lt;span&gt;The duration in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0466%;&quot;&gt;&lt;span&gt;&lt;span&gt;error_code&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%;&quot;&gt;&lt;span&gt;&lt;span&gt;INT16&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.2326%;&quot;&gt;&lt;span&gt;&lt;span&gt;The top level response error code.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0466%;&quot;&gt;&lt;span&gt;&lt;span&gt;session_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%;&quot;&gt;&lt;span&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.2326%;&quot;&gt;&lt;span&gt;&lt;span&gt;The fetch session ID, or 0 if this is not part of a fetch session.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.0466%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;responses&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.7209%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;[{topic_id, partitionDatas}, &amp;hellip;]&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 70.2326%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The response topics.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;partitionData에&amp;nbsp;Records&amp;nbsp;가&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;h2 data-ke-size=&quot;size26&quot;&gt;3. Commit Offset&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;733&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cViHwN/btskDgXaxWK/5UhskEsxxC2viktHkC9vmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cViHwN/btskDgXaxWK/5UhskEsxxC2viktHkC9vmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cViHwN/btskDgXaxWK/5UhskEsxxC2viktHkC9vmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcViHwN%2FbtskDgXaxWK%2F5UhskEsxxC2viktHkC9vmK%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;980&quot; height=&quot;733&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;733&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. commit 발생 시 ConsumerCoordinator의 commitOffset()을 호출 한다.&lt;br /&gt;2. ComsumerCoordinator는 request 객체를 생성하고, response에 대한 handler를 등록한다.&lt;br /&gt;3. ComsumerCoordinaor는 ConsumerNetworkClient의 send()를 호출하고, poll()을 통해 Broker response가 올 때 까지 Block 된다.&lt;br /&gt;4. Response가 오면, handler를 통해 offset이 extract하고 interceptors의 onCommit()을 실행한다.&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SyncCommit&amp;nbsp;vs&amp;nbsp;AsyncCommit&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Sync는 실패시 기본적으로 retry정책에 따라 retry 수행&lt;br /&gt;Async는&amp;nbsp;retry&amp;nbsp;정책을&amp;nbsp;onFailure에&amp;nbsp;직접&amp;nbsp;구현해야&amp;nbsp;함&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OffsetCommitRequestData (SCHEMA_8)&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;group_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The unique group identifier.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;generation_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The generation of the group.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;member_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The member ID assigned by the group coordinator.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;group_instance_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The unique identifier of the consumer instance provided by end user.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;topics&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;List&amp;lt;Topic&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The topics to commit offsets for.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;name&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The topic name.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;partitions&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;List&amp;lt;Partitions&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Each partition to commit offsets for.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;partition_index&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The partition index.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;committed_offset&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;int&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The message offset to be committed.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;committed_leader_epoch&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The leader epoch of this partition.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;committed_metadata&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Any associated metadata the client wants to keep.&lt;/span&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.confluent.io/learn-kafka/architecture/broker/&quot;&gt;https://developer.confluent.io/learn-kafka/architecture/broker/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/0974525&quot;&gt;https://d2.naver.com/helloworld/0974525&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>consume</category>
      <category>Kafka</category>
      <category>kafka consumer</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/58</guid>
      <comments>https://p-bear.tistory.com/58#entry58comment</comments>
      <pubDate>Wed, 21 Jun 2023 18:15:41 +0900</pubDate>
    </item>
    <item>
      <title>#2 EasyCalendar Flutter 구현</title>
      <link>https://p-bear.tistory.com/57</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C2cGO/btskEWJZ7sU/FeKo7g7DrQLxewsxfkFDb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C2cGO/btskEWJZ7sU/FeKo7g7DrQLxewsxfkFDb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C2cGO/btskEWJZ7sU/FeKo7g7DrQLxewsxfkFDb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC2cGO%2FbtskEWJZ7sU%2FFeKo7g7DrQLxewsxfkFDb0%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;960&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짤막 흥보: 호구홍시 조대봉 (인스타 툰)&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Flutter로 구현한 Client Web 관련 리뷰&lt;br /&gt;개발 중 이슈사항 리뷰&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Project Structure&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;169&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfIvn5/btskAE5iTDv/uEWRW3MZi8B0dFeW5K6Yu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfIvn5/btskAE5iTDv/uEWRW3MZi8B0dFeW5K6Yu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfIvn5/btskAE5iTDv/uEWRW3MZi8B0dFeW5K6Yu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfIvn5%2FbtskAE5iTDv%2FuEWRW3MZi8B0dFeW5K6Yu1%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;169&quot; height=&quot;118&quot; data-origin-width=&quot;169&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;main: main class&lt;br /&gt;main_state_page: 시작페이지, 로그인 체크&lt;br /&gt;calendar_page: 일정 확인 / 등록 페이지&lt;br /&gt;main_exception: custom exception&lt;br /&gt;network_client: REST 요청 관련 모듈&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리뷰&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. main class를 분리하는 의미가 없는듯하다.&lt;br /&gt;2. main_state_page의 의미가 많이 퇴색되었다. Google 인증도 main_state_page에 넣었어야 했다.&lt;br /&gt;3. 구현해 보니, 각 page에는 widget 생성 관련 로직만 넣고, 비즈니스? 로직은 외부로 빼는 것이 좋을 것 같다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Library&lt;/h2&gt;
&lt;pre id=&quot;code_1687233512807&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;flex_color_scheme: 7.0.4
getwidget: 3.1.1
flutter_secure_storage: ^8.0.0
dio: 5.1.1
flutter_web_auth: 0.5.0&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리뷰&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;flex_color_scheme를 통해 스킨색에 대한 컨트롤은 어느정도 구현되었다.&lt;br /&gt;getwidget으로 UI 컴포넌트를 만들었는데, 딱히 이쁘진 않음...&lt;br /&gt;flutter_secure_storage는 토큰정보를 저장하기 위해 도입했으며, 적절&lt;br /&gt;dio는 REST 요청 등 활용되는 Library인데, 기본 모듈보다 좋은 점은 찾지 못함&lt;br /&gt;flutter_web_auth는 OAuth2.0 Client를 구성하기 위해서 도입했으며, 가장 간단하게 client 연동을 구현했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이슈&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. OAuth2.0 Client Library 선정&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Flutter는 기본적으로 Native App을 위한 크로스 플랫폼 프레임워크이다.&lt;br /&gt;많은 라이브러리가 App을 위해 개발되었으며, Web은 아니다.&lt;br /&gt;OAuth2.0 Library 또한 대부분 Web을 제대로 지원하지 않았다. (WebView가 Web에 없기 때문)&lt;br /&gt;선정된 라이브러리는 Web에서 사용가능한 우회방식을 제시해준다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 예&lt;/p&gt;
&lt;pre id=&quot;code_1687234143814&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// API 사용
Future&amp;lt;String&amp;gt; getMainAuthorizationToken() async {
    return await FlutterWebAuth.authenticate(
        url: '$_loginPageUrl?$_loginPageQueryParams',
        callbackUrlScheme: _callbackUrlScheme);
}

// redirect 된 access_token parameter 추출
var accessToken = Uri.parse(result).queryParameters['access_token'];&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redirect 경로에 auth.html 추가&lt;/p&gt;
&lt;pre id=&quot;code_1687234190679&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;kr&quot;&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Authentication complete&amp;lt;/title&amp;gt;
    &amp;lt;p&amp;gt;Authentication is complete. If this does not happen automatically, please close the window.&amp;lt;/p&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;script&amp;gt;
    window.opener.postMessage({
        'flutter-web-auth': window.location.href
    }, window.location.origin);
    window.close();
&amp;lt;/script&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;redirect 되면 auth.html로 오게 만들고, 해당 페이지에서 기존 페이지로 돌아오게 하며, 라이브러리 사용법에 따라서 parameter를 받아서 사용한다.&lt;/blockquote&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;2. Dynamic Widget 생성&lt;/h3&gt;
&lt;pre id=&quot;code_1687234418879&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Future&amp;lt;List&amp;lt;Widget&amp;gt;&amp;gt; _createCalendarEvent() async {
    _selectedCalendarId = await networkClient.getCalendarId();
    List&amp;lt;dynamic&amp;gt; res = await networkClient.getCalendarEvents(_selectedCalendarId);
    res.sort((a, b) =&amp;gt; _compareStartTime(a, b));
    final targetEvents = res.sublist(0, 4);
    for (Map&amp;lt;String, dynamic&amp;gt; event in targetEvents) {
      Map&amp;lt;String, String&amp;gt; start = Map.from(event[&quot;start&quot;]);
      _addCalendarEvent(event[&quot;id&quot;], start[&quot;dateTime&quot;]!, event[&quot;summary&quot;]);
    }

    return eventList.values.toList();
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;위와 같이 Future로 Widget이 생성되는 경우, Widget 생성 또한 이에 대응해야 한다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1687234706654&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FutureBuilder(
  future: _createCalendarEvent(),
  initialData: const [Text(&quot;data loading 중&quot;)],
  builder: (context, snapshot) {
    return Column(
      key: UniqueKey(),
      mainAxisAlignment: MainAxisAlignment.start,
      children: snapshot.requireData,
    );
  },
),&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;FutureBuilder를 통해 Future를 받을 때 Widget을 구성해주는 Widget이 있다.&lt;/blockquote&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;3. Web build with prefix&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;배포되는 환경에서는, url에 prefix가 붙어야 했다.&lt;br /&gt;flutter navigate시 prefix가 붙어서 할 수 있도록 build 시 args를 추가했다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1687235135065&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;flutter build web --base-href &quot;/easycal/&quot;&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;h2 data-ke-size=&quot;size26&quot;&gt;총평&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;첫 Flutter 프로젝트여서, 기능구현에 초점을 맞췄다.&lt;br /&gt;내부 구현 구조가 대규모로 변경되는 사항이 많아서, 프로젝트는 유지보수에 어려움이 있을 것으로 예상된다.&lt;br /&gt;특히 계산로직과 widget 생성 로직 등이 뒤섞여있는 부분이 안타깝다.&lt;br /&gt;front 단 코딩에 쫌 더 생각을 해보고 진행해야겠다.&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;</description>
      <category>[완] 개인서버 개발/EasyCalendar(완)</category>
      <category>Flutter</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/57</guid>
      <comments>https://p-bear.tistory.com/57#entry57comment</comments>
      <pubDate>Tue, 20 Jun 2023 18:31:30 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Consumer #2 Partition Assignor Strategy</title>
      <link>https://p-bear.tistory.com/56</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BE6GJ/btsjH2spUwJ/CIpT7MYyif828ycHHWFfm1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BE6GJ/btsjH2spUwJ/CIpT7MYyif828ycHHWFfm1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BE6GJ/btsjH2spUwJ/CIpT7MYyif828ycHHWFfm1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBE6GJ%2FbtsjH2spUwJ%2FCIpT7MYyif828ycHHWFfm1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;390&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;390&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;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;Kafka Consumer에 대한 세부 조사&lt;br /&gt;#1 글에 이어서&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Consumer&amp;nbsp;Partition&amp;nbsp;Assignor &lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;288&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAznWF/btsjQjslN92/fu1tikf7kWgOFcVdES0E31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAznWF/btsjQjslN92/fu1tikf7kWgOFcVdES0E31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAznWF/btsjQjslN92/fu1tikf7kWgOFcVdES0E31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAznWF%2FbtsjQjslN92%2Ffu1tikf7kWgOFcVdES0E31%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;820&quot; height=&quot;288&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;288&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;파티션을 할당하기 위한 Interface&lt;br /&gt;consumer 설정의 partition.assingment.strategy 로 구현체를 세팅할 수 있다.&lt;br /&gt;ConsumerGroup의 정보와, Topic의 Partition 정보를 받아서, 배분한다.&lt;br /&gt;5개 정도의 구현체가 있다.&lt;br /&gt;Custom한 전략도 구현 가능하다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Consumer Rebalancing 전략은 Broker의 Coordinator가 아닌 Consumer들이 정한다.&lt;br /&gt;따라서 Rebalance에서 발생하는 모든 오퍼레이션은 Consumer (Leader)가 관장하여, coordinator는 leader가 오퍼레이션 하기 위한 브로커 역할을 한다.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&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;Range Assignor&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;661&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdleK3/btsjOoapUYE/HmMZ6YAFGCShTsRT5XKPN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdleK3/btsjOoapUYE/HmMZ6YAFGCShTsRT5XKPN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdleK3/btsjOoapUYE/HmMZ6YAFGCShTsRT5XKPN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdleK3%2FbtsjOoapUYE%2FHmMZ6YAFGCShTsRT5XKPN1%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;671&quot; height=&quot;661&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;661&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;기본 partition Assignor&lt;br /&gt;파티션과 컨슈머를 사전순으로 정렬하고 파티션을 컨슈머에게 순차적으로 할당하는 방식&lt;br /&gt;토픽별로 배분 한다.&lt;/blockquote&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;RoundRobin Assignor&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;661&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJpmYf/btsjO43Ooeh/V5t4YY3Dkv1mcIQLQuHoXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJpmYf/btsjO43Ooeh/V5t4YY3Dkv1mcIQLQuHoXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJpmYf/btsjO43Ooeh/V5t4YY3Dkv1mcIQLQuHoXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJpmYf%2FbtsjO43Ooeh%2FV5t4YY3Dkv1mcIQLQuHoXK%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;671&quot; height=&quot;661&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;661&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;파티션을 전체적으로 sort하고 컨슈머를 번갈아가면서 매핑&lt;br /&gt;Reassign 후 Consumer &amp;harr;︎ Partition 간의 매핑이 달라질 수 있다.&lt;br /&gt;Consumer Group내 Consumer들이 같은 토픽들을 subscribe 할 때 효율적&lt;/blockquote&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;Sticky Assignor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 배분 로직은 RoundRobin과 같음&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;init:&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;661&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cHAEi6/btsjH1NOkDC/d6SaL4VoItFG5mqclDDXbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cHAEi6/btsjH1NOkDC/d6SaL4VoItFG5mqclDDXbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cHAEi6/btsjH1NOkDC/d6SaL4VoItFG5mqclDDXbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcHAEi6%2FbtsjH1NOkDC%2Fd6SaL4VoItFG5mqclDDXbk%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;671&quot; height=&quot;661&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;661&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RoundRobin&amp;nbsp;After&amp;nbsp;Consumer1&amp;nbsp;Dead&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;661&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXhZBm/btsjOnP9ce6/zCUFkH3m3zSrWf3JiJQnd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXhZBm/btsjOnP9ce6/zCUFkH3m3zSrWf3JiJQnd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXhZBm/btsjOnP9ce6/zCUFkH3m3zSrWf3JiJQnd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXhZBm%2FbtsjOnP9ce6%2FzCUFkH3m3zSrWf3JiJQnd1%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;671&quot; height=&quot;661&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;661&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sticky After Consumer1 Dead &lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;661&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCSK7Z/btsjHIuh2L7/Czb44FMS1q1gtWyKSqRl41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCSK7Z/btsjHIuh2L7/Czb44FMS1q1gtWyKSqRl41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCSK7Z/btsjHIuh2L7/Czb44FMS1q1gtWyKSqRl41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCSK7Z%2FbtsjHIuh2L7%2FCzb44FMS1q1gtWyKSqRl41%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;671&quot; height=&quot;661&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;661&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;최대한 균등한 할당을 보장 (Consumer의 Partition 수의 차이는 최대 1)&lt;br /&gt;가본적으로 Round Robin&lt;br /&gt;&amp;rarr; A +2 &amp;lt;= B 인경우 A의 Topic의 남은 Partition은 B에 할당 불가&lt;br /&gt;Reassign시 최대한 기존 할당을 유지&lt;br /&gt;우선순위: 1. 균등 2. 기존 유지&lt;/blockquote&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;CooperativeStickyAssignor&amp;nbsp;(Kafka&amp;nbsp;ver&amp;nbsp;&amp;gt;=&amp;nbsp;2.4,&amp;nbsp;stable&amp;nbsp;in&amp;nbsp;2.5)&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;동작은 StickyAssignor와 동일하지만&lt;br /&gt;위 3가지에 사용되는 EAGER rebalance을 사용 안함.&lt;br /&gt;목적은 Consumer의 다운타임 최소화&lt;br /&gt;STW 관련 설명은 후술&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;EAGER&amp;nbsp;rebalance&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. Consumer의 변동 감지&lt;br /&gt;2. 모든 파티션의 할당을 취소한다.&lt;br /&gt;3. 파티션 재할당 전략을 통해 재 할당&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Cooperative&amp;nbsp;reablance&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. Consumer의 변동 감지 &lt;br /&gt;2. 모든 Comsumer는 자신의 구독정보 및 선호도(이전 할당 이력 등)를 그룹 코디네이터로 전송 &lt;br /&gt;3. 그룹 코디네이터는 정보를 조합해 consumer 리더에게 전송 &lt;br /&gt;4. 리더는 현 파티션 정보를 고려해 할당 정보를 생성한다. &lt;br /&gt;5. 재할당 전략으로 불필요한 재할당 (Sticky) 및 선호도 (이전 할당 이력)을 최대한 반영&lt;/blockquote&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;StreamsPartitionAssignor&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Kafka Streams 에서 사용하는 할당 전략&lt;br /&gt;자세한 사항은 추후 조사&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Rebalance와&amp;nbsp;STW &lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본&amp;nbsp;Reablance&amp;nbsp;상황의&amp;nbsp;STW &lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1771&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Xtynl/btsjJWea4Gv/HdNJVs2wuWIxd4Vyn4ugf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Xtynl/btsjJWea4Gv/HdNJVs2wuWIxd4Vyn4ugf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Xtynl/btsjJWea4Gv/HdNJVs2wuWIxd4Vyn4ugf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXtynl%2FbtsjJWea4Gv%2FHdNJVs2wuWIxd4Vyn4ugf1%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;1771&quot; height=&quot;638&quot; data-origin-width=&quot;1771&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Consumer 3가 join 하는 상황이다.&lt;br /&gt;1. consumer &amp;rarr; coordinator: 참가 요청&lt;br /&gt;2. coordinator &amp;rarr; 기존 consumers: heartbeat 또는 offsetFetch(consume) 의 response로 rebalance 시작을 noti&lt;br /&gt;3. rebalance를 수신받은 consumer1, 2 는 STW&lt;br /&gt;4. consumers &amp;rarr; coordinator: joinGroupRequest&lt;br /&gt;5. coordinator &amp;rarr; consumers: leader에게 전체 정보, 각 컨슈머에게 memberId 정보를 response로 전달&lt;br /&gt;6. consumer &amp;harr;︎ coordinator: 지속적으로 Sync Group을 진행하면서, rebalancing 완료를 기다림&lt;br /&gt;7. Rebalance 종료 후 STW 해제&lt;/blockquote&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;Rebalance&amp;nbsp;불합리 &lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;위 상황에서 Consumer 1,2의 p0, p1는 rebalance 전후로 변화가 없다.&lt;br /&gt;따라서 p0의 경우 STW에서 제외하는 것이 효율적이다.&lt;br /&gt;또한 Range, RoundRobin Assignor에서는 p0의 state 또한 재구성하는 오버헤드가 있다.&lt;/blockquote&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;CooperativeStickyAssignor를&amp;nbsp;통한&amp;nbsp;STW&amp;nbsp;최소화 &lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Step 1&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1625&quot; data-origin-height=&quot;877&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYLmby/btsjNKxKLNQ/46DTZhex0bxD8RYso6ovy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYLmby/btsjNKxKLNQ/46DTZhex0bxD8RYso6ovy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYLmby/btsjNKxKLNQ/46DTZhex0bxD8RYso6ovy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYLmby%2FbtsjNKxKLNQ%2F46DTZhex0bxD8RYso6ovy1%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;1625&quot; height=&quot;877&quot; data-origin-width=&quot;1625&quot; data-origin-height=&quot;877&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;신규 Consumer 3가 Join하는 상황이다.&lt;br /&gt;기존 Consumer 1,2는 join &amp;rarr; sync를 수행하지만, STW는 하지 않는다.&lt;br /&gt;Sync를 통해서 자신이 제외해야할 파티션을 결정한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Step 2&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;873&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FsyHD/btsjQfQ1GcP/GuipNkqQk1CawBapg58HC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FsyHD/btsjQfQ1GcP/GuipNkqQk1CawBapg58HC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FsyHD/btsjQfQ1GcP/GuipNkqQk1CawBapg58HC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFsyHD%2FbtsjQfQ1GcP%2FGuipNkqQk1CawBapg58HC0%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;1774&quot; height=&quot;873&quot; data-origin-width=&quot;1774&quot; data-origin-height=&quot;873&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Step 1에서 결정된 제외할 파티션을 revoke 한다.&lt;br /&gt;다시 Join, Sync 프로세스를 통해 Consumer3가 p2를 할당 받으면서 합류합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;a href=&quot;https://kafka.apache.org/documentation/#majordesignelements&quot;&gt;https://kafka.apache.org/documentation/#majordesignelements&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;a href=&quot;https://kafka.apache.org/34/javadoc/org/apache/kafka/clients/consumer/package-summary.html&quot;&gt;https://kafka.apache.org/34/javadoc/org/apache/kafka/clients/consumer/package-summary.html&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;a href=&quot;https://velog.io/@hyun6ik/Apache-Kafka-Partition-Assignment-Strategy&quot;&gt;https://velog.io/@hyun6ik/Apache-Kafka-Partition-Assignment-Strategy&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;a href=&quot;https://developer.confluent.io/learn-kafka/architecture/consumer-group-protocol/&quot;&gt;https://developer.confluent.io/learn-kafka/architecture/consumer-group-protocol/&lt;/a&gt;&lt;/li&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;a href=&quot;https://github.com/apache/kafka/tree/3.4&quot;&gt;https://github.com/apache/kafka/tree/3.4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>cooperative sticky assignor</category>
      <category>Kafka</category>
      <category>kafka consumer</category>
      <category>partition assignor</category>
      <category>range assignor</category>
      <category>round robin assignor</category>
      <category>sticky assignor</category>
      <category>stw</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/56</guid>
      <comments>https://p-bear.tistory.com/56#entry56comment</comments>
      <pubDate>Thu, 15 Jun 2023 19:37:01 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Consumer #1 Startup / Rebalance Flow</title>
      <link>https://p-bear.tistory.com/55</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgIFTv/btsjQgPNEfo/8AkIYlCssFKDlY6ynIKlUK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgIFTv/btsjQgPNEfo/8AkIYlCssFKDlY6ynIKlUK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgIFTv/btsjQgPNEfo/8AkIYlCssFKDlY6ynIKlUK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgIFTv%2FbtsjQgPNEfo%2F8AkIYlCssFKDlY6ynIKlUK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;390&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;390&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Kafka Consumer에 대한 세부 조사&lt;br /&gt;Kafka Client와 Kafka Broker의 rebalance flow 및 requset / response data 조사&lt;br /&gt;Kafka Consumer Assignor 관련 조사&lt;br /&gt;Kafka Consumer STW 관련 조사&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Consumer&amp;nbsp;Startup&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Group&amp;nbsp;Coordinator&amp;nbsp;란&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1437&quot; data-origin-height=&quot;884&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yzjbA/btsjPr5oosr/GKAK7W0kmHMe7DRHXgoKZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yzjbA/btsjPr5oosr/GKAK7W0kmHMe7DRHXgoKZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yzjbA/btsjPr5oosr/GKAK7W0kmHMe7DRHXgoKZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyzjbA%2FbtsjPr5oosr%2FGKAK7W0kmHMe7DRHXgoKZk%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;1437&quot; height=&quot;884&quot; data-origin-width=&quot;1437&quot; data-origin-height=&quot;884&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Kafka Broker에서 Consumer Group을 관리하기 위한 컴포넌트&lt;br /&gt;기본적으로 Consumer Group 별로 존재&lt;br /&gt;Cluster내 임의의 broker에 생성 됨&lt;br /&gt;Consumer Group내 member의 변동을 감지하고 Rebalance를 관장&lt;br /&gt;kafka topic을 이용해서 group의 metadata를 유지&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;총 3가지 단계로 coordinator와 consumer간의 협상이 일어난다.&lt;br /&gt;&amp;nbsp;1. start group: coordinator를 찾는 단계&lt;br /&gt;&amp;nbsp;2. join group: coordinator를 통해 consumer가 group에 join 하는 단계&lt;br /&gt;&amp;nbsp;3. sync group: join된 group에서 토픽 / 파티션을 sync하는 단계&lt;/blockquote&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;1.&amp;nbsp;Start&amp;nbsp;Group&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;836&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdikkp/btsjIohUh1V/OqsfgwjtKutKVBwoZmjUu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdikkp/btsjIohUh1V/OqsfgwjtKutKVBwoZmjUu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdikkp/btsjIohUh1V/OqsfgwjtKutKVBwoZmjUu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbdikkp%2FbtsjIohUh1V%2FOqsfgwjtKutKVBwoZmjUu0%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;1348&quot; height=&quot;836&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;836&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;일반적으로 Consumer가 Startup 할 시 group.id 를 기반으로 findCoordinator 요청을 한다. &lt;br /&gt;broker는 group.id를 기반으로 group coordinator를 생성할 브로커를 선정 및 생성하고 해당 coordinator의 endpoint를 응답&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;findCoordinatorRequestData (SCHEMA_4)&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.7907%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.2326%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 61.8605%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.7907%;&quot;&gt;&lt;span&gt;&lt;span&gt;key_type&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.2326%;&quot;&gt;&lt;span&gt;&lt;span&gt;INT8&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 61.8605%;&quot;&gt;&lt;span&gt;&lt;span&gt;The coordinator key type. (Group, transaction, etc.)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.7907%;&quot;&gt;&lt;span&gt;&lt;span&gt;coordinator_keys&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.2326%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 61.8605%;&quot;&gt;&lt;span&gt;&lt;span&gt;The coordinator keys.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;findCoordinatorResponseData (SCHEMA_4)&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.99998%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 62.2092%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;&lt;span&gt;&lt;span&gt;throttle_time_ms&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.99998%;&quot;&gt;&lt;span&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 62.2092%;&quot;&gt;&lt;span&gt;&lt;span&gt;The duration in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;&lt;span&gt;&lt;span&gt;coordinators&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.99998%;&quot;&gt;&lt;span&gt;&lt;span&gt;list&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 62.2092%;&quot;&gt;&lt;span&gt;&lt;span&gt;Each coordinator result in the response&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;coordinatorData.key&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.99998%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;String&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 62.2092%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The coordinator key.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;coordinatorData.node_id&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.99998%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 62.2092%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The node id.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;coordinatorData.host&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.99998%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;String&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 62.2092%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The host name.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;coordinatorData.port&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.99998%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;INT32&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 62.2092%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The port.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;&lt;span&gt;&lt;span&gt;coordinatorData.error_code&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.99998%;&quot;&gt;&lt;span&gt;&lt;span&gt;INT16&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 62.2092%;&quot;&gt;&lt;span&gt;&lt;span&gt;The error code, or 0 if there was no error.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 27.6744%;&quot;&gt;&lt;span&gt;&lt;span&gt;coordinatorData.error_message&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.99998%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 62.2092%;&quot;&gt;&lt;span&gt;&lt;span&gt;The error message, or null if there was no error.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;2.&amp;nbsp;Join&amp;nbsp;Group&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1817&quot; data-origin-height=&quot;1066&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/so2i9/btsjHdgBRaD/KmxAC3zDQX0b9MP0tsMcu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/so2i9/btsjHdgBRaD/KmxAC3zDQX0b9MP0tsMcu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/so2i9/btsjHdgBRaD/KmxAC3zDQX0b9MP0tsMcu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fso2i9%2FbtsjHdgBRaD%2FKmxAC3zDQX0b9MP0tsMcu0%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;1817&quot; height=&quot;1066&quot; data-origin-width=&quot;1817&quot; data-origin-height=&quot;1066&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;consumer들은 group coordinator로 join 요청 (구독 정보 포함)&lt;br /&gt;group coordinator는 그룹에서 최초로 join 요청을 보낸 consumer를 leader로 선정&lt;br /&gt;(최초 생성 시) Consumer Group의 offset 정보 등 group 관련 데이터 생성&lt;br /&gt;consumer에게 memberId를 응답, leader에게는 memberList 및 subscription 정보 전달&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;JoinGroupRequestData (SCHEMA_8)&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;group_id&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;String&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The group identifier.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;session_timeout_ms&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The coordinator considers the consumer dead if it receives no heartbeat after this timeout in milliseconds.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;rebalance_timeout_ms&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The maximum time in milliseconds that the coordinator will wait for each member to rejoin when rebalancing the group.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;member_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The member id assigned by the group coordinator.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;group_instance_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The unique identifier of the consumer instance provided by end user.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;protocol_type&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The unique name the for class of protocols implemented by the group we want to join.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;(consumer, connect)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;reason&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The reason why the member (re-)joins the group.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;protocols&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;span&gt;list&amp;lt;Protocol&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The list of protocols that the member supports. (Partition Assignment)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;protocol.name&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;String&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The protocol name.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;(Assigner Name)&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 20.814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;protocol.metadata&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.4884%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;bytes&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 65.5814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The protocol metadata.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;(Subscription &lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;객체를&lt;span&gt; serialize)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;JoinGroupResponseData (SCHEMA_9)&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;span&gt;throttle_time_ms&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The duration in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;span&gt;error_code&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The error code, or 0 if there was no error.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;span&gt;generation_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The generation ID of the group.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;span&gt;protocol_type&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The group protocol name.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;span&gt;protocol_name&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The group protocol selected by the coordinator.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;span&gt;leader&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The leader of the group.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;span&gt;skip_assignment&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;True if the leader must skip running the assignment.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;span&gt;member_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;span&gt;The member ID assigned by the group coordinator.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;members&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;List&amp;lt;Member&amp;gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;member.member_id&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;String&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The group member ID.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;member.group_instance_id&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;String&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The unique identifier of the consumer instance provided by end user.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.6512%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;member.metadata&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;bytes&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 60.814%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The group member metadata.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;span&gt;3.&amp;nbsp;SyncGroup&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1817&quot; data-origin-height=&quot;1068&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nKr2f/btsjOpAnotF/daMjiOkK3s9OjHTU6VN4HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nKr2f/btsjOpAnotF/daMjiOkK3s9OjHTU6VN4HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nKr2f/btsjOpAnotF/daMjiOkK3s9OjHTU6VN4HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnKr2f%2FbtsjOpAnotF%2FdaMjiOkK3s9OjHTU6VN4HK%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;1817&quot; height=&quot;1068&quot; data-origin-width=&quot;1817&quot; data-origin-height=&quot;1068&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Consumer Group Leader는 구독할 파티션에 대한 전략을 수행&lt;br /&gt;각 consumer member가 구독할 파티션 정보를 coordinator로 전달&lt;br /&gt;다른 consumer들은 coordinator로 지속적으로 자신이 구독할 파티션에 대한 sync 요청을 보냄&lt;br /&gt;consumer partition assign 전략은 다음편에 자세히 설명&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;SyncGroupRequestData (SCHEMA_5)&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;group_id&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;String&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The unique group identifier.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;generation_id&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;int&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The generation of the group.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;member_id&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;String&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The member ID assigned by the group.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;group_instance_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The unique identifier of the consumer instance provided by end user.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;protocol_type&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The group protocol type.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;protocol_name&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The group protocol name.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;assignments&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;List&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Each assignment.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;각&lt;span&gt; member &lt;/span&gt;별&lt;span&gt; assign &lt;/span&gt;된 결과 리스트&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;assignments.member_id&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;Strring&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;span&gt;The ID of the member to assign.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;assignments.assignment&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;bytes&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The member assignment.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;(assignment &lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;결과를&lt;span&gt; serialize)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;SyncGroupResponseData (SCHEMA_5)&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2326%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;name&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.18607%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;type&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 75.4651%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;Desc&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2326%;&quot;&gt;&lt;span&gt;&lt;span&gt;throttle_time_ms&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.18607%;&quot;&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 75.4651%;&quot;&gt;&lt;span&gt;&lt;span&gt;The duration in milliseconds for which the request was throttled due to a quota violation, or zero if the request did not violate any quota.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2326%;&quot;&gt;&lt;span&gt;&lt;span&gt;error_code&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.18607%;&quot;&gt;&lt;span&gt;&lt;span&gt;int&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 75.4651%;&quot;&gt;&lt;span&gt;&lt;span&gt;The error code, or 0 if there was no error.&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2326%;&quot;&gt;&lt;span&gt;&lt;span&gt;protocol_type&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.18607%;&quot;&gt;&lt;span&gt;&lt;span&gt;String&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 75.4651%;&quot;&gt;&lt;span&gt;&lt;span&gt;The group protocol type.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;span&gt;(consumer, connect)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2326%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;protocol_name&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.18607%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;String&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 75.4651%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The group protocol name.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;= assignment.getName()&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.2326%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;assignment&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.18607%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;bytes&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 75.4651%;&quot;&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;The member assignment.&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Consumer Rebalance&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rebalancing 조건&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 인스턴스가 timeout안에 heartbeat 실패하여 group에서 제외 되었을 때&lt;br /&gt;2. 인스턴스가 group에 추가되었을 때&lt;br /&gt;3. 구독중인 토픽에 파티션이 추가 되었을 때&lt;br /&gt;4. *로 topic을 subscribe 중에, 조건에 맞는 topic이 추가되었을 때&lt;br /&gt;5. consumer group이 시작 될 때&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rebalance&amp;nbsp;noti&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uLD99/btsjHInvMbo/HH2v56iudpWKfeiMnkmcS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uLD99/btsjHInvMbo/HH2v56iudpWKfeiMnkmcS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uLD99/btsjHInvMbo/HH2v56iudpWKfeiMnkmcS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuLD99%2FbtsjHInvMbo%2FHH2v56iudpWKfeiMnkmcS0%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;1394&quot; height=&quot;876&quot; data-origin-width=&quot;1394&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;heartbeat response 또는 offsetFetch response를 통해 consumer에게 notify한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kafka.apache.org/documentation/#majordesignelements&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://kafka.apache.org/documentation/#majordesignelements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kafka.apache.org/34/javadoc/org/apache/kafka/clients/consumer/package-summary.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://kafka.apache.org/34/javadoc/org/apache/kafka/clients/consumer/package-summary.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://velog.io/@hyun6ik/Apache-Kafka-Partition-Assignment-Strategy&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@hyun6ik/Apache-Kafka-Partition-Assignment-Strategy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.confluent.io/learn-kafka/architecture/consumer-group-protocol/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.confluent.io/learn-kafka/architecture/consumer-group-protocol/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/tree/3.4&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/apache/kafka/tree/3.4&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;</description>
      <category>개발 일지</category>
      <category>Kafka</category>
      <category>kafka consumer</category>
      <category>rebalance</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/55</guid>
      <comments>https://p-bear.tistory.com/55#entry55comment</comments>
      <pubDate>Wed, 14 Jun 2023 19:22:47 +0900</pubDate>
    </item>
    <item>
      <title>Expand Kafka Cluster</title>
      <link>https://p-bear.tistory.com/54</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NTc1y/btsjPJR9W83/RtCkWcHPPdQwgiP8B9mPF0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NTc1y/btsjPJR9W83/RtCkWcHPPdQwgiP8B9mPF0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NTc1y/btsjPJR9W83/RtCkWcHPPdQwgiP8B9mPF0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNTc1y%2FbtsjPJR9W83%2FRtCkWcHPPdQwgiP8B9mPF0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;390&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;390&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Kafka Cluster를 확장할 시 발생하는 이펙트에 대한 조사&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Broker&amp;nbsp;Data&amp;nbsp;Reassignment&amp;nbsp;Strategy&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단순&amp;nbsp;Broker&amp;nbsp;Add&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본적으로 Broker가 추가되는 경우 기존 topic 및 Data에 대해서는 아무런 작업을 진행하지 않는다. 앞으로 추가되는 topic에 대해서 신규 브로커가 포함된 형태로 Cluster가 동작한다.&lt;/blockquote&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;Migrate&amp;nbsp;data&amp;nbsp;to&amp;nbsp;new&amp;nbsp;Broker&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;수동적으로 트리거링하여 기존 데이터를 새 Broker에 마이그레이션 할 수 있다. &lt;br /&gt;시스템 중단 없이 작업 가능한 방법 &lt;br /&gt;내부적으로 다음 프로세스를 적용하여 마이그레이션이 진행됩니다. &lt;br /&gt;&amp;nbsp;1. 신규 Broker를 기존 파티션의 follower로 지정 &lt;br /&gt;&amp;nbsp;2. 신규 Broker는 기존 파티션의 모든 Data를 replicate &lt;br /&gt;&amp;nbsp;3. replication 완료 후 신규 브로커는 해당 topic의 ISR (In-Sync-Replica)로 편입 &lt;br /&gt;&amp;nbsp;3. ISR중 누군가는 해당 topic의 replication을 delete&lt;/blockquote&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;Data&amp;nbsp;Migration&amp;nbsp;Tool&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;파티션재할당을 위해서는 Reassignment Tool을 활용 (kafka-reassign-partitions.sh) &lt;br /&gt;모든 작업은 관리자가 수동으로 진행 &lt;br /&gt;Tool의 3가지 옵션 (단계) &lt;br /&gt;&amp;nbsp;1. --generate: Topic과 Broker list를 지정하여, target Broker들에 Topic들을 재할당 합니다. &lt;br /&gt;&amp;nbsp;2. --execute: reassignment-json-file을 통해 계획에 따라 재할당 진행 &lt;br /&gt;&amp;nbsp;3. --verify: execute의 진행 상황을 확인 &lt;br /&gt;generate를 통해 토픽과 브로커의 리스트를 지정하면, kafka tool이 execute plan을 생성합니다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;reassign&amp;nbsp;topics&amp;nbsp;선정&lt;/h4&gt;
&lt;pre id=&quot;code_1686636696958&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt; cat topics-to-move.json
{&quot;topics&quot;: [{&quot;topic&quot;: &quot;foo1&quot;},
            {&quot;topic&quot;: &quot;foo2&quot;}],
&quot;version&quot;:1
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;reassign&amp;nbsp;계획&amp;nbsp;생성&lt;/h4&gt;
&lt;pre id=&quot;code_1686636718553&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt; bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 --topics-to-move-json-file topics-to-move.json --broker-list &quot;5,6&quot; --generate
Current partition replica assignment

{&quot;version&quot;:1,
&quot;partitions&quot;:[{&quot;topic&quot;:&quot;foo1&quot;,&quot;partition&quot;:0,&quot;replicas&quot;:[2,1]},
              {&quot;topic&quot;:&quot;foo1&quot;,&quot;partition&quot;:1,&quot;replicas&quot;:[1,3]},
              {&quot;topic&quot;:&quot;foo1&quot;,&quot;partition&quot;:2,&quot;replicas&quot;:[3,4]},
              {&quot;topic&quot;:&quot;foo2&quot;,&quot;partition&quot;:0,&quot;replicas&quot;:[4,2]},
              {&quot;topic&quot;:&quot;foo2&quot;,&quot;partition&quot;:1,&quot;replicas&quot;:[2,1]},
              {&quot;topic&quot;:&quot;foo2&quot;,&quot;partition&quot;:2,&quot;replicas&quot;:[1,3]}]
}

Proposed partition reassignment configuration

{&quot;version&quot;:1,
&quot;partitions&quot;:[{&quot;topic&quot;:&quot;foo1&quot;,&quot;partition&quot;:0,&quot;replicas&quot;:[6,5]},
              {&quot;topic&quot;:&quot;foo1&quot;,&quot;partition&quot;:1,&quot;replicas&quot;:[5,6]},
              {&quot;topic&quot;:&quot;foo1&quot;,&quot;partition&quot;:2,&quot;replicas&quot;:[6,5]},
              {&quot;topic&quot;:&quot;foo2&quot;,&quot;partition&quot;:0,&quot;replicas&quot;:[5,6]},
              {&quot;topic&quot;:&quot;foo2&quot;,&quot;partition&quot;:1,&quot;replicas&quot;:[6,5]},
              {&quot;topic&quot;:&quot;foo2&quot;,&quot;partition&quot;:2,&quot;replicas&quot;:[5,6]}]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;execute&lt;/h4&gt;
&lt;pre id=&quot;code_1686636741430&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt; bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 --reassignment-json-file expand-cluster-reassignment.json --execute
Current partition replica assignment

{&quot;version&quot;:1,
&quot;partitions&quot;:[{&quot;topic&quot;:&quot;foo1&quot;,&quot;partition&quot;:0,&quot;replicas&quot;:[2,1]},
              {&quot;topic&quot;:&quot;foo1&quot;,&quot;partition&quot;:1,&quot;replicas&quot;:[1,3]},
              {&quot;topic&quot;:&quot;foo1&quot;,&quot;partition&quot;:2,&quot;replicas&quot;:[3,4]},
              {&quot;topic&quot;:&quot;foo2&quot;,&quot;partition&quot;:0,&quot;replicas&quot;:[4,2]},
              {&quot;topic&quot;:&quot;foo2&quot;,&quot;partition&quot;:1,&quot;replicas&quot;:[2,1]},
              {&quot;topic&quot;:&quot;foo2&quot;,&quot;partition&quot;:2,&quot;replicas&quot;:[1,3]}]
}

Save this to use as the --reassignment-json-file option during rollback
Successfully started partition reassignments for foo1-0,foo1-1,foo1-2,foo2-0,foo2-1,foo2-2&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;verify&lt;/h4&gt;
&lt;pre id=&quot;code_1686636759142&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;gt; bin/kafka-reassign-partitions.sh --bootstrap-server localhost:9092 --reassignment-json-file expand-cluster-reassignment.json --verify
Status of partition reassignment:
Reassignment of partition [foo1,0] is completed
Reassignment of partition [foo1,1] is still in progress
Reassignment of partition [foo1,2] is still in progress
Reassignment of partition [foo2,0] is completed
Reassignment of partition [foo2,1] is completed
Reassignment of partition [foo2,2] is completed&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- &lt;a href=&quot;https://kafka.apache.org/documentation/#basic_ops_cluster_expansion&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://kafka.apache.org/documentation/#basic_ops_cluster_expansion&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>data migration tool</category>
      <category>Kafka</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/54</guid>
      <comments>https://p-bear.tistory.com/54#entry54comment</comments>
      <pubDate>Tue, 13 Jun 2023 19:12:59 +0900</pubDate>
    </item>
    <item>
      <title>Webclient 동작 원리 및 Configuration</title>
      <link>https://p-bear.tistory.com/53</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;536&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CLgdZ/btshBmz0puP/ckcsLKv43a63zpYHDkFVX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CLgdZ/btshBmz0puP/ckcsLKv43a63zpYHDkFVX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CLgdZ/btshBmz0puP/ckcsLKv43a63zpYHDkFVX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCLgdZ%2FbtshBmz0puP%2FckcsLKv43a63zpYHDkFVX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;536&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;536&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Webclient는 Spring Webflux에 포함된 외부 요청 / 응답에 대한 Handling을 담당하는 모듈이다.&lt;br /&gt;기존 RestTemplate의 요청 &amp;lt;-&amp;gt; 응답 사이의 동기성을 해결하고, 보다 효과적으로 서버 리소스를 사용하기 위해 등장했다.&lt;br /&gt;내부적으로 비동기적으로 구성되어 있으며, 구현은 Spring Webflux를 활용했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WebClient 사용 분석&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;WebClient는 총 4단계로 요청 -&amp;gt; 응답 처리를 추상화하였다.&lt;br /&gt;Http Config &amp;rarr; Reqeust Config &amp;rarr; Exchange &amp;rarr; Response Handling&lt;br /&gt;각각&lt;br /&gt;&amp;nbsp;- http 기본 설정 (socket 설정 포함)&lt;br /&gt;&amp;nbsp;- 요청 정의&lt;br /&gt;&amp;nbsp;- 발송&lt;br /&gt;&amp;nbsp;- 응답 처리&lt;/blockquote&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;1. Build with Config&lt;/h3&gt;
&lt;pre id=&quot;code_1685418871392&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WebClient.builder()
  ....
  .build()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;DefaultWebClientBuilder&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1685418879442&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public WebClient build() {
  ClientHttpConnector connectorToUse = this.connector != null ? this.connector : this.initConnector();
  ExchangeFunction exchange = this.exchangeFunction == null ? ExchangeFunctions.create(connectorToUse, this.initExchangeStrategies()) : this.exchangeFunction;
  ExchangeFunction filteredExchange = this.filters != null ? (ExchangeFunction)this.filters.stream().reduce(ExchangeFilterFunction::andThen).map((filter) -&amp;gt; {
    return filter.apply(exchange);
  }).orElse(exchange) : exchange;
  HttpHeaders defaultHeaders = this.copyDefaultHeaders();
  MultiValueMap&amp;lt;String, String&amp;gt; defaultCookies = this.copyDefaultCookies();
  return new DefaultWebClient(filteredExchange, this.initUriBuilderFactory(), defaultHeaders, defaultCookies, this.defaultRequest, new DefaultWebClientBuilder(this));
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;사실상 Req &amp;harr;︎ Res 전 구간의 설정 및 설정 구현체의 reference를 가지고 있다.&lt;/b&gt;&lt;br /&gt;구현체 내부적으로 Config를 가지고 있으며,&lt;br /&gt;&lt;b&gt;Config를 통해 동일한 설정을 가진 webclient builder를 만들 수 있다. (mutate)&lt;/b&gt;&lt;br /&gt;기본적으로 동작할 설정을 가진 구현체를 생성하여, 재활용한다.&lt;br /&gt;&lt;b&gt;(WebClient는 builder를 멤버 가지고 있으며 mutate 시 해당 builder를 리턴해서 재사용성을 높였다)&lt;/b&gt;&lt;/blockquote&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;2. Request 준비&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kDn9l/btshYdA2dDV/Ay7pXP1oybxALGrYXqFJnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kDn9l/btshYdA2dDV/Ay7pXP1oybxALGrYXqFJnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kDn9l/btshYdA2dDV/Ay7pXP1oybxALGrYXqFJnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkDn9l%2FbtshYdA2dDV%2FAy7pXP1oybxALGrYXqFJnK%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;291&quot; height=&quot;250&quot; data-origin-width=&quot;291&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4cDYs/btshCgTShkq/gBODisO1kZWA5hzWEqKOeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4cDYs/btshCgTShkq/gBODisO1kZWA5hzWEqKOeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4cDYs/btshCgTShkq/gBODisO1kZWA5hzWEqKOeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4cDYs%2FbtshCgTShkq%2FgBODisO1kZWA5hzWEqKOeK%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;776&quot; height=&quot;582&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGfTWG/btshHN4y07J/4eDtaYwHzT2FYhdthBm87K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGfTWG/btshHN4y07J/4eDtaYwHzT2FYhdthBm87K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGfTWG/btshHN4y07J/4eDtaYwHzT2FYhdthBm87K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGfTWG%2FbtshHN4y07J%2F4eDtaYwHzT2FYhdthBm87K%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;386&quot; height=&quot;499&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;499&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Request를 생성하기 위한 각종 정보를 세팅하는 작업&lt;br /&gt;Uri, Header, Body 등 Request에 필요한 정보를 interface 형태로 spec을 정의하고, 최종 구현체가 모든 정보를 가지고 전달할 수 있게 한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;3.-Exchange-to-get-ResponseSpec&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;17799&quot; data-ke-size=&quot;size23&quot;&gt;3. Exchange to get ResponseSpec&lt;/h3&gt;
&lt;pre id=&quot;code_1685419014762&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public WebClient.ResponseSpec retrieve() {
  return new DefaultResponseSpec(this.exchange(), this::createRequest);
}

public Mono&amp;lt;ClientResponse&amp;gt; exchange() {
  ClientRequest request = this.inserter != null ? this.initRequestBuilder().body(this.inserter).build() : this.initRequestBuilder().build();
  return Mono.defer(() -&amp;gt; {
    Mono&amp;lt;ClientResponse&amp;gt; responseMono = DefaultWebClient.this.exchangeFunction.exchange(request).checkpoint(&quot;Request to &quot; + this.httpMethod.name() + &quot; &quot; + this.uri + &quot; [DefaultWebClient]&quot;).switchIfEmpty(DefaultWebClient.NO_HTTP_CLIENT_RESPONSE_ERROR);
    if (this.contextModifier != null) {
      responseMono = responseMono.contextWrite(this.contextModifier);
    }

    return responseMono;
  });
}

private HttpRequest createRequest() {
      return new HttpRequest() {
        private final URI uri = DefaultRequestBodyUriSpec.this.initUri();
        private final HttpHeaders headers = DefaultRequestBodyUriSpec.this.initHeaders();

        public HttpMethod getMethod() {
          return DefaultRequestBodyUriSpec.this.httpMethod;
        }

        public String getMethodValue() {
          return DefaultRequestBodyUriSpec.this.httpMethod.name();
        }

        public URI getURI() {
          return this.uri;
        }

        public HttpHeaders getHeaders() {
          return this.headers;
        }
      };
    }&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ClientResponse을 받는 메서드&lt;br /&gt;1. Request Spec으로부터 정보를 받아 ClientRequest를 생성&lt;br /&gt;2. ExchangeFuction을 통해 exchange 수행 (ExchangeFuction은 후술)&lt;br /&gt;3. ResponseSpec 생성 (Exchange 결과(Mono) + Request 객체)&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;4.-Response-Handling&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;19381&quot; data-ke-size=&quot;size23&quot;&gt;4. Response Handling&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;349&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byAGSw/btshKRsfocI/ZeqsiEb9bokIuKHLU9X2h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byAGSw/btshKRsfocI/ZeqsiEb9bokIuKHLU9X2h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byAGSw/btshKRsfocI/ZeqsiEb9bokIuKHLU9X2h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyAGSw%2FbtshKRsfocI%2FZeqsiEb9bokIuKHLU9X2h1%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;349&quot; height=&quot;608&quot; data-origin-width=&quot;349&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ClientResponse를 처리할 수 있는 메서드를 지원한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Configuration&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 666px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 17px;&quot;&gt;Builder Method&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 17px;&quot;&gt;Desc&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;Example&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;baseUrl(String baseUrl)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;기본 Url 세팅&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;.baseUrl(&quot;http://localhost&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 40px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;defaultUriVariables(Map&amp;lt;String, ?&amp;gt; defaultUriVariables)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 40px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;기본 Uri 변수 세팅&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;최종 URL의 {}값을 치환&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 40px;&quot;&gt;.defaultUriVariables(Map.of(&quot;path1&quot;,&amp;nbsp;&quot;값1&quot;,&amp;nbsp;&quot;query1&quot;,&amp;nbsp;&quot;값2&quot;))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 37px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;uriBuilderFactory(UriBuilderFactory uriBuilderFactory)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 37px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;내부적으로 사용되는 BuilderFactory를 customize&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 37px;&quot;&gt;.uriBuilderFactory(new&amp;nbsp;DefaultUriBuilderFactory())&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;defaultHeader(String header, String... values)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;기본 Header 세팅&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 34px;&quot;&gt;.defaultHeader(HttpHeaders.CONTENT_TYPE,&amp;nbsp;MediaType.APPLICATION_JSON_VALUE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;defaultHeaders(Consumer&amp;lt;HttpHeaders&amp;gt; headersConsumer)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Consumer를 accept(this.initHeaders())&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 34px;&quot;&gt;Consumer&amp;lt;HttpHeaders&amp;gt;&amp;nbsp;headersConsumer&amp;nbsp;=&amp;nbsp;httpHeaders&amp;nbsp;-&amp;gt;&amp;nbsp;httpHeaders.setContentType(MediaType.APPLICATION_JSON); &lt;br /&gt;WebClient.builder() &lt;br /&gt;&amp;nbsp;&amp;nbsp;.defaultHeaders(headersConsumer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;defaultCookie(String cookie, String... values)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;기본 Cookie 세팅&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 34px;&quot;&gt;.defaultCookie(&quot;cookie_key&quot;,&amp;nbsp;&quot;cookie_value&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;defaultCookies(Consumer&amp;lt;MultiValueMap&amp;lt;String, String&amp;gt;&amp;gt; cookiesConsumer)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;cookiesConsumer.accept(this.initCookies());&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 34px;&quot;&gt;Consumer&amp;lt;MultiValueMap&amp;lt;String,String&amp;gt;&amp;gt;&amp;nbsp;cookiesConsumer&amp;nbsp;=&amp;nbsp;map&amp;nbsp;-&amp;gt;&amp;nbsp;map.add(&quot;defaultCookie&quot;,&amp;nbsp;&quot;value&quot;); &lt;br /&gt;WebClient.builder() &lt;br /&gt;&amp;nbsp;&amp;nbsp;.defaultCookies(cookiesConsumer)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 60px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;defaultRequest(Consumer&amp;lt;RequestHeadersSpec&amp;lt;?&amp;gt;&amp;gt; defaultRequest)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 60px;&quot;&gt;&lt;span&gt;defaultRequest가 있으면 andThen으로 추가&lt;/span&gt;&lt;br /&gt;&lt;span&gt;없으면 defaultRequest로 세팅&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 60px;&quot;&gt;Consumer&amp;lt;WebClient.RequestHeadersSpec&amp;lt;?&amp;gt;&amp;gt;&amp;nbsp;defaultRequest&amp;nbsp;=&amp;nbsp; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;requestHeadersSpec&amp;nbsp;-&amp;gt;&amp;nbsp;requestHeadersSpec.attribute(&quot;test&quot;,&amp;nbsp;&quot;q&quot;); &lt;br /&gt;WebClient.builder() &lt;br /&gt;&amp;nbsp;&amp;nbsp;.defaultRequest(defaultRequest)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 37px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;filter(ExchangeFilterFunction filter)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 37px;&quot;&gt;&lt;span&gt;webClient용 filter 추가&lt;/span&gt;&lt;br /&gt;&lt;span&gt;request / response handling&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 37px;&quot;&gt;.filter(ExchangeFilterFunction.ofRequestProcessor(this::loggingRequestInfo)) &lt;br /&gt;//&amp;nbsp;로깅용&amp;nbsp;필터를&amp;nbsp;구현하여&amp;nbsp;add&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 37px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 37px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;filters(Consumer&amp;lt;List&amp;lt;ExchangeFilterFunction&amp;gt;&amp;gt; filtersConsumer)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 37px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Filter Consumer를 accept(this.initFilters())&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 37px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 40px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;clientConnector(ClientHttpConnector connector)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 40px;&quot;&gt;&lt;span&gt;ClientHttpConnector 세팅&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot; data-renderer-mark=&quot;true&quot; data-text-custom-color=&quot;#4c9aff&quot;&gt;자세한 사항은 후술&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 40px;&quot;&gt;.clientConnector(new&amp;nbsp;ReactorClientHttpConnector(HttpClient.create()))&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 57px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 57px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;codecs(Consumer&amp;lt;ClientCodecConfigurer&amp;gt; configurer)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 57px;&quot;&gt;&lt;span&gt;configure Encoding / Decoding&lt;/span&gt;&lt;br /&gt;&lt;span&gt;clientCodecConfigurer를 통해 register&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot; data-renderer-mark=&quot;true&quot; data-text-custom-color=&quot;#4c9aff&quot;&gt;자세한 사항은 후술&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 57px;&quot;&gt;.codecs( &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clientCodecConfigurer&amp;nbsp;-&amp;gt;&amp;nbsp;{ &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clientCodecConfigurer.customCodecs().register(new&amp;nbsp;Jackson2JsonDecoder()); &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;clientCodecConfigurer.customCodecs().register(new&amp;nbsp;Jackson2JsonEncoder()); &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 40px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;exchangeStrategies(ExchangeStrategies strategies)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 40px;&quot;&gt;&lt;span&gt;exchange의 stratege를 세팅&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot; data-renderer-mark=&quot;true&quot; data-text-custom-color=&quot;#4c9aff&quot;&gt;자세한 사항은 후술&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 40px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 51px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 51px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;@Deprecated&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;exchangeStrategies(Consumer&amp;lt;ExchangeStrategies.Builder&amp;gt; configurer)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 51px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;deprecate&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 51px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 60px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;exchangeFunction(ExchangeFunction exchangeFunction)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 60px;&quot;&gt;&lt;span&gt;connector와 strategies를 가진 ExchangeFuction을 세팅&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot; data-renderer-mark=&quot;true&quot; data-text-custom-color=&quot;#4c9aff&quot;&gt;자세한 사항은 후술&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 60px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 33.217%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;apply(Consumer&amp;lt;Builder&amp;gt; builderConsumer)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.4496%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Consumer를 accept(this)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 34px;&quot;&gt;Consumer&amp;lt;WebClient.Builder&amp;gt;&amp;nbsp;baseUrlToB&amp;nbsp;=&amp;nbsp;builder&amp;nbsp;-&amp;gt;&amp;nbsp;builder.baseUrl(&quot;b.com&quot;); &lt;br /&gt;WebClient.builder() &lt;br /&gt;&amp;nbsp;&amp;nbsp;.baseUrl(&quot;a.com&quot;) &lt;br /&gt;&amp;nbsp;&amp;nbsp;.apply(baseUrlToB);&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WebClient Components 분석&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ClientConnector&lt;/h3&gt;
&lt;pre id=&quot;code_1685419461741&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface ClientHttpConnector {
  Mono&amp;lt;ClientHttpResponse&amp;gt; connect(HttpMethod method, URI uri, Function&amp;lt;? super ClientHttpRequest, Mono&amp;lt;Void&amp;gt;&amp;gt; requestCallback);
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;request 정보 + Callback Function을 받아서 Mono로 Response를 전달하는 method를 가진 interface&lt;br /&gt;HTTP 통신의 최종 목적이 되는 메서드이다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;253&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/egGZWG/btshRyluGJc/WdyGHPtUsmiU85kDptReo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/egGZWG/btshRyluGJc/WdyGHPtUsmiU85kDptReo0/img.png&quot; data-alt=&quot;구현체들&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/egGZWG/btshRyluGJc/WdyGHPtUsmiU85kDptReo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FegGZWG%2FbtshRyluGJc%2FWdyGHPtUsmiU85kDptReo0%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;1027&quot; height=&quot;253&quot; data-origin-width=&quot;1027&quot; data-origin-height=&quot;253&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;구현체들&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;구현체들 마다 차이가 있지만, 대부분 비슷한 로직을 가졌으며, 내부적으로 HttpClient 구현체가 다르다.&lt;br /&gt;ex) ReactorClientHttpConnector &amp;rarr; Netty의 HttpClient, &lt;br /&gt;JettyClientHttpConnector &amp;rarr; jetty.client의 HttpClient를 가진다&lt;/blockquote&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;ClientCodecConfigurer&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Encoder 및 Decoder를 세팅하기 위한 Config Interface&lt;br /&gt;핵심은 추상클래스의 BaseCodecConfigurer의 멤버인 BaseDefaultCodecs, DefaultCustomCodecs를 Config 하는 기능 제공&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;752&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btVpxj/btshMV86mYN/dhVxhzQM0YAifDzClmktx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btVpxj/btshMV86mYN/dhVxhzQM0YAifDzClmktx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btVpxj/btshMV86mYN/dhVxhzQM0YAifDzClmktx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtVpxj%2FbtshMV86mYN%2FdhVxhzQM0YAifDzClmktx0%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;852&quot; height=&quot;752&quot; data-origin-width=&quot;852&quot; data-origin-height=&quot;752&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;세팅된 Reader와 Writer를 제공해 주는 핵심 메서드&lt;br /&gt;모양을 보면 defaultCodec에 customCodec을 apply 해서 제공한다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;BaseDefaultCodecs&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FL6cR/btshRyZ6hH1/joKHAMI6KQYY6dk3I5ZJZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FL6cR/btshRyZ6hH1/joKHAMI6KQYY6dk3I5ZJZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FL6cR/btshRyZ6hH1/joKHAMI6KQYY6dk3I5ZJZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFL6cR%2FbtshRyZ6hH1%2FjoKHAMI6KQYY6dk3I5ZJZ1%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;372&quot; height=&quot;367&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;367&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;기본적으로 지원하는 코덱에 대한 세팅이 되어있는 클래스&lt;br /&gt;특히 jackson2JsonDecoder, Encoder를 많이 사용하게 된다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDU5Js/btshChFmteV/ynkPv4Skgf1az6KQleEyJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDU5Js/btshChFmteV/ynkPv4Skgf1az6KQleEyJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDU5Js/btshChFmteV/ynkPv4Skgf1az6KQleEyJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDU5Js%2FbtshChFmteV%2FynkPv4Skgf1az6KQleEyJK%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;644&quot; height=&quot;180&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;추가적으로 코덱 단에서 설정할 수 있는 부분이 있는데&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;maxInMemorySize(int size)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;&amp;nbsp;- Encode, Decode시의 Message의 최대크기를 정할 수 있다.&lt;br /&gt;&amp;nbsp;- default: 256KB&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;enableLoggingRequestDetails(boolean var1)&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;b&gt;&amp;nbsp;- Requset / Response에 대한 상세로깅을 활성화할 수 있음&lt;br /&gt;&amp;nbsp;- 기본적으로 Authorization Header 등 민감 정보가 올 수 있는 부분 때문에, default = false&lt;/b&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DefaultCustomCodecs&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ioe36/btshPphX1fk/VblOZaLev61eREWDke1DEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ioe36/btshPphX1fk/VblOZaLev61eREWDke1DEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ioe36/btshPphX1fk/VblOZaLev61eREWDke1DEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIoe36%2FbtshPphX1fk%2FVblOZaLev61eREWDke1DEK%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;507&quot; height=&quot;106&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;BaseCodecConfigurer가 가진 내부 final 클래스&lt;br /&gt;customCodec을 apply 하기 위해 일종의 custom encoder, decoder의 홀더 역할을 한다.&lt;br /&gt;핵심 메서드인 private void addCodec(Object codec, boolean applyDefaultConfig)을 통해 readers와 writers를 세팅한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;ExchangeFunction&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;24017&quot; data-ke-size=&quot;size23&quot;&gt;ExchangeFunction&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 id=&quot;ExchangeFunction-interface&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;24035&quot; data-ke-size=&quot;size20&quot;&gt;ExchangeFunction interface&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div&gt;
&lt;div data-context-id=&quot;127008835&quot; data-type=&quot;file&quot; data-node-type=&quot;media&quot; data-width=&quot;1362&quot; data-height=&quot;251&quot; data-id=&quot;8cf4491a-ca14-4541-a990-5b3b20c86a29&quot; data-collection=&quot;contentId-127008835&quot; data-file-name=&quot;image-20230509-003822.png&quot; data-file-size=&quot;36244&quot; data-file-mime-type=&quot;image/png&quot; data-alt=&quot;&quot;&gt;
&lt;div id=&quot;newFileExperienceWrapper&quot; data-testid=&quot;media-card-view&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxLVUM/btshVCnBIQY/ulYXUJzZRwcBn8pUEdBgA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxLVUM/btshVCnBIQY/ulYXUJzZRwcBn8pUEdBgA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxLVUM/btshVCnBIQY/ulYXUJzZRwcBn8pUEdBgA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxLVUM%2FbtshVCnBIQY%2FulYXUJzZRwcBn8pUEdBgA0%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;1348&quot; height=&quot;248&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Request를 받아서 Response를 받아오는 작업의 interface&lt;br /&gt;filter apply 메서드와 함께 구현해야 함&lt;/blockquote&gt;
&lt;h4 id=&quot;ExchangeFunctions&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;24140&quot; data-ke-size=&quot;size20&quot;&gt;ExchangeFunctions&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;185&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQHWMC/btsh16IbZSV/ozcbDHEAgvZVNtyzxpXKG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQHWMC/btsh16IbZSV/ozcbDHEAgvZVNtyzxpXKG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQHWMC/btsh16IbZSV/ozcbDHEAgvZVNtyzxpXKG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQHWMC%2Fbtsh16IbZSV%2FozcbDHEAgvZVNtyzxpXKG0%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;1348&quot; height=&quot;185&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;185&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;ExchangeFunction을 생성할 수 있는 Util 클래스&lt;br /&gt;내부적으로 ExchangeFunction을 구현한 DefaultExchangeFunction을 가지고 있다.&lt;/blockquote&gt;
&lt;h4 id=&quot;DefaultExchangeFunction&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;24263&quot; data-ke-size=&quot;size20&quot;&gt;DefaultExchangeFunction&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;div style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-layout=&quot;center&quot; data-node-type=&quot;mediaSingle&quot;&gt;
&lt;div&gt;
&lt;div data-context-id=&quot;127008835&quot; data-type=&quot;file&quot; data-node-type=&quot;media&quot; data-width=&quot;601&quot; data-height=&quot;176&quot; data-id=&quot;6de0c185-2c62-4b2f-905e-41912e87e44e&quot; data-collection=&quot;contentId-127008835&quot; data-file-name=&quot;image-20230509-013119.png&quot; data-file-size=&quot;26715&quot; data-file-mime-type=&quot;image/png&quot; data-alt=&quot;&quot;&gt;
&lt;div id=&quot;newFileExperienceWrapper&quot; data-testid=&quot;media-card-view&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mV8QJ/btshG7hVJM9/lApOi0liXzEDoyqKuRkdA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mV8QJ/btshG7hVJM9/lApOi0liXzEDoyqKuRkdA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mV8QJ/btshG7hVJM9/lApOi0liXzEDoyqKuRkdA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmV8QJ%2FbtshG7hVJM9%2FlApOi0liXzEDoyqKuRkdA0%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;601&quot; height=&quot;176&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/prEol/btshTvB9Nn1/kKDIriKlMXlxIDjAyFTcK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/prEol/btshTvB9Nn1/kKDIriKlMXlxIDjAyFTcK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/prEol/btshTvB9Nn1/kKDIriKlMXlxIDjAyFTcK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FprEol%2FbtshTvB9Nn1%2FkKDIriKlMXlxIDjAyFTcK0%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;910&quot; height=&quot;104&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Connector와 ExchangeStrategies를 가지고 있다.&lt;br /&gt;connector를 통해 connect를 수행하고, exchangeStrategies를 통해 message read / write를 수행한다.&lt;br /&gt;default로 logging이 false인데, strategies가 들어오면 전략에 동봉된 코덱을 확인&lt;/blockquote&gt;
&lt;pre id=&quot;code_1685419783726&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Mono&amp;lt;ClientResponse&amp;gt; exchange(ClientRequest clientRequest) {
  Assert.notNull(clientRequest, &quot;ClientRequest must not be null&quot;);
  HttpMethod httpMethod = clientRequest.method();
  URI url = clientRequest.url();
  return this.connector.connect(httpMethod, url, (httpRequest) -&amp;gt; {
    return clientRequest.writeTo(httpRequest, this.strategies);
  }).doOnRequest((n) -&amp;gt; {
    this.logRequest(clientRequest);
  }).doOnCancel(() -&amp;gt; {
    ExchangeFunctions.logger.debug(clientRequest.logPrefix() + &quot;Cancel signal (to close connection)&quot;);
  }).onErrorResume(WebClientUtils.WRAP_EXCEPTION_PREDICATE, (t) -&amp;gt; {
    return this.wrapException(t, clientRequest);
  }).map((httpResponse) -&amp;gt; {
    String logPrefix = this.getLogPrefix(clientRequest, httpResponse);
    this.logResponse(httpResponse, logPrefix);
    return new DefaultClientResponse(httpResponse, this.strategies, logPrefix, httpMethod.name() + &quot; &quot; + url, () -&amp;gt; {
      return this.createRequest(clientRequest);
    });
  });
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. connector를 통해 connect 수행&lt;br /&gt;&amp;nbsp;- connect의 callback 메서드로 request와 strategies를 수행하도록 세팅&lt;br /&gt;2. logging 및 Exception 처리 등록&lt;br /&gt;3. map 체이닝으로 ClientResponse를 생성&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Message Write&lt;/h4&gt;
&lt;pre id=&quot;code_1685419866970&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static &amp;lt;M extends ReactiveHttpOutputMessage&amp;gt; Mono&amp;lt;Void&amp;gt; writeWithMessageWriters(
			M outputMessage, BodyInserter.Context context, Object body, ResolvableType bodyType, @Nullable ReactiveAdapter adapter) {

		Publisher&amp;lt;?&amp;gt; publisher;
		if (body instanceof Publisher) {
			publisher = (Publisher&amp;lt;?&amp;gt;) body;
		}
		else if (adapter != null) {
			publisher = adapter.toPublisher(body);
		}
		else {
			publisher = Mono.just(body);
		}
		MediaType mediaType = outputMessage.getHeaders().getContentType();
		return context.messageWriters().stream()
				.filter(messageWriter -&amp;gt; messageWriter.canWrite(bodyType, mediaType))
				.findFirst()
				.map(BodyInserters::cast)
				.map(writer -&amp;gt; write(publisher, bodyType, mediaType, outputMessage, context, writer))
				.orElseGet(() -&amp;gt; Mono.error(unsupportedError(bodyType, context, mediaType)));
	}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;BodyInserters에서 Body를 write 할 때 MessageWriter 중 첫 번째 것을 사용한다.&lt;br /&gt;fromValue &amp;rarr; writeWithMessageWriters(&amp;hellip;)&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Message Read&lt;/h4&gt;
&lt;pre id=&quot;code_1685419893999&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private static &amp;lt;T, S extends Publisher&amp;lt;T&amp;gt;&amp;gt; S readWithMessageReaders(
			ReactiveHttpInputMessage message, BodyExtractor.Context context, ResolvableType elementType,
			Function&amp;lt;HttpMessageReader&amp;lt;T&amp;gt;, S&amp;gt; readerFunction,
			Function&amp;lt;UnsupportedMediaTypeException, S&amp;gt; errorFunction,
			Supplier&amp;lt;S&amp;gt; emptySupplier) {

		if (VOID_TYPE.equals(elementType)) {
			return emptySupplier.get();
		}
		MediaType contentType = Optional.ofNullable(message.getHeaders().getContentType())
				.orElse(MediaType.APPLICATION_OCTET_STREAM);

		return context.messageReaders().stream()
				.filter(reader -&amp;gt; reader.canRead(elementType, contentType))
				.findFirst()
				.map(BodyExtractors::&amp;lt;T&amp;gt;cast)
				.map(readerFunction)
				.orElseGet(() -&amp;gt; {
					List&amp;lt;MediaType&amp;gt; mediaTypes = context.messageReaders().stream()
							.flatMap(reader -&amp;gt; reader.getReadableMediaTypes(elementType).stream())
							.collect(Collectors.toList());
					return errorFunction.apply(
							new UnsupportedMediaTypeException(contentType, mediaTypes, elementType));
				});
	}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;BodyExtractor에서 등록된 messageReader 중 첫 번째 reader를 통해 메시지를 Read 한다.&lt;br /&gt;toMono / toFlux &amp;rarr; readWithMessageReaders(&amp;hellip;)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WebClient 간단 세팅 Template&lt;/h2&gt;
&lt;pre id=&quot;code_1685420291744&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WebClient.builder()
  .clientConnector(new ReactorClientHttpConnector(HttpClient.create()
      // SSL 인증서 무시 설정
      .secure(SslProvider.builder()
          .sslContext(SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE))
          .defaultConfiguration(SslProvider.DefaultConfigurationType.NONE)
          // Handshake Timeout tjfwjd
          .handshakeTimeoutMillis(2000)
          .build())
      // DNS Resolver 설정
      .resolver(DefaultAddressResolverGroup.INSTANCE)    // Sets the JVM built-in resolver.
      // Connection Timeout 설정
      .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.applicationProperties.getTcpClientConnectionTimeout())
      .doOnConnected(connection -&amp;gt;
          connection
              // ReadTimeout 설정
              .addHandlerLast(new ReadTimeoutHandler(this.applicationProperties.getTcpClientReadTimeout()))
              // WriteTimeout 설정
              .addHandlerLast(new WriteTimeoutHandler(this.applicationProperties.getTcpClientWriteTimeout())))))
  .exchangeStrategies(ExchangeStrategies.builder()
      .codecs(configurer -&amp;gt; {
          // Date Size 무제한 설정
          configurer.defaultCodecs().maxInMemorySize(-1);
          // 살세 로깅 enable
          configurer.defaultCodecs().enableLoggingRequestDetails(true);
      })
      .build())
  // MDC로 transactionId 넣는 설정
  .filter(mdcFilter)
  // Req, Res 로깅 필터 등록 설정
  .filter(ExchangeFilterFunction.ofRequestProcessor(WebClientConfig::loggingRequestInfo))
  .filter(ExchangeFilterFunction.ofResponseProcessor(WebClientConfig::loggingResponseInfo))
  .build();
  
  

private static  Mono&amp;lt;ClientRequest&amp;gt; loggingRequestInfo(final ClientRequest request) {
  StringBuffer sb = new StringBuffer();
  sb.append(&quot;Req 3rd &amp;gt; &quot;).append(request.method())
      .append(&quot;, URI=&quot;).append(request.url())
      .append(&quot;, Header=&quot;).append(request.headers().isEmpty() ? &quot;&quot; : JsonUtil.buildJsonStr(request.headers()))
      .append(&quot;, Body=&quot;).append(JsonUtil.buildJsonStr(request.body()));

  log.info(sb.toString());

  return Mono.just(request);
}

private static Mono&amp;lt;ClientResponse&amp;gt; loggingResponseInfo(final ClientResponse response) {
  log.info(&quot;Res 3rd &amp;gt; {}&quot;, response.statusCode());
  return Mono.just(response);
}

private static final ExchangeFilterFunction mdcFilter = (request, next) -&amp;gt; {
  // here runs on main(request's) thread
  Map&amp;lt;String, String&amp;gt; map = MDC.getCopyOfContextMap();
  return next.exchange(request)
      .doOnEach(value -&amp;gt; {
        // here runs on reactor's thread
        if (map != null) {
          MDC.setContextMap(map);
        }
      });
};&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;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;WebClient는 REST Client를 Webflux에 맞게 인터페이스 및 설정을 제공&lt;br /&gt;&lt;b&gt;WebClient Instance는 Immutable&lt;/b&gt;&lt;br /&gt;mutate를 통해서 요청 / 응답 설정에 대한 커스텀만 하며, 기본적인 인스턴스는 하나다. &lt;br /&gt;&lt;b&gt;Http 이하 계층과, 비즈니스 부분이 분리될 수 있도록 설계됨 (Connector 설정이 되어있다면&amp;hellip;)&lt;/b&gt;&lt;br /&gt;WebClient는 총 4단계로 요청 &amp;rarr; 응답이 처리를 추상화하였다&lt;br /&gt;&amp;nbsp;- Http Config &amp;rarr; Reqeust Config &amp;rarr; Exchange &amp;rarr; Response Handling&lt;br /&gt;HttpClientConnector는 최초 요청 시 Pool을 만든다. (Provider의 역할)&lt;br /&gt;Timeout 설정은 총 5가지 정도 있음&lt;br /&gt;&amp;nbsp;- Connection Timeout&lt;br /&gt;&amp;nbsp;- Handshake Timeout&lt;br /&gt;&amp;nbsp;- Read Timeout&lt;br /&gt;&amp;nbsp;- Write Timeout&lt;br /&gt;&amp;nbsp;- Response Timeout&lt;br /&gt;REST 요청 중 여러 구간에서 Handler를 달 수 있음&lt;br /&gt;&amp;nbsp;- Request 전 / 후&lt;br /&gt;&amp;nbsp;- Response 전 / 후&lt;br /&gt;&amp;nbsp;- Connection 전 / 후&lt;br /&gt;&amp;nbsp;- Serialize, Deserialize 시&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>spring</category>
      <category>spring webclient</category>
      <category>spring webflux</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/53</guid>
      <comments>https://p-bear.tistory.com/53#entry53comment</comments>
      <pubDate>Tue, 30 May 2023 13:21:21 +0900</pubDate>
    </item>
    <item>
      <title>#1 EasyCalendar 기획 및 설계</title>
      <link>https://p-bear.tistory.com/52</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;일정 정리를 위한 Calendar 관리는 제 삶에 필수적인 사항입니다.&lt;br /&gt;하지만, 일정 입력 시 UX 적으로 번거로운 부분이 있습니다.&lt;br /&gt;가령 반복적이지만 비 정기적인 일정 (운동, 모임 같은)을 입력할 때&lt;br /&gt;날싸선택 -&amp;gt; 내용 입력 -&amp;gt; 시간 입력 -&amp;gt; 장소 입력 등등의 과정을 거치며&lt;br /&gt;조금 더 편하게 입력할 수 있지 않을까 합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기획&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;EasyCalendar란 반복적으로 일어나는 컨텍스트를 템플릿 화 하여, 반복되는 내용을 미리 입력해 놓고, 간편하게 등록하는 웹 서비스이다.&lt;br /&gt;&lt;br /&gt;전체적인 구성으로는 4 페이즈로 나누어집니다.&lt;br /&gt;1. 로그인 / 구글 연동 페이즈&lt;br /&gt;&amp;nbsp;- 최초 EntryPoint에서 p-bear 로그인 연동을 통해 로그인 및 토큰 발급&lt;br /&gt;&amp;nbsp;- 서비스 진입 시 구글 연동을 확인하며, 구글 연동 실패 / 미연동 시 구글 연동 진입&lt;br /&gt;&amp;nbsp;- 연동 성공 시 서비스&amp;nbsp;&lt;br /&gt;2. 일정 확인 및 간편 입력 버튼이 있는 DashBoard 페이즈&lt;br /&gt;&amp;nbsp;- 앞으로 시작될 3개의 이벤트에 대한 내용을 보여줍니다.&lt;br /&gt;&amp;nbsp;- 일정 템플릿이 리스트업 됩니다.&lt;br /&gt;&amp;nbsp;- 템플릿 등록버튼이 있습니다.&lt;br /&gt;3. 일정 등록 페이즈&lt;br /&gt;&amp;nbsp;- 일정 등록 버튼 입력 시, 일정등록에 필요한 정보 입력 컨테이너가 해당 버튼 아래에 생성 또는 모달&lt;br /&gt;&amp;nbsp;- 일정 등록 버튼 위치&lt;br /&gt;4. 일정 템플릿 등록 페이즈&lt;br /&gt;&amp;nbsp;- 일정 템플릿 등록을 위한 정보 입력 페이지가 나옵니다.&lt;br /&gt;&amp;nbsp;- 템플릿을 Type별로 관리할 예정&lt;br /&gt;&amp;nbsp; &amp;nbsp; - WeekDayType: 이번주 또는 다음 주 xx요일로 사전입력된 정보를 등록할 수 있는 타입&lt;br /&gt;&amp;nbsp;- 템플릿에 대한 정보는 서버에서 관리&lt;br /&gt;&amp;nbsp;- 템플릿 CRD 제공&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;UI 설계&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S5kZm/btshwmrsnTK/dnmMDcreiRRjsBoKGOB9I0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S5kZm/btshwmrsnTK/dnmMDcreiRRjsBoKGOB9I0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S5kZm/btshwmrsnTK/dnmMDcreiRRjsBoKGOB9I0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS5kZm%2FbtshwmrsnTK%2FdnmMDcreiRRjsBoKGOB9I0%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;651&quot; height=&quot;741&quot; data-origin-width=&quot;651&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기능 설계&lt;/h2&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;267&quot; data-origin-height=&quot;761&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqzCPJ/btshsLFUImv/UwNU21TkX4Uws3jkjzpyKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqzCPJ/btshsLFUImv/UwNU21TkX4Uws3jkjzpyKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqzCPJ/btshsLFUImv/UwNU21TkX4Uws3jkjzpyKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqzCPJ%2FbtshsLFUImv%2FUwNU21TkX4Uws3jkjzpyKk%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;267&quot; height=&quot;761&quot; data-origin-width=&quot;267&quot; data-origin-height=&quot;761&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;- client는 p-bear AccessToken 만 관리&lt;br /&gt;&amp;nbsp;- 구글연동을 통해 서버에서 사용자 연동 계정 및 refresh_token 관리&lt;br /&gt;&amp;nbsp;- client는 유효한 p-bear AccessToken으로 요청을 하며, google Token이 필요한 경우 서버에서 캐싱 또는 refresh를 통해 Google로 ByPass&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기능 정리&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;API&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;Desc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;OAuth2.0 로그인&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;로그인 페이지 제공, 로그인 후 지정된 redirectUri로 redirect하여, client 해당 URI를 통해 token 획득&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;Google 연동&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;Google OAuth API를 통해 authorization_code 방식으로 code를 획득&lt;br /&gt;서버로 code를 전달하여 연동 작업 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;Google 연동 확인&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;p-bear AccessToken을 통해 Google 연동 정보를 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;Calendar 리스트 조회&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;내 연동된 계정의 Google Calendar 리스트 조회 기능&lt;br /&gt;&lt;br /&gt;서버 필수 제공 정보&lt;br /&gt;&amp;nbsp;- id (pk)&lt;br /&gt;&amp;nbsp;- desc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;Event 리스트 조회&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;지정된 Calendar의 등록된 이벤트 리스트 조회 기능&lt;br /&gt;&lt;br /&gt;요청 사항&lt;br /&gt;&amp;nbsp;- calendarId&lt;br /&gt;&amp;nbsp;- 갯수&lt;br /&gt;&amp;nbsp;- 이벤트 시작 시간&lt;br /&gt;&lt;br /&gt;서버 필수 제공 정보&lt;br /&gt;&amp;nbsp;- id (pk)&lt;br /&gt;&amp;nbsp;- title&lt;br /&gt;&amp;nbsp;- desc&lt;br /&gt;&amp;nbsp;- 시작 시간&lt;br /&gt;&amp;nbsp;- 종료 시간&lt;br /&gt;&amp;nbsp;- 등록 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;템플릿 등록&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;지정된 형식의 템플릿 등록 기능&lt;br /&gt;이름과 type으로 등록하며, 기타 정보 jsonObject로 제공 및 서버에서 validation 후 저장&lt;br /&gt;&lt;br /&gt;요청 사항&lt;br /&gt;&amp;nbsp;- 이름&lt;br /&gt;&amp;nbsp;- type&lt;br /&gt;&amp;nbsp;- 일정 명, 일정 상세&lt;br /&gt;&amp;nbsp;- properties (json)&lt;br /&gt;&lt;br /&gt;응답으로 id 값을 준다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;템플릿 리스트 조회&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;등록된 템플릿 리스트로 조회하는 기능&lt;br /&gt;&lt;br /&gt;템플릿 관련 정보 전체 조회&lt;br /&gt;리스트에 객체로 모든 정보가 담겨서 나온다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.3023%;&quot;&gt;템플릿 삭제&lt;/td&gt;
&lt;td style=&quot;width: 75.6977%;&quot;&gt;리스트 화면에서 object를 길게 클릭 또는 삭제 버튼 활성화해서 서버로 삭제 요청을 보낸다.&lt;br /&gt;서버에서 id를 통해 템플릿 데이터를 삭제한다.&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ETC&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;이번 프로젝트의 목적은 Flutter에 익숙해는 것이 첫 번째이다.&lt;br /&gt;Flutter 주요 구현 사항&lt;br /&gt;&amp;nbsp;- UI&lt;br /&gt;&amp;nbsp;- Http 통신&lt;br /&gt;&amp;nbsp;- 토큰 관리&lt;/blockquote&gt;</description>
      <category>[완] 개인서버 개발/EasyCalendar(완)</category>
      <category>Google Calendar</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/52</guid>
      <comments>https://p-bear.tistory.com/52#entry52comment</comments>
      <pubDate>Fri, 26 May 2023 15:08:39 +0900</pubDate>
    </item>
    <item>
      <title>HttpClient (Netty) Configuration 정리</title>
      <link>https://p-bear.tistory.com/51</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;164&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jo3Ul/btsgNX1tRXi/IatKF4sXRgPa4lCQOkp3q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jo3Ul/btsgNX1tRXi/IatKF4sXRgPa4lCQOkp3q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jo3Ul/btsgNX1tRXi/IatKF4sXRgPa4lCQOkp3q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJo3Ul%2FbtsgNX1tRXi%2FIatKF4sXRgPa4lCQOkp3q1%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;318&quot; height=&quot;164&quot; data-origin-width=&quot;318&quot; data-origin-height=&quot;164&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Spring Webflux의 WebClient의 설정을 정리하기 전&lt;br /&gt;HTTP 관련 구현체인 Netty의 HttpClient 설정을 우선 정리&lt;br /&gt;기본적인 설정만 정리&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HttpClient Configuration&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 230px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;Config&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;Desc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;disableRetry(boolean disableRetry)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;request 실패 시 retry 여부&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;doOnRedirect(BiConsumer&amp;lt;? super HttpClientResponse,? super Connection&amp;gt; doOnRedirect)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;redirect 응답 시 Consumer 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 34px;&quot;&gt;&lt;b&gt;doOnConnected(Consumer&amp;lt;? super Connection&amp;gt; doOnConnected)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 34px;&quot;&gt;&lt;b&gt;connection success시 consumer 설정&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;followRedirect(boolean followRedirect)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;redirect 시 follow 여부&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;keepAlive(boolean keepAlive)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;keepAlive 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;http2Settings(Consumer&amp;lt;Http2SettingsSpec.Builder&amp;gt; http2Settings)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;http2 관련 세팅&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;b&gt;responseTimeout(Duration maxReadOperationInterval)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;b&gt;connection 이후 response에 걸리는 총 시간에 대한 설정&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;b&gt;secure(SslProvider sslProvider)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;span&gt;&lt;b&gt;SSL 관련 설정&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: left;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;사설인증서 무시&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;b&gt;SSL Timeout&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;등등&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;websocket(WebsocketClientSpec websocketClientSpec)&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;웹소켓 관련 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;그 외 hearders, doOnRequest, doOnResponseError 등 다수의 설정들이 존재&lt;/blockquote&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;ClientTransport&amp;nbsp;Configuration&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 52.2093%;&quot;&gt;Config&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot;&gt;Desc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 52.2093%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;channelGroup(ChannelGroup channelGroup)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;채널 그룹 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 52.2093%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;doOnChannelInit(ChannelPipelineConfigurer doOnChannelInit)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;채널 pipeline init 시 동작할 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 52.2093%;&quot;&gt;&lt;b&gt;option(ChannelOption&amp;lt;O&amp;gt; key, @Nullable O value)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot;&gt;&lt;b&gt;채널옵션 설정 (후술)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 52.2093%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;runOn(EventLoopGroup eventLoopGroup)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 47.7907%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;이벤트 그룹 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;ChannelOption&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;io.netty.channel.ChannelOption&amp;lt;T&amp;gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-layout=&quot;default&quot;&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 105px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;Config&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;Desc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;b&gt;RCVBUF_ALLOCATOR&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;&lt;b&gt;수신 버퍼 allocate 사이즈 지정&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;b&gt;CONNECT_TIMEOUT_MILLIS&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;b&gt;커넥션 타임아웃&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;MAX_MESSAGES_PER_READ&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;한번에 읽을 최대 메시지 사이즈 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;SO_SNDBUF, SO_RCVBUF&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;송/수신 버퍼의 크기 조정 (UDP에서 사용)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;네트워크 관련 설정을 할 수 있다.&lt;br /&gt;주요 설정으로&lt;br /&gt;&amp;nbsp;- SSL Provider를 통한 SSL 설정&lt;br /&gt;&amp;nbsp;- responseTimeout 설정&lt;br /&gt;&amp;nbsp;- Connection Timeout 설정&lt;br /&gt;&amp;nbsp;- read / write Timeout 설정 (doOnConnected에 handler를 추가해서)&lt;br /&gt;&amp;nbsp;- DNS resolver 관련 설정&lt;br /&gt;&amp;nbsp;- Buffer size 관련 설정&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Timeout 관련해서 connection, read, write는 설정할 만하다. 다만 response Timeout의 경우 특별한 이유가 없는 한 Client 입장에서 설정하는 건 문제의 여지가 많을 듯하다.&lt;br /&gt;DNS 관련 설정으로 resolver 설정을 DEFAULT로 안 하면, 도메인에서 ip를 찾지 못하는 이슈가 있었다.&lt;br /&gt;기타 설정들은 특별한 이유가 없는 한 default로 하는 게 최적일 것으로 보인다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://projectreactor.io/docs/netty/release/api/reactor/netty/http/client/HttpClient.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://projectreactor.io/docs/netty/release/api/reactor/netty/http/client/HttpClient.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>HttpClient</category>
      <category>netty</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/51</guid>
      <comments>https://p-bear.tistory.com/51#entry51comment</comments>
      <pubDate>Thu, 25 May 2023 19:05:55 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Connect 사용성 검토</title>
      <link>https://p-bear.tistory.com/50</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYxEwJ/btsg1cQjiuL/63UMQVkYHmfkzkutAVkMuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYxEwJ/btsg1cQjiuL/63UMQVkYHmfkzkutAVkMuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYxEwJ/btsg1cQjiuL/63UMQVkYHmfkzkutAVkMuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYxEwJ%2Fbtsg1cQjiuL%2F63UMQVkYHmfkzkutAVkMuK%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;665&quot; height=&quot;332&quot; data-origin-width=&quot;665&quot; data-origin-height=&quot;332&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2360&quot; data-origin-height=&quot;518&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ETcQ0/btsg1eHl54x/yXXN1t1TknH5XVJtem87GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ETcQ0/btsg1eHl54x/yXXN1t1TknH5XVJtem87GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ETcQ0/btsg1eHl54x/yXXN1t1TknH5XVJtem87GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FETcQ0%2Fbtsg1eHl54x%2FyXXN1t1TknH5XVJtem87GK%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;2360&quot; height=&quot;518&quot; data-origin-width=&quot;2360&quot; data-origin-height=&quot;518&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;- 데이터 파이프라인 생성 시&amp;nbsp; 반복 작업을 줄이고 효율적인 전송을 이루기 위한 애플리케이션&lt;br /&gt;&amp;nbsp;- 반복작업에 대해서 매번 Producer, Consumer 애플리케이션의 배포/운영해야 하는 비효율 보완&lt;br /&gt;&amp;nbsp;- Connect 를 이용하면 특정한 작업 형태를 템플릿으로 만들어 놓은 Connector 를 실행함으로써 반복작업을 줄일 수 있음.&lt;br /&gt;&amp;nbsp;- Kafka 브로커와 별도의 클러스터를 구성하며 Connector 가 실행되는 환경을 제공&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장단점&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 37px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;관리 용이&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;모니터링 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;개발 생산성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;최적화 및 유지보수되는 pre-build connector&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;시스템 복잡성 증가&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;보안 문제 가능성 증가 (타 시스템 연동 때문)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;제한된 기능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Confluent Platform 소개&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kXLDK/btsgTjpnZHA/2e6GYsWNsRiF7gxsEoDY8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kXLDK/btsgTjpnZHA/2e6GYsWNsRiF7gxsEoDY8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kXLDK/btsgTjpnZHA/2e6GYsWNsRiF7gxsEoDY8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkXLDK%2FbtsgTjpnZHA%2F2e6GYsWNsRiF7gxsEoDY8k%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;667&quot; height=&quot;370&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Confluent Platform은 여러 소스 데이터를 이용하는 실시간 데이터 파이프라인과 스트리밍 애플리케이션을 쉽게 구축할 수 있는 Kafka 기반 플랫폼&lt;br /&gt;Kafka를 창시한 Jay Kreps는 핵심 멤버들과 함께 Kafka 개발에 집중하기 위해 2014년에 Confluent라는 회사를 창업&lt;br /&gt;Kafka를 Enterpise에서 SaaS 형태로 제공하는 솔루션&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVaVOT/btsg1etOXpS/w8hEByIF8qVZrXy5KV0E8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVaVOT/btsg1etOXpS/w8hEByIF8qVZrXy5KV0E8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVaVOT/btsg1etOXpS/w8hEByIF8qVZrXy5KV0E8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVaVOT%2Fbtsg1etOXpS%2Fw8hEByIF8qVZrXy5KV0E8k%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;535&quot; height=&quot;582&quot; data-origin-width=&quot;535&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Java 뿐 아니라 다양한 랭귀지(C, .Net, Python)로 개발 가능&lt;br /&gt;REST Proxy, MQTT Proxy 통해 Kafka 연결성 확보&lt;br /&gt;100+ Pre-built Connectors 이용 가능하고, 표준 Schema 통한 개발로 운영 복잡성 제거&lt;br /&gt;익숙한 SQL Syntax (ksqlDB) 사용하여 개발이 용이하고, 아키텍처를 단순화&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;License 검토&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Desc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Community License&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span&gt;Pre-built Connectors (Free)&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Confluent REST Proxy&lt;br /&gt;&lt;/span&gt;&lt;span&gt;ksqlDB&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Confluent Schema Registry&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Confluent Admin REST API&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Enterprise License&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span&gt;Confluent Server (kafka)&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Schema Linking&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Pre-builtConnectors (Premium, Commercial)&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Confluent Control Center&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Confluent for Kubernetes&lt;/span&gt;&lt;br /&gt;&lt;span&gt;Confluent Replicator&lt;/span&gt;&lt;br /&gt;&lt;span&gt;MQTT Proxy&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Developer License&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;span&gt;Confluent Platform features for &lt;b&gt;free&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;single broker per cluster&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;여러 single broker cluster 사용은 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;249&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vD26X/btsgYz6Chbu/KgTXbNQceaHS4MAK830O1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vD26X/btsgYz6Chbu/KgTXbNQceaHS4MAK830O1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vD26X/btsgYz6Chbu/KgTXbNQceaHS4MAK830O1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvD26X%2FbtsgYz6Chbu%2FKgTXbNQceaHS4MAK830O1k%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;522&quot; height=&quot;249&quot; data-origin-width=&quot;522&quot; data-origin-height=&quot;249&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Confluent Platform Overview&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;cluster 선택 페이지&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;778&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2wOeB/btsgTjCXinv/IZ2ElG41t5r5nMITxQMeok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2wOeB/btsgTjCXinv/IZ2ElG41t5r5nMITxQMeok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2wOeB/btsgTjCXinv/IZ2ElG41t5r5nMITxQMeok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2wOeB%2FbtsgTjCXinv%2FIZ2ElG41t5r5nMITxQMeok%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;612&quot; height=&quot;778&quot; data-origin-width=&quot;612&quot; data-origin-height=&quot;778&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;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;메인 overview 페이지&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;903&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dsgAkR/btsg0tZf3Mn/yJI98HskmVYKXjPArg2YU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dsgAkR/btsg0tZf3Mn/yJI98HskmVYKXjPArg2YU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dsgAkR/btsg0tZf3Mn/yJI98HskmVYKXjPArg2YU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdsgAkR%2Fbtsg0tZf3Mn%2FyJI98HskmVYKXjPArg2YU1%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;1522&quot; height=&quot;903&quot; data-origin-width=&quot;1522&quot; data-origin-height=&quot;903&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;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;토픽 상세 페이지 중 일부&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;829&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VThFG/btsg1b4WLAU/nnT0tTPgJbZf8gbhbtrC3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VThFG/btsg1b4WLAU/nnT0tTPgJbZf8gbhbtrC3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VThFG/btsg1b4WLAU/nnT0tTPgJbZf8gbhbtrC3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVThFG%2Fbtsg1b4WLAU%2FnnT0tTPgJbZf8gbhbtrC3K%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;1452&quot; height=&quot;829&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;829&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Pre-built&amp;nbsp;Connectors&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Kafka 생태계에 참여하는 각 회사에서 Connector 제공&lt;br /&gt;Confluent Platform에서 요구사항에 맞춰 개발한 Connector 제공 (대부분 유료)&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;종류&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Source Connector: 소스로부터 데이터를 토픽으로 발행하는 커넥터&lt;/li&gt;
&lt;li&gt;Sink Connector: 토픽으로부터 데이터를 소스에 적재하는 커넥터&lt;/li&gt;
&lt;li&gt;Transform Connector: source / sink 커넥터로 데이터가 가기 전 컨버팅을 수행하는 커넥터&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Free Connectors (23년 상반기 기준)&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 473px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style2&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 38.4884%; height: 20px;&quot;&gt;Connectors&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 20px;&quot;&gt;종류&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 20px;&quot;&gt;Desc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot; rowspan=&quot;4&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;DB Connector&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;JDBC Connector (Source and Sink)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source, Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Redis Connector (Source and Sink) by Redis&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source, Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;MongoDB Connector (Source and Sink)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source, Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;각종 CDC Connector&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 20px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source, Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 20px;&quot;&gt;CDC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 40px;&quot; rowspan=&quot;5&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;converter&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.4884%; height: 40px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Kafka Connect Protobuf Converter&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 40px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Transform&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 40px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;kafka schema registry 대상 컨버터&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Kafka Connect Avro Converter&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Transform&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Kafka Connect JSON Schema Converter&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Transform&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Kafka Connect Common Transformations&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Transform&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Kafka Connect JSON Schema Transformations&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Transform&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 34px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;validation, filter, ket,value convert&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 17px;&quot; rowspan=&quot;13&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;External System Connector&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Snowflake Sink Connector&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;HDFS 2 Sink Connector&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;b&gt;ElasticSearch Source / Sink Connector&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;b&gt;Source, Sink&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Amazon S3 Sink Connector&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Google BigQuery Sink Connector&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Spooldir Source Connector&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;FilePulse Source Connector&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Datadog Logs Sink Connector&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Twitter Source Connector&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;AWS Lambda Sink Connector&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Shell Sink/Source Connector&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source, Sink&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;IRC Source Connector&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 38.4884%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;RSS Source Connector&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 17px;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 25%; height: 20px;&quot;&gt;기타&lt;/td&gt;
&lt;td style=&quot;width: 38.4884%; height: 20px;&quot;&gt;&lt;b&gt;Datagen Source Connector&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 13.1395%; height: 20px;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Source&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.3721%; height: 20px;&quot;&gt;&lt;b&gt;MockData generator (for develop)&lt;/b&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Connector 사용 예시 (with Confluent Platform)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ex: MongoSinkConnector&lt;/h3&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;824&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLbyyH/btsgNXAdBW1/KYqbUcll9WiS14WMMXOrz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLbyyH/btsgNXAdBW1/KYqbUcll9WiS14WMMXOrz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLbyyH/btsgNXAdBW1/KYqbUcll9WiS14WMMXOrz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLbyyH%2FbtsgNXAdBW1%2FKYqbUcll9WiS14WMMXOrz1%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;824&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;824&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;각종 설정은 각 Connector 제작자의 document를 따른다&lt;br /&gt;Mongo 커넥터의 경우 MongoDB 단체에서 제작 및 배포&lt;/blockquote&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;옵션&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;Desc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Topic 관련&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;어느 토픽에서 데이터를 가져올 것인가&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Common&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span&gt;kafka에서 가져온 데이터 가져오는 방법&lt;/span&gt;&lt;br /&gt;&lt;span&gt;binary data &amp;rarr; connector input&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Transform&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;transform connector 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Error Handling&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span&gt;에러 처리 관련&lt;/span&gt;&lt;br /&gt;&lt;span&gt;에러가 발생했을 때 핸들링하는 방법 설정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Errors&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;커넥터 자체의 에러에 대한 설정 (기동할 때 등)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Connection&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;MongoDB connection&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Overrides&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;기존 설정에 대해서 override&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Namespace&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Sink 할 대상 database, collection 지정&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Writes&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span&gt;DB에 write 할 때의 설정&lt;/span&gt;&lt;br /&gt;&lt;span&gt;timeout, retry 정책 등&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Post Processing&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;key, value filtering, field rename&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Id Strategies&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;생성할 record의 id 정책 (_id 관련)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.2558%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;Change Data Capture&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: left;&quot;&gt;CDC 관련 핸들링&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1437&quot; data-origin-height=&quot;483&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAeXQx/btsgGkbK2Ud/ICpmwR6nwt3mLP6wLQu0pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAeXQx/btsgGkbK2Ud/ICpmwR6nwt3mLP6wLQu0pk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAeXQx/btsgGkbK2Ud/ICpmwR6nwt3mLP6wLQu0pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAeXQx%2FbtsgGkbK2Ud%2FICpmwR6nwt3mLP6wLQu0pk%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;1437&quot; height=&quot;483&quot; data-origin-width=&quot;1437&quot; data-origin-height=&quot;483&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;- 비즈니스 로직이 없는 단순 ETL에 경우 활용 가능&lt;br /&gt;&amp;nbsp;- 관리의 편의성이 목적인데, Confluent Platform 같은 솔루션이 없으면 편의성 크게 상실&lt;br /&gt;&amp;nbsp;- Connector 설정이 생각보다 복잡 (Connector vendor에 따라서 속성이 너무 많음)&lt;br /&gt;&amp;nbsp;- 현 상황에서는 ElasticSearch를 kafka에 붙이는 용도, 테스트 데이터 발행하는 용도로 사용 가능&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/connect/index.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.confluent.io/platform/current/connect/index.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.confluent.io/platform/7.4/overview.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.confluent.io/platform/7.4/overview.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>confluent platform</category>
      <category>Kafka</category>
      <category>kafka connect</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/50</guid>
      <comments>https://p-bear.tistory.com/50#entry50comment</comments>
      <pubDate>Wed, 24 May 2023 18:50:06 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Schema Registry</title>
      <link>https://p-bear.tistory.com/49</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oQEB5/btsg0nYwJJC/Jl6kYnjdK8dkHAl1KkxT0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oQEB5/btsg0nYwJJC/Jl6kYnjdK8dkHAl1KkxT0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oQEB5/btsg0nYwJJC/Jl6kYnjdK8dkHAl1KkxT0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoQEB5%2Fbtsg0nYwJJC%2FJl6kYnjdK8dkHAl1KkxT0K%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;664&quot; height=&quot;369&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;369&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;- Schema Registry는 Kafka와 별도로 동작하는 프로세스이며, Kafka에서 발생하는 Message에 대한 Schema를 관리하는 역할을 가진다.&lt;br /&gt;&amp;nbsp;- RESTful 인터페이스로 동작한다.&lt;br /&gt;&amp;nbsp;- Confluent에서 공개한 오픈소스 (Hortonworks, Spring Cloud Stream 도 있음)&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Confluent Schema Registry&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;목적&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;데이터 호환성을 유지 및 관리하기 위해서 개발되었다.&lt;br /&gt;producer, consumer가 많을 수 있는 분산 시스템 환경에서, 데이터에 대한 호환성을 유지하기 위함&lt;br /&gt;또한 schema에 대한 관리를 하기 위함. (data governance, 정확성, 신뢰성)&lt;/blockquote&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;스키마 변경 시 발생하는 오류 방지: 메시지에 대한 변경이 일어날 때 보다 안정적으로 진행 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;중복된 스키마 방지: 스키마의 중앙관리로 중복된 스키마를 방지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;호환성 검사 (유료): kafka에 발행되는 메시지를 validation 할 수 있다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;스키마 버전관리: 스키마를 여러 버전으로 관리 가능하다. 롤백 등 가능&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;그룹간 협업 강화: schema에 대한 공유 및 보장&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span&gt;관리 포인트 증가(서버): schema registry 서버를 관리해야 함&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;관리 포인트 증가: 엄격한 스키마 검증이 있기 때문에, 스키마에 대한 관리 필요&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;중앙 집중식 서비스: 확장성, 가용성 문제 발생 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ol&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Schema Registry Flow&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BA6Bv/btsgELtv6QB/y4g5NFrkV6mYOa8FA8kzb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BA6Bv/btsgELtv6QB/y4g5NFrkV6mYOa8FA8kzb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BA6Bv/btsgELtv6QB/y4g5NFrkV6mYOa8FA8kzb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBA6Bv%2FbtsgELtv6QB%2Fy4g5NFrkV6mYOa8FA8kzb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1001&quot; height=&quot;566&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btxpvr/btsgTiQ0DtG/WmF5t9hJPMX5ZXRJUZgMaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btxpvr/btsgTiQ0DtG/WmF5t9hJPMX5ZXRJUZgMaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btxpvr/btsgTiQ0DtG/WmF5t9hJPMX5ZXRJUZgMaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbtxpvr%2FbtsgTiQ0DtG%2FWmF5t9hJPMX5ZXRJUZgMaK%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;1116&quot; height=&quot;862&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1273&quot; data-origin-height=&quot;862&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oF8sj/btsgDLU35Kf/8JgxZzjkSUK2xglsyOYOuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oF8sj/btsgDLU35Kf/8JgxZzjkSUK2xglsyOYOuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oF8sj/btsgDLU35Kf/8JgxZzjkSUK2xglsyOYOuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoF8sj%2FbtsgDLU35Kf%2F8JgxZzjkSUK2xglsyOYOuk%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;1273&quot; height=&quot;862&quot; data-origin-width=&quot;1273&quot; data-origin-height=&quot;862&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;CachedSchemaRegistryClient: Confluent에서 제공하는 Scehma registry 연동 라이브러리 안에 한 모듈, 기동 시 schema registry에 등록된 모든 schema를 캐싱하며, _schmas를 consume 해서 신규 스키마에 대한 정보를 캐싱&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Schema Registry는 kafka를 storage로 사용한다.&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;241&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBoXks/btsgEKuA673/Q76CG58uZ9y4VoDAQkYdx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBoXks/btsgEKuA673/Q76CG58uZ9y4VoDAQkYdx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBoXks/btsgEKuA673/Q76CG58uZ9y4VoDAQkYdx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBoXks%2FbtsgEKuA673%2FQ76CG58uZ9y4VoDAQkYdx1%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;741&quot; height=&quot;241&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;241&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1684728842322&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// topic: _schemas

// schema insert message
{
  &quot;subject&quot;: &quot;test.message-value&quot;,
  &quot;version&quot;: 1,
  &quot;id&quot;: 2,
  &quot;schema&quot;: &quot;{\&quot;type\&quot;:\&quot;record\&quot;,\&quot;name\&quot;:\&quot;Device\&quot;,\&quot;namespace\&quot;:\&quot;com.example.kafkatest.data.device\&quot;,\&quot;fields\&quot;:[{\&quot;name\&quot;:\&quot;id\&quot;,\&quot;type\&quot;:\&quot;long\&quot;},{\&quot;name\&quot;:\&quot;capability\&quot;,\&quot;type\&quot;:{\&quot;type\&quot;:\&quot;record\&quot;,\&quot;name\&quot;:\&quot;Capability\&quot;,\&quot;namespace\&quot;:\&quot;com.example.kafkatest.data.device.capability\&quot;,\&quot;fields\&quot;:[{\&quot;name\&quot;:\&quot;name\&quot;,\&quot;type\&quot;:{\&quot;type\&quot;:\&quot;string\&quot;,\&quot;avro.java.string\&quot;:\&quot;String\&quot;}},{\&quot;name\&quot;:\&quot;value\&quot;,\&quot;type\&quot;:{\&quot;type\&quot;:\&quot;string\&quot;,\&quot;avro.java.string\&quot;:\&quot;String\&quot;}}]}}]}&quot;,
  &quot;deleted&quot;: false
}

// schema update (evolution) message
{
   &quot;subject&quot;: &quot;test.message-value&quot;,
   &quot;version&quot;: 2,
   &quot;id&quot;: 3,
   &quot;schema&quot;: &quot;{\&quot;type\&quot;:\&quot;record\&quot;,\&quot;name\&quot;:\&quot;Device\&quot;,\&quot;namespace\&quot;:\&quot;com.example.kafkatest.data.device\&quot;,\&quot;fields\&quot;:[{\&quot;name\&quot;:\&quot;id\&quot;,\&quot;type\&quot;:\&quot;long\&quot;},{\&quot;name\&quot;:\&quot;location\&quot;,\&quot;type\&quot;:{\&quot;type\&quot;:\&quot;string\&quot;,\&quot;avro.java.string\&quot;:\&quot;String\&quot;},\&quot;default\&quot;:\&quot;default-location\&quot;},{\&quot;name\&quot;:\&quot;capability\&quot;,\&quot;type\&quot;:{\&quot;type\&quot;:\&quot;record\&quot;,\&quot;name\&quot;:\&quot;Capability\&quot;,\&quot;namespace\&quot;:\&quot;com.example.kafkatest.data.device.capability\&quot;,\&quot;fields\&quot;:[{\&quot;name\&quot;:\&quot;name\&quot;,\&quot;type\&quot;:{\&quot;type\&quot;:\&quot;string\&quot;,\&quot;avro.java.string\&quot;:\&quot;String\&quot;}},{\&quot;name\&quot;:\&quot;value\&quot;,\&quot;type\&quot;:{\&quot;type\&quot;:\&quot;string\&quot;,\&quot;avro.java.string\&quot;:\&quot;String\&quot;}}]}}]}&quot;,
   &quot;deleted&quot;: false
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;producer는 publish 전 Schema Registry를 통해 스키마를 등록(갱신,확장)할 수 있으며, 실패 시 메시지 생성을 안 한다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1684728862585&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// Schema 등록 실패
2023-04-17 16:31:46.154 ERROR 26796 --- [io-40001-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.kafka.KafkaException: Send failed; nested exception is org.apache.kafka.common.errors.InvalidConfigurationException: Schema being registered is incompatible with an earlier schema for subject &quot;test.message-value&quot;; error code: 409] with root cause
org.apache.kafka.common.errors.InvalidConfigurationException: Schema being registered is incompatible with an earlier schema for subject &quot;test.message-value&quot;; error code: 409&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;h2 data-ke-size=&quot;size26&quot;&gt;Schema Evolution 전략&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;692&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TFHvj/btsgDJwcGpl/BXiXyf1pPr7SDIkaOqjRc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TFHvj/btsgDJwcGpl/BXiXyf1pPr7SDIkaOqjRc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TFHvj/btsgDJwcGpl/BXiXyf1pPr7SDIkaOqjRc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTFHvj%2FbtsgDJwcGpl%2FBXiXyf1pPr7SDIkaOqjRc1%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;876&quot; height=&quot;692&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;692&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;각 전략마다 schema를 POST 하는데 200인지 409인지 결정됨&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11번가&amp;nbsp;Schema&amp;nbsp;Registry&amp;nbsp;도입&amp;nbsp;예시&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;11번가의 고민&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-size=&quot;size16&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;background-color: #ffffff; color: #707070; text-align: start;&quot; data-renderer-start-pos=&quot;10766&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot; data-renderer-mark=&quot;true&quot; data-text-custom-color=&quot;#ff5630&quot;&gt;DTO 의 필드가 변경되면 어떻게 될까요?&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;-&amp;gt; 필드의 타입이나 이름이 변경된다면 Consumer에서 해석하지 못하는 메시지를 Producer 가 전송하게 될 수도 있는데 막을 수 있는 방법은 조심하는 방법밖에는 없습니다.&lt;/p&gt;
&lt;p data-renderer-start-pos=&quot;10894&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;DTO에 변경사항이 발생해서 적용할 때 배포는 Producer 쪽을 먼저 해야 하나요? Consumer를 먼저 해야 하나요?&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;-&amp;gt; 어떤 변경이 어떤 영향을 주게 될지 잘 모르겠습니다. 변경이 생길 때마다 영향범위를 체크하고 이상이 없을지 충분히 테스트하고 배포해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #707070; text-align: start;&quot; data-renderer-start-pos=&quot;11048&quot; data-ke-size=&quot;size16&quot;&gt;Producer 가 json 포맷으로 메시지를 serialize 해서 Kafka topic에 전달하면 topic 에는 json 프로퍼티의 key 값을 반복적으로 저장하게 될 텐데 디스크 낭비 아닌가요?&lt;br /&gt;-&amp;gt; 낭비입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #707070; text-align: start;&quot; data-renderer-start-pos=&quot;11173&quot; data-ke-size=&quot;size16&quot;&gt;Producer는 Consumer 가 해석할 수 있는 메시지만을 전달하도록 할 수는 없을까요?&lt;/p&gt;
&lt;/blockquote&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;Schema Evolution 전략&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;FULL 전략을 사용&lt;br /&gt;배포 시점을 제외, 서로 다른 schema version을 사용 안 함&lt;br /&gt;배포 시점에 Deserialize Exception을 방지하기 위해 FULL 전략 채용&lt;br /&gt;모든 필드에 대해 default를 지정하는 컨밴션 적용&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Conclusion&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;- Schema Registry는 Kafka로 인해 끊어진 Message의 Schema를 관리 및 보장하기 위해 개발된 관리 프로세스&lt;br /&gt;&amp;nbsp;- data 호환성과 관리 포인트 난이도 사이에 trade-off가 존재한다.&lt;br /&gt;&amp;nbsp;- 다수의 팀이 kafka에 붙는 대형 개발사에서는 필수 요소로 손꼽힘&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/schema-registry/index.html#about-sr&quot;&gt;https://docs.confluent.io/platform/current/schema-registry/index.html#about-sr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@stephane.maarek/introduction-to-schemas-in-apache-kafka-with-the-confluent-schema-registry-3bf55e401321&quot;&gt;https://medium.com/@stephane.maarek/introduction-to-schemas-in-apache-kafka-with-the-confluent-schema-registry-3bf55e401321&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@gaemi/kafka-%EC%99%80-confluent-schema-registry-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%8A%A4%ED%82%A4%EB%A7%88-%EA%B4%80%EB%A6%AC-1-cdf8c99d2c5c&quot;&gt;https://medium.com/@gaemi/kafka-%EC%99%80-confluent-schema-registry-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%8A%A4%ED%82%A4%EB%A7%88-%EA%B4%80%EB%A6%AC-1-cdf8c99d2c5c&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://avro.apache.org/docs/1.11.1/specification/&quot;&gt;https://avro.apache.org/docs/1.11.1/specification/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://11st-tech.github.io/2022/06/28/schema-registry-in-live11/&quot;&gt;https://11st-tech.github.io/2022/06/28/schema-registry-in-live11/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>Avro</category>
      <category>Kafka</category>
      <category>kafka schema registry</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/49</guid>
      <comments>https://p-bear.tistory.com/49#entry49comment</comments>
      <pubDate>Tue, 23 May 2023 18:17:44 +0900</pubDate>
    </item>
    <item>
      <title>Apache Avro 란</title>
      <link>https://p-bear.tistory.com/48</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crezqY/btsgG40G22f/dpaDz81ImEAMeapKy2ibxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crezqY/btsgG40G22f/dpaDz81ImEAMeapKy2ibxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crezqY/btsgG40G22f/dpaDz81ImEAMeapKy2ibxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrezqY%2FbtsgG40G22f%2FdpaDz81ImEAMeapKy2ibxk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;409&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;409&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Avro&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Avro 란&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Avro란 Apache에서 만든 프레임워크로 데이터 직렬화 기능을 제공한다.&lt;br /&gt;JSON과 비슷한 형식이지만, 스키마가 존재한다.&lt;br /&gt;Avro = schema + binary(json value)&lt;/blockquote&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;장점&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;단점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;스키마를 통해 데이터 구조 및 타입을 알 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;데이터 압축&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;스키마 변경에 유연하게 대응 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;binary로 serialize 되어서, 디버깅 등 상황에서 데이터 확인에 어려움이 있다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;스키마에 대한 관리가 필요하다 (관리 포인트 증가)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;DataType&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;Primitive&lt;/span&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;span style=&quot;color: #666666; text-align: left;&quot;&gt;null&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;boolean&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;int&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;long&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;float&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;double&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;bytes&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;string&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;Complex&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&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;span style=&quot;color: #666666; text-align: left;&quot;&gt;record &lt;/span&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: #666666; text-align: left;&quot;&gt;name (M): name of record, String &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;namespace (O): 패키지, String &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;doc (O): record documentation, String &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;aliases (O): alias 지정, String&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&amp;nbsp; &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;fields (M): record의 속성들, Array &lt;/span&gt;&lt;/span&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: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;name (M): field name, String &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;doc (O): field documentation, String &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;type (M): dataType of field &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;default (O, default): field의 default 값 지정 &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;order (O): ascending, descending, ignore &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;aliases: field alias 지정, String&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Enums&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&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;span style=&quot;color: #666666; text-align: left;&quot;&gt;name (M): name of record, String &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;namespace (O): 패키지, String &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;doc (O): record documentation, String &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;aliases (O): alias 지정, String &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;symbols (M): unique 한 value 지정, Array&amp;lt;String&amp;gt; &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;default (O, default): default 값 지정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;Array&lt;/span&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;span style=&quot;color: #666666; text-align: left;&quot;&gt;items (M): element의 type, String &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;default (O, default): default 값 지정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;map&lt;/span&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;span style=&quot;color: #666666; text-align: left;&quot;&gt;values (M): element의 type, String &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;default (O, default): default 값 지정&lt;/span&gt;&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;default values&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;611&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B5AwD/btsgEMeR7bB/onQ69LoXrYnVPk5viQjVCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B5AwD/btsgEMeR7bB/onQ69LoXrYnVPk5viQjVCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B5AwD/btsgEMeR7bB/onQ69LoXrYnVPk5viQjVCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB5AwD%2FbtsgEMeR7bB%2FonQ69LoXrYnVPk5viQjVCK%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;332&quot; height=&quot;611&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;611&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;Example&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Record&lt;/h4&gt;
&lt;pre id=&quot;code_1684728153530&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;record&quot;,
  &quot;name&quot;: &quot;LongList&quot;,
  &quot;aliases&quot;: [&quot;LinkedLongs&quot;],                      // old name for this
  &quot;fields&quot; : [
    {&quot;name&quot;: &quot;value&quot;, &quot;type&quot;: &quot;long&quot;},             // each element has a long
    {&quot;name&quot;: &quot;next&quot;, &quot;type&quot;: [&quot;null&quot;, &quot;LongList&quot;]} // optional next element
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Enum&lt;/h4&gt;
&lt;pre id=&quot;code_1684728166847&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;type&quot;: &quot;record&quot;,
  &quot;name&quot;: &quot;LongList&quot;,
  &quot;aliases&quot;: [&quot;LinkedLongs&quot;],                      // old name for this
  &quot;fields&quot; : [
    {&quot;name&quot;: &quot;value&quot;, &quot;type&quot;: &quot;long&quot;},             // each element has a long
    {&quot;name&quot;: &quot;next&quot;, &quot;type&quot;: [&quot;null&quot;, &quot;LongList&quot;]} // optional next element
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Serialized Data&lt;/h4&gt;
&lt;pre id=&quot;code_1684728186447&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import avro.schema
from avro.datafile import DataFileReader, DataFileWriter
from avro.io import DatumReader, DatumWriter

schema = avro.schema.parse(open(&quot;user.avsc&quot;).read())  # need to know the schema to write

writer = DataFileWriter(open(&quot;users.avro&quot;, &quot;w&quot;), DatumWriter(), schema)
writer.append({&quot;name&quot;: &quot;Alyssa&quot;, &quot;favorite_number&quot;: 256})
writer.append({&quot;name&quot;: &quot;Ben&quot;, &quot;favorite_number&quot;: 7, &quot;favorite_color&quot;: &quot;red&quot;})
writer.close()


0000000    O   b   j 001 004 026   a   v   r   o   .   s   c   h   e   m
0000020    a 272 003   {   &quot;   t   y   p   e   &quot;   :       &quot;   r   e   c
0000040    o   r   d   &quot;   ,       &quot;   n   a   m   e   s   p   a   c   e
0000060    &quot;   :       &quot;   e   x   a   m   p   l   e   .   a   v   r   o
0000100    &quot;   ,       &quot;   n   a   m   e   &quot;   :       &quot;   U   s   e   r
0000120    &quot;   ,       &quot;   f   i   e   l   d   s   &quot;   :       [   {   &quot;
0000140    t   y   p   e   &quot;   :       &quot;   s   t   r   i   n   g   &quot;   ,
0000160        &quot;   n   a   m   e   &quot;   :       &quot;   n   a   m   e   &quot;   }
0000200    ,       {   &quot;   t   y   p   e   &quot;   :       [   &quot;   i   n   t
0000220    &quot;   ,       &quot;   n   u   l   l   &quot;   ]   ,       &quot;   n   a   m
0000240    e   &quot;   :       &quot;   f   a   v   o   r   i   t   e   _   n   u
0000260    m   b   e   r   &quot;   }   ,       {   &quot;   t   y   p   e   &quot;   :
0000300        [   &quot;   s   t   r   i   n   g   &quot;   ,       &quot;   n   u   l
0000320    l   &quot;   ]   ,       &quot;   n   a   m   e   &quot;   :       &quot;   f   a
0000340    v   o   r   i   t   e   _   c   o   l   o   r   &quot;   }   ]   }
0000360  024   a   v   r   o   .   c   o   d   e   c  \b   n   u   l   l
0000400   \0 211 266   / 030 334   ˪  **   P 314 341 267 234 310   5 213
0000420    6 004   ,  \f   A   l   y   s   s   a  \0 200 004 002 006   B
0000440    e   n  \0 016  \0 006   r   e   d 211 266   / 030 334   ˪  **
0000460    P 314 341 267 234 310   5 213   6
0000471&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;h2 data-ke-size=&quot;size26&quot;&gt;사용성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IDE Support&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5IcrG/btsgYzrqJ75/4YjwZnnsFI82VqBB6PTQEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5IcrG/btsgYzrqJ75/4YjwZnnsFI82VqBB6PTQEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5IcrG/btsgYzrqJ75/4YjwZnnsFI82VqBB6PTQEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5IcrG%2FbtsgYzrqJ75%2F4YjwZnnsFI82VqBB6PTQEk%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;982&quot; height=&quot;712&quot; data-origin-width=&quot;982&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pFUYz/btsgNX7rQS9/uWsFr8yHFXkfDkXYtKZn60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pFUYz/btsgNX7rQS9/uWsFr8yHFXkfDkXYtKZn60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pFUYz/btsgNX7rQS9/uWsFr8yHFXkfDkXYtKZn60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpFUYz%2FbtsgNX7rQS9%2FuWsFr8yHFXkfDkXYtKZn60%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;1064&quot; height=&quot;197&quot; data-origin-width=&quot;1064&quot; data-origin-height=&quot;197&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Import&lt;/h3&gt;
&lt;pre id=&quot;code_1684728255849&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;....

&amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.apache.avro&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;avro&amp;lt;/artifactId&amp;gt;
        &amp;lt;version&amp;gt;1.11.0&amp;lt;/version&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;

....

&amp;lt;build&amp;gt;
    &amp;lt;plugins&amp;gt;
        &amp;lt;plugin&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.avro&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;avro-maven-plugin&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;1.11.0&amp;lt;/version&amp;gt;
            &amp;lt;executions&amp;gt;
                &amp;lt;execution&amp;gt;
                    &amp;lt;phase&amp;gt;generate-sources&amp;lt;/phase&amp;gt;
                    &amp;lt;goals&amp;gt;
                        &amp;lt;goal&amp;gt;schema&amp;lt;/goal&amp;gt;
                    &amp;lt;/goals&amp;gt;
                    &amp;lt;configuration&amp;gt;
                        &amp;lt;sourceDirectory&amp;gt;${project.basedir}/src/main/resources/avro/&amp;lt;/sourceDirectory&amp;gt;
                        &amp;lt;outputDirectory&amp;gt;${project.basedir}/src/main/java/&amp;lt;/outputDirectory&amp;gt;
                    &amp;lt;/configuration&amp;gt;
                &amp;lt;/execution&amp;gt;
            &amp;lt;/executions&amp;gt;
        &amp;lt;/plugin&amp;gt;
        &amp;lt;plugin&amp;gt;
            &amp;lt;groupId&amp;gt;org.apache.maven.plugins&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;maven-compiler-plugin&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;3.8.1&amp;lt;/version&amp;gt;
            &amp;lt;configuration&amp;gt;
                &amp;lt;source&amp;gt;1.8&amp;lt;/source&amp;gt;
                &amp;lt;target&amp;gt;1.8&amp;lt;/target&amp;gt;
            &amp;lt;/configuration&amp;gt;
        &amp;lt;/plugin&amp;gt;
    &amp;lt;/plugins&amp;gt;
&amp;lt;/build&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Schema 작성&lt;/h3&gt;
&lt;pre id=&quot;code_1684728271155&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;name&quot;: &quot;Device&quot;,
  &quot;namespace&quot;: &quot;com.xx.xxx.device&quot;,
  &quot;type&quot;: &quot;record&quot;,
  &quot;fields&quot;: [
    {
      &quot;name&quot;: &quot;id&quot;,
      &quot;type&quot;: &quot;long&quot;
    },
    {
      &quot;name&quot;: &quot;Capability&quot;,
      &quot;type&quot;: {
        &quot;name&quot;: &quot;Capability&quot;,
        &quot;namespace&quot;: &quot;com.xx.xxx.device.capability&quot;,
        &quot;type&quot;: &quot;record&quot;,
        &quot;fields&quot;: [
          {
            &quot;name&quot;: &quot;name&quot;,
            &quot;type&quot;: &quot;string&quot;
          },
          {
            &quot;name&quot;: &quot;value&quot;,
            &quot;type&quot;: &quot;string&quot;
          }
        ]
      }
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Serialize&lt;/h3&gt;
&lt;pre id=&quot;code_1684728353350&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void serializeTestData() throws IOException {
    Device device = Device.newBuilder()
        .setId(1L)
        .setCapability(Capability.newBuilder()
            .setName(&quot;light&quot;)
            .setValue(&quot;on&quot;)
            .build())
        .build();

    DatumWriter&amp;lt;Device&amp;gt; deviceDatumWriter;
    DataFileWriter&amp;lt;Device&amp;gt; dataFileWriter = null;
    try {
      deviceDatumWriter = new SpecificDatumWriter&amp;lt;&amp;gt;();
      dataFileWriter = new DataFileWriter&amp;lt;&amp;gt;(deviceDatumWriter);
      dataFileWriter.create(device.getSchema(), new File(&quot;test.avro&quot;));
      dataFileWriter.append(device);
    } catch (IOException e) {
      throw new RuntimeException(e);
    } finally {
      if (dataFileWriter != null) {
        dataFileWriter.close();
      }
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Deseiralize&lt;/h3&gt;
&lt;pre id=&quot;code_1684728376711&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void deserializeTestData() throws IOException {
    DatumReader&amp;lt;Device&amp;gt; deviceDatumReader = null;
    DataFileReader&amp;lt;Device&amp;gt; dataFileReader = null;

    try {
      deviceDatumReader = new SpecificDatumReader&amp;lt;&amp;gt;(Device.class);
      dataFileReader = new DataFileReader&amp;lt;&amp;gt;(new File(&quot;test.avro&quot;), deviceDatumReader);
      Device device = null;
      while (dataFileReader.hasNext()) {
        device = dataFileReader.next(device);
        System.out.println(device);
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      if (dataFileReader != null) {
        dataFileReader.close();
      }
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Result&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;66&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IGGuz/btsgKoYy8aD/GtkmjDH2VlR8iTBiMPJ3gK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IGGuz/btsgKoYy8aD/GtkmjDH2VlR8iTBiMPJ3gK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IGGuz/btsgKoYy8aD/GtkmjDH2VlR8iTBiMPJ3gK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIGGuz%2FbtsgKoYy8aD%2FGtkmjDH2VlR8iTBiMPJ3gK%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;304&quot; height=&quot;66&quot; data-origin-width=&quot;304&quot; data-origin-height=&quot;66&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;81&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bblijq/btsgEcdOSiU/1Cdlg1nTTnzz35j5GUrW50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bblijq/btsgEcdOSiU/1Cdlg1nTTnzz35j5GUrW50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bblijq/btsgEcdOSiU/1Cdlg1nTTnzz35j5GUrW50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbblijq%2FbtsgEcdOSiU%2F1Cdlg1nTTnzz35j5GUrW50%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;1061&quot; height=&quot;81&quot; data-origin-width=&quot;1061&quot; data-origin-height=&quot;81&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.confluent.io/platform/current/schema-registry/index.html#about-sr&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.confluent.io/platform/current/schema-registry/index.html#about-sr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@stephane.maarek/introduction-to-schemas-in-apache-kafka-with-the-confluent-schema-registry-3bf55e401321&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/@stephane.maarek/introduction-to-schemas-in-apache-kafka-with-the-confluent-schema-registry-3bf55e401321&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/@gaemi/kafka-%EC%99%80-confluent-schema-registry-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%8A%A4%ED%82%A4%EB%A7%88-%EA%B4%80%EB%A6%AC-1-cdf8c99d2c5c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://medium.com/@gaemi/kafka-%EC%99%80-confluent-schema-registry-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EC%8A%A4%ED%82%A4%EB%A7%88-%EA%B4%80%EB%A6%AC-1-cdf8c99d2c5c&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://avro.apache.org/docs/1.11.1/specification/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://avro.apache.org/docs/1.11.1/specification/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://11st-tech.github.io/2022/06/28/schema-registry-in-live11/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://11st-tech.github.io/2022/06/28/schema-registry-in-live11/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&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;</description>
      <category>개발 일지</category>
      <category>Avro</category>
      <category>Kafka</category>
      <category>kafka schema registry</category>
      <category>schema registry</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/48</guid>
      <comments>https://p-bear.tistory.com/48#entry48comment</comments>
      <pubDate>Mon, 22 May 2023 18:08:09 +0900</pubDate>
    </item>
    <item>
      <title>브라우저와 Redirect (feat. XMLHttpRequest)</title>
      <link>https://p-bear.tistory.com/47</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;XMLHttpRequest란?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&amp;nbsp;- All modern browsers have a built-in XMLHttpRequest object to request data from a server.&lt;br /&gt;비동기적 통신을 브라우저에서 제공하기 위한 W3C 표준 스펙이다.&lt;br /&gt;브라우저에서 제공하는 객체이며, jQuery의 Ajax 또한 XMLHttpRequest를 활용하여 구현되어 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redirect 란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AIuli/btsgDhTjqb2/ibkVtU9UOQy8KS6YTMHSO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AIuli/btsgDhTjqb2/ibkVtU9UOQy8KS6YTMHSO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AIuli/btsgDhTjqb2/ibkVtU9UOQy8KS6YTMHSO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAIuli%2FbtsgDhTjqb2%2FibkVtU9UOQy8KS6YTMHSO1%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;731&quot; height=&quot;388&quot; data-origin-width=&quot;731&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;요청 client에게 Server가 다른 곳으로 다시 요청하기 위한 HTTP Response Status이다.&lt;br /&gt;&amp;nbsp;- 301 Moved Permanently: 요청한 리소스의 URI가 변경됨,&amp;nbsp;redirect 시 method가 GET으로 자동 변경&lt;br /&gt;&amp;nbsp;- 302 Found: 요청한 리소스의 URI가 일시적으로 변경됨, redirect 시 method가 GET으로 자동 변경&lt;br /&gt;&amp;nbsp;- 307 Temporary Redirect: 요청한 리소스가 다른 URI에 있으며, 302와 동일하지만, method가 변경 안됩니다.&lt;br /&gt;&amp;nbsp;- 308 Permanent Redurect: 요청한 리소스가 영구적으로 변경됨, 301과 동일하지만, method가 변경 안됩니다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;즉 서버에서 Client를 다른 URI로 넘기고 싶을 때 주로 사용된다.&lt;br /&gt;OAuth 2.0 스펙에서, authorization 관련 요청 시, 토큰을 redirect를 통해서 전달한다.&lt;br /&gt;서버에서 3xx HttpStatus로 응답하면 Client는 Response Header의 Location 정보를 가지고 Redirect를 수행한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;XMLHttpRequest의 Redirect 응답&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;XMLHttpRequest는 브라우저에서 제공하는 Object이다.&lt;br /&gt;따라서 해당 Object의 통신은 브라우저가 구현했다.&lt;br /&gt;기본적으로 XMLHttpRequest는 Redirect를 자동으로 수행한다.&lt;br /&gt;&lt;br /&gt;또한 XHR의 fetch 메서드에서 Redirect에 대한 핸들링을 설정할 수 있다.&lt;/blockquote&gt;
&lt;pre id=&quot;code_1684504527710&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fetch(url, {
    headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
    },
    redirect: &quot;follow&quot;,
}).then(function (response) {
    if (response.status === 401) {
        alert(&quot;로그인에 실패하였습니다.&quot;);
        console.log(response.status);
        return;
    }

    console.log(response.url);
    window.location.href = response.url;
});&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;개인 서버의 로그인 페이지의 script 중 일부이다.&lt;br /&gt;redirect는 3가지 옵션을 지원한다.&lt;br /&gt;follow: 항상 redirect를 수행한다. 즉 send 이후 제공받는 response 객체는 redirect 된 응답 값이다.&lt;br /&gt;manual: redirect를 수행하지 않는다. 하지만, 보안상의 이유로 Location 값을 response로부터 받을 수 없다.&lt;br /&gt;error: redirect status를 오류로 인식한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이슈사항 및 work around&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Flutter 프로젝트 진행 중&lt;br /&gt;OAuth 2.0 authorization_code의 redirect 되기 전 응답의 Loaction 헤더값을 핸들링하고 싶었지만&lt;br /&gt;해당 값을 불러올 수 없었다.&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;기본적으로 Flutter의 기본 io 라이브러리의 web build는 XHR 사용하여 Flutter에 맞게 구현되어 있다.&lt;br /&gt;하지만, 한번 레핑 되는 과정에서 redirect의 핸들링을 항상 follow 하도록 구현되어 있다.&lt;br /&gt;결국 XHR을 직접 컨트롤하기 위해, 단일 페이지로 구성된 HTML + javascript로 따로 구현했다...&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Status&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.mozilla.org/ko/docs/Web/HTTP/Status&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3schools.com/xml/xml_http.asp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.w3schools.com/xml/xml_http.asp&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>browser</category>
      <category>redirect</category>
      <category>w3c</category>
      <category>XMLHttpRequest</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/47</guid>
      <comments>https://p-bear.tistory.com/47#entry47comment</comments>
      <pubDate>Fri, 19 May 2023 23:02:06 +0900</pubDate>
    </item>
    <item>
      <title>#7 3rd OAuth 연동 (google)</title>
      <link>https://p-bear.tistory.com/46</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;390&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cSOrpC/btsgDNJ4SPE/bG9TKhnVU8MvbI3yPtuKq0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cSOrpC/btsgDNJ4SPE/bG9TKhnVU8MvbI3yPtuKq0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cSOrpC/btsgDNJ4SPE/bG9TKhnVU8MvbI3yPtuKq0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcSOrpC%2FbtsgDNJ4SPE%2FbG9TKhnVU8MvbI3yPtuKq0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;780&quot; height=&quot;390&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;390&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;개인 서버의 인증 시스템과 타 인증시스템의 Integration에 대한 설계를 진행해 보았다.&lt;br /&gt;OAuth2.0 시스템은 타 인증 시스템에 연동할 수 있는 스펙을 가지고 있기 때문에&lt;br /&gt;비교적 수월한 설계가 가능했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인증/인가 정책&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;인증 시스템의 기본 정책에 대한 결정이 필요했다.&lt;br /&gt;1. 인증은 무엇을 기준으로 할 것인가?&lt;br /&gt;2. 인가는 무엇을 기준으로 할 것인가?&lt;br /&gt;&lt;br /&gt;결정 사항&lt;br /&gt;1. 인증은 내 서버의 계정을 기준으로 한다.&lt;br /&gt;&amp;nbsp;- 무조건 내 서버 계정으로 발급받은 토큰으로 인증을 수행한다.&lt;br /&gt;2. 외부인증은 내 서버의 계정에 1:다 매핑하며, 인증된 내 서버 계정의 부가정보로 정의한다.&lt;br /&gt;3. 인가는 내 서버 계정이 발급받은 토큰의 scope를 기준으로 한다.&lt;br /&gt;4. 토큰 업그레이드를 통해 토큰의 인가를 관리할 수 있도록 한다.&lt;br /&gt;&amp;nbsp;- 토큰 업그레이드는 토큰은 그대로 두고, 토큰에 매핑된 부가정보만 추가 / 변경되는 프로세스&lt;br /&gt;5. 외부 인증시스템과의 연동 또한 토큰 업그레이드 방식으로 진행한다.&lt;br /&gt;&amp;nbsp;- 외부 계정 연동은 내 서버 계정에 부가 정보를 추가하는 방식&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;토큰 발급 / 토큰 업그레이드 Flow&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;북극곰은콜라 인증 Sequence.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;1113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVk5bx/btsgCiKSgxT/PsfKev2JZ6LL7w7EzJGJs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVk5bx/btsgCiKSgxT/PsfKev2JZ6LL7w7EzJGJs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVk5bx/btsgCiKSgxT/PsfKev2JZ6LL7w7EzJGJs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVk5bx%2FbtsgCiKSgxT%2FPsfKev2JZ6LL7w7EzJGJs1%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;688&quot; height=&quot;1113&quot; data-filename=&quot;북극곰은콜라 인증 Sequence.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;1113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;title&amp;nbsp;북극곰은콜라&amp;nbsp;인증&amp;nbsp;Sequence&lt;br /&gt;&amp;nbsp;&amp;nbsp;&lt;br /&gt;note&amp;nbsp;over&amp;nbsp;Client,&amp;nbsp;LoginPage,&amp;nbsp;GateWay_Auth,&amp;nbsp;Main&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;일반&amp;nbsp;인증&amp;nbsp;(Authorization_code)&lt;br /&gt;end&amp;nbsp;note&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Client-&amp;gt;GateWay_Auth:&amp;nbsp;인증&amp;nbsp;요청&amp;nbsp;(GET&amp;nbsp;/authorize)&lt;br /&gt;GateWay_Auth-&amp;gt;Client:&amp;nbsp;Redirect&amp;nbsp;(308)&lt;br /&gt;Client-&amp;gt;LoginPage:&amp;nbsp;Redirect&lt;br /&gt;LoginPage-&amp;gt;GateWay_Auth:&amp;nbsp;인증&amp;nbsp;요청&amp;nbsp;(GET&amp;nbsp;/authroize)\nWith&amp;nbsp;id/pw&lt;br /&gt;GateWay_Auth-&amp;gt;LoginPage:&amp;nbsp;Redirect&amp;nbsp;(308)&lt;br /&gt;LoginPage-&amp;gt;Client:&amp;nbsp;Redirect&amp;nbsp;with&amp;nbsp;token&lt;br /&gt;&lt;br /&gt;note&amp;nbsp;over&amp;nbsp;Client,&amp;nbsp;LoginPage,&amp;nbsp;GateWay_Auth,&amp;nbsp;Main,&amp;nbsp;Google&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;구글&amp;nbsp;연동&lt;br /&gt;end&amp;nbsp;note&lt;br /&gt;&lt;br /&gt;Client-&amp;gt;Google:&amp;nbsp;인증&amp;nbsp;요청&amp;nbsp;(Authorization_code)&lt;br /&gt;Google-&amp;gt;Client:&amp;nbsp;Redirect&amp;nbsp;(with&amp;nbsp;code)&lt;br /&gt;Client-&amp;gt;GateWay_Auth:&amp;nbsp;POST&amp;nbsp;/oauth/token/google\n(my_token&amp;nbsp;+&amp;nbsp;Google_token)&lt;br /&gt;&lt;br /&gt;GateWay_Auth-&amp;gt;Google:&amp;nbsp;토큰&amp;nbsp;발급&lt;br /&gt;Google-&amp;gt;GateWay_Auth:&amp;nbsp;response&lt;br /&gt;GateWay_Auth-&amp;gt;GateWay_Auth:&amp;nbsp;parse&amp;nbsp;id_token(JWT)&lt;br /&gt;GateWay_Auth-&amp;gt;Main:&amp;nbsp;Check&amp;nbsp;Google&amp;nbsp;연동\n(id_token&amp;nbsp;subject로)&lt;br /&gt;Main-&amp;gt;GateWay_Auth:&amp;nbsp;response&lt;br /&gt;&lt;br /&gt;alt&amp;nbsp;not&amp;nbsp;연동&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;GateWay_Auth-&amp;gt;GateWay_Auth:&amp;nbsp;Get&amp;nbsp;User&amp;nbsp;From&amp;nbsp;Token&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;GateWay_Auth-&amp;gt;Main:&amp;nbsp;POST&amp;nbsp;연동&amp;nbsp;정보&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Main-&amp;gt;GateWay_Auth:&amp;nbsp;response&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;GateWay_Auth-&amp;gt;GateWay_Auth:&amp;nbsp;Token&amp;nbsp;Upgrade\n토큰&amp;nbsp;연동정보&amp;nbsp;추가&lt;br /&gt;end&lt;br /&gt;&lt;br /&gt;GateWay_Auth-&amp;gt;Client:&amp;nbsp;response&amp;nbsp;upgraded&amp;nbsp;Token&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;일반인증:&lt;br /&gt;기본적으로 OAuth2.0 Authorization의 implicit grant 방식을 변형하여 사용하여 인증을 수행한다.&lt;br /&gt;custom parameter로 id / pw에 대한 정보를 받아서, username / password로 인증을 수행&lt;br /&gt;username / password가 없을 시 로그인 페이지로 redirect&lt;br /&gt;username / password 검증으로 인증 통과 시 토큰 발급&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;구글 아이디 연동:&lt;br /&gt;일반인증을 통한 토큰으로 1차 인증&lt;br /&gt;구글에서 발급받은 authorization_code를 받아서 구글에서 토큰 발급&lt;br /&gt;일반토큰의 accountId를 통해 연동이력확인, 없을 시 google id_token의 JWT 값을 파싱 하여 Google 연동 POST&lt;br /&gt;연동 후 토큰 업그레이드 진행 (구글 토큰에 대한 정보 account 정보에 매핑)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;https://developers.google.com/identity/protocols/oauth2?hl=ko#2.-obtain-an-access-token-from-the-google-authorization-server.&lt;/li&gt;
&lt;li&gt;https://developers.google.com/identity/protocols/oauth2/native-app?hl=ko&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>[완] 개인서버 개발/공통 서비스 개발(완)</category>
      <category>Google OAuth</category>
      <category>GoogleApi</category>
      <category>OAuth</category>
      <category>OAuth2</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/46</guid>
      <comments>https://p-bear.tistory.com/46#entry46comment</comments>
      <pubDate>Fri, 19 May 2023 22:37:28 +0900</pubDate>
    </item>
    <item>
      <title>#6 Webflux기반 OAuth2서버 + gateway 구축</title>
      <link>https://p-bear.tistory.com/45</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;124&quot; data-origin-height=&quot;123&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRxESR/btsfdrwzsEd/hJKWN3D1TVDQqvJEOoueZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRxESR/btsfdrwzsEd/hJKWN3D1TVDQqvJEOoueZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRxESR/btsfdrwzsEd/hJKWN3D1TVDQqvJEOoueZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRxESR%2FbtsfdrwzsEd%2FhJKWN3D1TVDQqvJEOoueZK%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;124&quot; height=&quot;123&quot; data-origin-width=&quot;124&quot; data-origin-height=&quot;123&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;서비스를 위해서&amp;nbsp; 서버에 접근제어를 위한 인증서버가 필요했다.&lt;br /&gt;&lt;b&gt;개인적으로 정리한 요구사항:&lt;/b&gt;&lt;br /&gt;&amp;nbsp;- 개인 서버의 계정을 기반으로 인증을 수행할 수 있어야 한다.&lt;br /&gt;&amp;nbsp;- 내부적인 서비스 호출에 인가를 받을 수 있는 구조여야 한다.&lt;br /&gt;&amp;nbsp;- OAuth2.0 최소 스펙 이상 동작할 수 있어야 한다.&lt;br /&gt;&amp;nbsp;- 추후 외부 OAuth 프로토콜과 연계 될 수 있는 구조여야 한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gateway OAuth 2.0 서버 개요&lt;/h2&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; Server Architecture&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;인증서버구축.drawio.png&quot; data-origin-width=&quot;1083&quot; data-origin-height=&quot;1772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1J4nM/btsfOICOsuu/yFGcHvPBzSk6F6w49a2td1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1J4nM/btsfOICOsuu/yFGcHvPBzSk6F6w49a2td1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1J4nM/btsfOICOsuu/yFGcHvPBzSk6F6w49a2td1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1J4nM%2FbtsfOICOsuu%2FyFGcHvPBzSk6F6w49a2td1%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;1083&quot; height=&quot;1772&quot; data-filename=&quot;인증서버구축.drawio.png&quot; data-origin-width=&quot;1083&quot; data-origin-height=&quot;1772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;전체적인 서버구성에 추가적인 요소들이 있다.&lt;br /&gt;1. Nginx의 추가&lt;br /&gt;&amp;nbsp;- WebServer의 역할을 하며, 개인 서버의 유일한 Entry Point&lt;br /&gt;&amp;nbsp;- URL Prefix를 통해서 각 서버로 연동을 해준다.&lt;br /&gt;&amp;nbsp;- Web Resource (static page)에 대한 제공을 한다.&lt;br /&gt;&lt;br /&gt;2. Gateway Auth Server&lt;br /&gt;&amp;nbsp;- API 서버로 접근하기 위한 Entry Point 역할을 한다.&lt;br /&gt;&amp;nbsp;- Nginx로 /gateway로 접근 가능&lt;br /&gt;&amp;nbsp;- API 서버로 접근할 수 있는 유일한 루트&lt;br /&gt;&amp;nbsp;- API에 대한 인증 및 인가를 수행한다. (OAuth 2.0 기반)&lt;br /&gt;&amp;nbsp;- Client에 대한 관리도 진행한다.&lt;br /&gt;&lt;br /&gt;3. Redis&lt;br /&gt;&amp;nbsp;- 토큰 등 캐싱을 위한 데이터를 임시 보관한다.&lt;br /&gt;&amp;nbsp;- 캐시 DB로 역할을 FIX&lt;/blockquote&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; Gateway OAuth 서버 구축&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt; 앞으로 늘어날 서비스들로 라우팅할 수 있는 Gateway 서버가 필요했다.&lt;br /&gt;또한 해당 포인트에서 인증 및 인가를 수행하는 것이 효율적이라는 판단으로 해당 서비스에 구현했다.&lt;br /&gt;이 아이디어는 Line에서 소개한 다음 글에서 얻었다.&lt;br /&gt;https://engineering.linecorp.com/ko/blog/service-authentication-sidecar-proxy&lt;br /&gt;&lt;br /&gt;개인서버를 스케일아웃 할 수 없는 구조에서, 인증서버를 따로 두는 것은 비효율적이라 판단하고&lt;br /&gt;gateway에서 인증 / 인가 서비스를 직접 하여 불필요한 트레픽을 줄였다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;난관&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Cloud Gateway를 기반으로 서버를 구축했다.&lt;br /&gt;문제는 Webflux 기반 Spring Security 프로젝트에서, 더 이상 인증서버에 대한 기능 (토큰 발급 등 OAuth2.0 스펙)의 지원을 중단했다는 점이다.&lt;br /&gt;따라서 Spring 프로젝트로 따로 OAuth2.0 스펙을 구현해야하는 상황이다.&lt;br /&gt;&lt;br /&gt;OAuth2.0은 오래된 스펙인 만큼 해당 서버를 구축할 수 있는 라이브러리가 있을 것이라 기대했다.&lt;br /&gt;하지만 2가지 문제가 있었다.&lt;br /&gt;1. OAuth 2.0 을 사용하는 client 입장의 라이브러리와 SDK가 대부분&lt;br /&gt;2. 대부분의 라이브러리는 블로킹 기반의 서버에서 활용 가능하도록 API가 구성되어있음&lt;br /&gt;&lt;br /&gt;처음 시도했던 방법은 블로킹 기반으로 설계된 라이브러리를 비동기 방식으로 Wrapping해서 구현해 보려 했다.&lt;br /&gt;하지만, 대부분 Datasource를 제공하는 방식에서 문제가 생겼다.&lt;br /&gt;결국 DataSource로 부터 데이터를 동기적으로 세팅하도록 설계가 되어있어서, Integrate하는데 문제가 발생했다.&lt;br /&gt;&lt;br /&gt;그래도 직접 구현하기에는 스펙 자체가 작지 않아서 다른 방식을 찾아보았다.&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Nimbus라는 OAuth2.0 기반 엔터프라이즈 인테그레이션 서비스를 제공하는 회사?단체?가 있다.&lt;br /&gt;Numbus에서 자신들의 OAuth2.0 서버를 기존 서버에 인테그레이션 할 수 있는 SDK를 제공한다.&lt;br /&gt;해당 오픈소스에는 OAuth2.0의 인터페이스를 전체적으로 제공하고있다.&lt;br /&gt;하지만 인증 로직에 대한 부분은 구현되어있지 않았다.&lt;br /&gt;&lt;br /&gt;해당 SDK를 활용해서&lt;br /&gt;인증 요청 -&amp;gt; (SDK) -&amp;gt; 인증로직 (구현) -&amp;gt; (SDK) -&amp;gt; 응답&lt;br /&gt;하는 방식으로 최소한의 로직 구현으로 인증서버를 구축하기로 결정했다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OAuth 2.0 서비스 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 라이브러리&lt;/h3&gt;
&lt;pre id=&quot;code_1684056693765&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation(&quot;org.springframework.boot:spring-boot-starter-webflux&quot;)
implementation(&quot;org.springframework.cloud:spring-cloud-starter-gateway&quot;)
implementation(&quot;org.springframework.boot:spring-boot-starter-security&quot;)
implementation(&quot;org.springframework.boot:spring-boot-starter-data-r2dbc&quot;)
implementation(&quot;org.springframework.boot:spring-boot-starter-data-redis-reactive&quot;)

implementation(&quot;com.nimbusds:c2id-server-sdk:4.53&quot;)
implementation(&quot;org.mariadb:r2dbc-mariadb:1.0.3&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kotlin으로 구현된 서버이며&lt;br /&gt;Spring Webflux 기반으로 한정된 서버 리소스를 최대할 활용하고자 했다.&lt;br /&gt;Spring Security로 인증 / 인가에 대한 기능을 구현하고&lt;br /&gt;nimbus 라이브러리를 활용해 인증을 위한 서비스를 구현했다.&lt;br /&gt;Database를 논블로킹으로 활용 할 수 있게 R2DBC 인터페이스를 사용했으며&lt;br /&gt;연동되는 DB는Redis와 MariaDB 이다.&lt;/blockquote&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;Client Service&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;783&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJnANR/btsfPCWT95Q/IG4FosJ3sO0nHumsFKpke0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJnANR/btsfPCWT95Q/IG4FosJ3sO0nHumsFKpke0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJnANR/btsfPCWT95Q/IG4FosJ3sO0nHumsFKpke0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJnANR%2FbtsfPCWT95Q%2FIG4FosJ3sO0nHumsFKpke0%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;1494&quot; height=&quot;783&quot; data-origin-width=&quot;1494&quot; data-origin-height=&quot;783&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;OAuth 2.0 의 Client에 대한 create, delete, verify 기능을 제공할 수 있도록 구현했다.&lt;br /&gt;&lt;br /&gt;패키지 외부적으로 ClientHandler를 통해서 Client에 대한 CRD를 제공한다.&lt;br /&gt;ClientAuthenticationVerifier는 Nimbus의 Client Verifier를 위한 모듈이며, 이는 Nimbus의 Verifier를 논블로킹 방식으로 레핑했다.&lt;/blockquote&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;OAuth Service&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1327&quot; data-origin-height=&quot;1270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI5NYF/btsfjKhxYGS/5EZKKHiDvngtVzNqlhSM8K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI5NYF/btsfjKhxYGS/5EZKKHiDvngtVzNqlhSM8K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI5NYF/btsfjKhxYGS/5EZKKHiDvngtVzNqlhSM8K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI5NYF%2FbtsfjKhxYGS%2F5EZKKHiDvngtVzNqlhSM8K%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;1327&quot; height=&quot;1270&quot; data-origin-width=&quot;1327&quot; data-origin-height=&quot;1270&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;OAuthHandler: 토큰 발급 요청을 적절하게 파싱하는 핸들러, Nimbus SDK를 적극 활용, Authoriz e 요청을 위한 로직은 이쪽에 구현&lt;br /&gt;TokenService: 토큰 발급을 위한 로직들이 구현되어있는 핸들러, 구현된 사항으로는 Authorize Code, Authorize Token, Resource Owner 방식 3가지 이다.&lt;br /&gt;TokenStore: 토큰에 대한 CRUD를 담당하는 클래스.&lt;br /&gt;OAuthRedisRepository: 토큰을 Redis에 CRD를 하는 레파지토리&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인증 / 인가 서비스 구현&lt;/h2&gt;
&lt;pre id=&quot;code_1684058850579&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun securityWebFilterChain(
    serverHttpSecurity: ServerHttpSecurity,
    authManager: AuthManager,
    authenticationConverter: AuthenticationConverter): SecurityWebFilterChain {
    val filter = AuthenticationWebFilter(authManager)
    filter.setServerAuthenticationConverter(authenticationConverter)

    val authorizeExchangeSpec = serverHttpSecurity
        .authorizeExchange()

    // permitAll 세팅
    permitAllApiMap.forEach{ entry -&amp;gt;
        entry.value.forEach { uri -&amp;gt;
            authorizeExchangeSpec.pathMatchers(entry.key, uri).permitAll()
        }
    }

    authorizeExchangeSpec
        // main 서버 기본 scope -&amp;gt; main:*
        .pathMatchers(&quot;/main/**&quot;).hasAuthority(&quot;SCOPE_main:*&quot;)

    return authorizeExchangeSpec
        .and()
        .addFilterAfter(filter, SecurityWebFiltersOrder.AUTHENTICATION)

        .csrf().disable()
        .formLogin().disable()
        .httpBasic().disable()
        .logout().disable()
        .cors().configurationSource(corsConfigurationSource())
        .and()
        .build()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Security를 활용해서 인가에 대한 컨트롤을 수행한다.&lt;br /&gt;회원가입, 토큰 발급 등에 대해서 permitAll(), 나머지는 서비스별로 SCOPE를 확인 하도록 설정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GateWay Service&lt;/h2&gt;
&lt;pre id=&quot;code_1684058958381&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
fun proxyRouterFunction(routeLocatorBuilder: RouteLocatorBuilder): RouteLocator = routeLocatorBuilder.routes()
    .route(&quot;main&quot;) { predicateSpec -&amp;gt;
        predicateSpec
            .path(&quot;/main/**&quot;)
            .filters { it
                .filter(apiAccessControlFilter.apply(ApiAccessControlFilter.Config()))
                .rewritePath(&quot;^/main&quot;, &quot;&quot;)
            }
            .uri(mainRouteUrl)
    }
    .build()&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Spring Cloud Gate를 활용하여 구성&lt;br /&gt;/main으로 들어오는 요청을 main 서비스로 라우팅 한다.&lt;br /&gt;prefix는 제거해서 요청&lt;br /&gt;&lt;br /&gt;전체  flow 예시로&lt;br /&gt;/gateway/main/api/xxx&lt;br /&gt;Nginx에서 gateway가 지워지면서 gateway서버로 접근&lt;br /&gt;gateway filter단에서 토큰 검증&lt;br /&gt;/main을 지우면서 main 서버로 /api/xxx로 요청&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;추후 계획&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;기본 로그인 / 회원가입 페이지를 Niginx를 통해 제공하며&lt;br /&gt;묵시적 코드 플로우를 통해 토큰을 제공할 예정이다.&lt;br /&gt;전체적인 구성은&lt;br /&gt;개별 서비스 -&amp;gt; login 페이지 (redirectURI 포함) -&amp;gt; 로그인 수행 -&amp;gt; 개별 서비스로 redirect&lt;br /&gt;로 구성 되며, 로그인 페이지에 구글 연동 정도 추가 할 예정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://oauth.net/2/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://oauth.net/2/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc6749&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.rfc-editor.org/rfc/rfc6749&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://connect2id.com/products/server&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://connect2id.com/products/server&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;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;</description>
      <category>[완] 개인서버 개발/공통 서비스 개발(완)</category>
      <category>gateway</category>
      <category>OAuth2</category>
      <category>webflux</category>
      <category>인증서버</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/45</guid>
      <comments>https://p-bear.tistory.com/45#entry45comment</comments>
      <pubDate>Sun, 14 May 2023 19:14:21 +0900</pubDate>
    </item>
    <item>
      <title>Redis 설치 (ubuntu)</title>
      <link>https://p-bear.tistory.com/44</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;389&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2sVlb/btscPdANt6z/Wj7B1NcolsZ7jzhkypFc21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2sVlb/btscPdANt6z/Wj7B1NcolsZ7jzhkypFc21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2sVlb/btscPdANt6z/Wj7B1NcolsZ7jzhkypFc21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2sVlb%2FbtscPdANt6z%2FWj7B1NcolsZ7jzhkypFc21%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;389&quot; height=&quot;130&quot; data-origin-width=&quot;389&quot; data-origin-height=&quot;130&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치&lt;/h2&gt;
&lt;pre id=&quot;code_1682512427496&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// apt 업데이트
sudo apt-get update
sudo apt-get upgrade

// 설치
sudo apt-get install redis-server

// 설정
sudo vi /etc/redis/redis.conf
maxmemory 1g
maxmemory-policy allkeys-lru
bind xxx.xxx.xxx.xxx , xxx.xxx.xxx.xxx

// 재시작
sudo systemctl restart redis-server.service

// 확인
netstat -nlpt | grep 6379&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;h2 data-ke-size=&quot;size26&quot;&gt;방화벽 설정&lt;/h2&gt;
&lt;pre id=&quot;code_1683295433954&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo iptables -A INPUT -p tcp --dport 6379 -j ACCEPT
sudo netfilter-persistent save
sudo netfilter-persistent reload&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;h2 data-ke-size=&quot;size26&quot;&gt;Data 디렉토리 변경&lt;/h2&gt;
&lt;pre id=&quot;code_1683297318813&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// redis conf 수정
sudo vi /etc/redis/redis.conf

// dir 설정
dir {데이터 디렉토리}

// redis.service 수정
vi /etc/systemd/system/redis.service

// 디렉토리 관련 설정
ReadWriteDirectories=-{데이터 디렉토리}&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;&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://hayden-archive.tistory.com/429&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hayden-archive.tistory.com/429&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발 일지</category>
      <category>Redis</category>
      <category>redis 설치</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/44</guid>
      <comments>https://p-bear.tistory.com/44#entry44comment</comments>
      <pubDate>Wed, 26 Apr 2023 21:33:55 +0900</pubDate>
    </item>
    <item>
      <title>R2DBC history 및 issue</title>
      <link>https://p-bear.tistory.com/43</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;491&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KJ008/btsbCHWSjWK/TAVqQKdkkNiD6sqYdipXl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KJ008/btsbCHWSjWK/TAVqQKdkkNiD6sqYdipXl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KJ008/btsbCHWSjWK/TAVqQKdkkNiD6sqYdipXl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKJ008%2FbtsbCHWSjWK%2FTAVqQKdkkNiD6sqYdipXl0%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;491&quot; height=&quot;102&quot; data-origin-width=&quot;491&quot; data-origin-height=&quot;102&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Reactive Relational Database Connectivity&lt;br /&gt;SQL database에서 Reactive programming을 지원하기 위해 디자인된 새로운 Open Specification&lt;br /&gt;R2DBC를 쉽게 사용할 수 있도록 ORM의 사용법을 참고하여 설계하였으나, ORM Framework에서 제공하는 다양한 기능을 제공하지 않음.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;특징&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Based on the Reactive Streams specification.&amp;nbsp;&lt;br /&gt;&amp;nbsp;- provides a fully-reactive non-blocking API.&lt;br /&gt;&lt;br /&gt;Works with relational databases. &lt;br /&gt;&amp;nbsp;- JDBC의 Blocking 특성과 달리, R2DBC를 사용하면 Reactive API를 사용하여 SQL database로 작업할 수 있다.&lt;br /&gt;&lt;br /&gt;Supports scalable solutions.&lt;br /&gt;&amp;nbsp;- Reactive Stream을 사용하면 기존의 &quot;연결 당 하나의 스레드&quot;모델에서보다 강력하고 확장 가능한 접근 방식이 가능하다.&lt;br /&gt;&lt;br /&gt;Provides an open specification.&lt;br /&gt;&amp;nbsp;- R2DBC 는 open specification이며 드라이버 공급업체가 spec을 구현하고 클라이언트가 사용할 SPI(Service Provider Interface) 제공합니다.&lt;br /&gt;&amp;nbsp;- R2DBC는 최소한의 SPI를 목표로 한다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;구현체&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;clickhouse-r2dbc&amp;nbsp;- R2DBC wrapper of async Java client for ClickHouse.&lt;br /&gt;cloud-spanner-r2dbc&amp;nbsp;- driver for Google Cloud Spanner.&lt;br /&gt;jasync-sql&amp;nbsp;- R2DBC wrapper for Java &amp;amp; Kotlin Async Database Driver for MySQL and PostgreSQL (written in Kotlin).&lt;br /&gt;oracle-r2dbc&amp;nbsp;- native driver implemented for Oracle.&lt;br /&gt;r2dbc-h2&amp;nbsp;- native driver implemented for H2 as a test database.&lt;br /&gt;r2dbc-mariadb&amp;nbsp;- native driver implemented for MariaDB.&lt;br /&gt;r2dbc-mssql&amp;nbsp;- native driver implemented for Microsoft SQL Server.&lt;br /&gt;r2dbc-mysql&amp;nbsp;- native driver implemented for MySQL (previously&amp;nbsp;mirromutth/r2dbc-mysql).&lt;br /&gt;r2dbc-postgresql&amp;nbsp;- native driver implemented for PostgreSQL.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SPI History ~ 1.0&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;0.8&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;initial version&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;0.9&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Extended transaction definitions&lt;/li&gt;
&lt;li&gt;Improved Bind Parameter declararation&lt;/li&gt;
&lt;li&gt;Consumption&amp;nbsp;of&amp;nbsp;OUT&amp;nbsp;Parameters&lt;/li&gt;
&lt;li&gt;Consumption of Result Segments&lt;/li&gt;
&lt;li&gt;Lifecycle extension&lt;/li&gt;
&lt;li&gt;Refinement of Option&lt;/li&gt;
&lt;li&gt;Refinement of RowMetadata&lt;/li&gt;
&lt;li&gt;Lock&amp;nbsp;Wait&amp;nbsp;and&amp;nbsp;Statement&amp;nbsp;Timeouts&lt;/li&gt;
&lt;li&gt;Refined&amp;nbsp;Exceptions&amp;nbsp;for&amp;nbsp;Readable.get(&amp;hellip;)&amp;nbsp;and&amp;nbsp;Statement.bind(&amp;hellip;)&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;1.0&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Revised Result.getRowsUpdated() signature&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;b&gt;1.0 스펙 중 이전 버전이랑 하위호환이 안되게 하는 주범, update 시 update row를 Long -&amp;gt; Integer로 변경해서, update(save) 시 classCastException이 발생한다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Removed RowMetadata.getColumnNames() method&lt;/li&gt;
&lt;li&gt;TCK Extensions&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Data R2DBC&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzgqCY/btsbGwBi9mg/7CuK3gzy00f0t4QPkfAg5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzgqCY/btsbGwBi9mg/7CuK3gzy00f0t4QPkfAg5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzgqCY/btsbGwBi9mg/7CuK3gzy00f0t4QPkfAg5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzgqCY%2FbtsbGwBi9mg%2F7CuK3gzy00f0t4QPkfAg5K%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;1015&quot; height=&quot;631&quot; data-origin-width=&quot;1015&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.5116%;&quot;&gt;Spring Data R2DBC version&lt;/td&gt;
&lt;td style=&quot;width: 27.4419%;&quot;&gt;imported R2DBC SPI version&lt;/td&gt;
&lt;td style=&quot;width: 46.0465%;&quot;&gt;주요 기능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.5116%;&quot;&gt;1.0.0&lt;/td&gt;
&lt;td style=&quot;width: 27.4419%;&quot;&gt;0.8&lt;/td&gt;
&lt;td style=&quot;width: 46.0465%;&quot;&gt;initial major version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.5116%;&quot;&gt;1.1.0&lt;/td&gt;
&lt;td style=&quot;width: 27.4419%;&quot;&gt;0.8.1&lt;/td&gt;
&lt;td style=&quot;width: 46.0465%;&quot;&gt;r2dbcEntityTemplate 기능 추가&lt;br /&gt;query derivation 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.5116%;&quot;&gt;1.2.0&lt;/td&gt;
&lt;td style=&quot;width: 27.4419%;&quot;&gt;0.8.3&lt;/td&gt;
&lt;td style=&quot;width: 46.0465%;&quot;&gt;r2dbcAuditing 추가&lt;br /&gt;@Value support&lt;br /&gt;support Oracle R2DBC Driver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.5116%;&quot;&gt;1.3.0&lt;/td&gt;
&lt;td style=&quot;width: 27.4419%;&quot;&gt;0.8.4&lt;/td&gt;
&lt;td style=&quot;width: 46.0465%;&quot;&gt;Query By Example 기능 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.5116%;&quot;&gt;1.4.0&lt;/td&gt;
&lt;td style=&quot;width: 27.4419%;&quot;&gt;0.8.5&lt;/td&gt;
&lt;td style=&quot;width: 46.0465%;&quot;&gt;upgrade dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.5116%;&quot;&gt;1.5.0&lt;/td&gt;
&lt;td style=&quot;width: 27.4419%;&quot;&gt;0.9.1&lt;/td&gt;
&lt;td style=&quot;width: 46.0465%;&quot;&gt;upgrade dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 26.5116%;&quot;&gt;3.0.0&lt;/td&gt;
&lt;td style=&quot;width: 27.4419%;&quot;&gt;1.0.0&lt;/td&gt;
&lt;td style=&quot;width: 46.0465%;&quot;&gt;spring boot 3 대응&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Spring Boot Starter Data R2DBC version&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Spring Data R2DBC version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;2.3.2&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;1.1.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;2.4.0&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;1.2.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;2.5.0&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;1.3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;2.6.0&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;1.4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;2.7.0&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;1.5.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;3.0.0&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;3.0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL, MariaDB Driver&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관련 드라이버&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 108px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 10.0001%; height: 17px;&quot;&gt;DB Type&lt;/td&gt;
&lt;td style=&quot;width: 22.907%; height: 17px;&quot;&gt;Driver&lt;/td&gt;
&lt;td style=&quot;width: 12.7907%; height: 17px;&quot;&gt;develop by&lt;/td&gt;
&lt;td style=&quot;width: 54.3023%; height: 17px;&quot;&gt;Desc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 10.0001%; height: 40px;&quot; rowspan=&quot;3&quot;&gt;Mysql&lt;/td&gt;
&lt;td style=&quot;width: 22.907%; height: 40px;&quot;&gt;jasync-sql&lt;/td&gt;
&lt;td style=&quot;width: 12.7907%; height: 40px;&quot;&gt;jasync-sql&lt;/td&gt;
&lt;td style=&quot;width: 54.3023%; height: 40px;&quot;&gt;코틀린으로 개발된 드라이버&lt;br /&gt;MySQL, PostgreSQL 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 22.907%; height: 17px;&quot;&gt;r2dbc-mysql&lt;/td&gt;
&lt;td style=&quot;width: 12.7907%; height: 17px;&quot;&gt;mirrormutth&lt;/td&gt;
&lt;td style=&quot;width: 54.3023%; height: 17px;&quot;&gt;최초의 mysql r2dbc driver 구현체&lt;br /&gt;현재는 개발 중단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 22.907%; height: 17px;&quot;&gt;r2dbc-mysql&lt;/td&gt;
&lt;td style=&quot;width: 12.7907%; height: 17px;&quot;&gt;asyncer-io&lt;/td&gt;
&lt;td style=&quot;width: 54.3023%; height: 17px;&quot;&gt;mysql 5.5, 5.6, 5.7, 8.0 호환&lt;br /&gt;r2dbc-spi 1.0까지 호환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 10.0001%; height: 17px;&quot;&gt;MariaDB&lt;/td&gt;
&lt;td style=&quot;width: 22.907%; height: 17px;&quot;&gt;mariadb-connector-r2dbc&lt;/td&gt;
&lt;td style=&quot;width: 12.7907%; height: 17px;&quot;&gt;mariadb-corp&lt;/td&gt;
&lt;td style=&quot;width: 54.3023%; height: 17px;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;MariaDB 공식 r2dbc 지원 드라이버&lt;/b&gt;&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;p data-ke-size=&quot;size16&quot;&gt;MySQL 계열 사용 시 공식드라이버인 MariaDB를 사용하는 것이 안정적 일 듯하다&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;h2 data-ke-size=&quot;size26&quot;&gt;주저리주저리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신규 프로젝트에서 R2DBC 사용 도중 update 시 driver에서 오류가 올라와서 부랴부랴 알아봄.&lt;/li&gt;
&lt;li&gt;SPI 1.0 버전이 나온 것까진 알고 있었는데 interface상 하위호환이 안될 줄은 상상도 못 함...&lt;/li&gt;
&lt;li&gt;일단 버전에 맞는 mariaDB 구현체를 사용해서 해결한 상태.&lt;/li&gt;
&lt;li&gt;이슈 해결 후 조사한 게 억울해서 블로그에 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈 stacktrace&lt;/p&gt;
&lt;pre id=&quot;code_1682056829027&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;20230421 14:59:18.570 [db-sql-netty-thread-2] ERROR o.s.b.a.w.r.e.AbstractErrorWebExceptionHandler - [695cf522-1]  500 Server Error for HTTP PUT &quot;/dev/data&quot; 
java.lang.ClassCastException: class java.lang.Long cannot be cast to class java.lang.Integer (java.lang.Long and java.lang.Integer are in module java.base of loader 'bootstrap')
	at java.base/java.util.stream.Collectors.lambda$summingInt$19(Collectors.java:673)
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ Handler org.springframework.web.reactive.function.server.RouterFunctionDsl$PUT$2@10647471 [DispatcherHandler]
	*__checkpoint ⇢ com.pbear.mainfunctionalserver.common.log.LoggingWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ HTTP PUT &quot;/dev/data&quot; [ExceptionHandlingWebHandler]
Original Stack Trace:
		at java.base/java.util.stream.Collectors.lambda$summingInt$19(Collectors.java:673)
		at reactor.core.publisher.MonoStreamCollector$StreamCollectorSubscriber.onNext(MonoStreamCollector.java:132)
		at reactor.core.publisher.FluxFlatMap$FlatMapMain.tryEmitScalar(FluxFlatMap.java:489)
		at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:422)
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
		at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79)
		at reactor.core.publisher.FluxMap$MapSubscriber.onNext(FluxMap.java:122)
		at reactor.core.publisher.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:282)
		at reactor.core.publisher.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:863)
		at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1839)
		at reactor.core.publisher.MonoCompletionStage.lambda$subscribe$0(MonoCompletionStage.java:96)
		at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:930)
		at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:907)
		at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:506)
		at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2073)
		at com.github.jasync.sql.db.util.FutureUtilsKt.success(FutureUtils.kt:16)
		at com.github.jasync.sql.db.mysql.MySQLConnection.succeedQueryPromise$lambda-12(MySQLConnection.kt:361)
		at java.base/java.util.Optional.ifPresent(Optional.java:183)
		at com.github.jasync.sql.db.mysql.MySQLConnection.succeedQueryPromise(MySQLConnection.kt:360)
		at com.github.jasync.sql.db.mysql.MySQLConnection.onOk(MySQLConnection.kt:227)
		at com.github.jasync.sql.db.mysql.codec.MySQLConnectionHandler.channelRead0(MySQLConnectionHandler.kt:125)
		at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:99)
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
		at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
		at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
		at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318)
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
		at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
		at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
		at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
		at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
		at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
		at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)
		at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)
		at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)
		at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)
		at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
		at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
		at java.base/java.lang.Thread.run(Thread.java:834)&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://r2dbc.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://r2dbc.io/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://r2dbc.io/spec/1.0.0.RELEASE/spec/html/#changes&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://r2dbc.io/spec/1.0.0.RELEASE/spec/html/#changes&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/74972672/java-lang-integer-cannot-be-cast-to-class-java-lang-long-in-r2dbc-mysql-model-cl&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/74972672/java-lang-integer-cannot-be-cast-to-class-java-lang-long-in-r2dbc-mysql-model-cl&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-data-commons/wiki/Release-Trains&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/spring-projects/spring-data-commons/wiki/Release-Trains&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;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;</description>
      <category>개발 일지</category>
      <category>r2dbc</category>
      <category>r2dbc-mariadb</category>
      <category>r2dbc-mysql</category>
      <category>spring</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/43</guid>
      <comments>https://p-bear.tistory.com/43#entry43comment</comments>
      <pubDate>Fri, 21 Apr 2023 15:01:47 +0900</pubDate>
    </item>
    <item>
      <title>Mysql data directory 변경</title>
      <link>https://p-bear.tistory.com/42</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/usLZL/btsbglebIpV/1UK13wI19z72ZI5mKqLg90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/usLZL/btsbglebIpV/1UK13wI19z72ZI5mKqLg90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/usLZL/btsbglebIpV/1UK13wI19z72ZI5mKqLg90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FusLZL%2FbtsbglebIpV%2F1UK13wI19z72ZI5mKqLg90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;530&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;530&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;mysql 데이터를 마운트한 하드로 옮겨서 운영하거나 백업 등을 할 때 유용한 가이드 요약 번역&lt;br /&gt;ubuntu 용 가이드&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Data Directory 확인&lt;/h2&gt;
&lt;pre id=&quot;code_1681864499416&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// config 파일로 확인
sudo grep -R --color datadir /etc/mysql/*

// DB 내에서 확인
mysql&amp;gt; SELECT @@datadir;
+---------------------------+
| @@datadir                 |
+---------------------------+
| /var/lib/mysql            |
+---------------------------+
1 row in set (0.000 sec)&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;h2 data-ke-size=&quot;size26&quot;&gt;Data 복사&lt;/h2&gt;
&lt;pre id=&quot;code_1681864585277&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 옮길 폴더 생성 및 권한
mkdir /data
sudo chown mysql:mysql /data

// mysql stop
sudo systemctl stop mysql

// 복사
// rsync -avzh {from} {to}
sudo rsync -avzh /var/lib/mysql/ /data

// 보안 설정
sudo vi /etc/apparmor.d/tunables/alias

// 아래 한줄 추가
alias /var/lib/mysql -&amp;gt; /data,&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;h2 data-ke-size=&quot;size26&quot;&gt;설정 변경 및 확인&lt;/h2&gt;
&lt;pre id=&quot;code_1681864818149&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// config 파일에서 확인한 datadir 옵션을 변경
datadir = /data

// mysql 시작
sudo systemctl start mysql

// 접속 후 확인
mysql&amp;gt; SELECT @@datadir;
+---------------------------+
| @@datadir                 |
+---------------------------+
| /data                     |
+---------------------------+
1 row in set (0.000 sec)&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;&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://linuxhint.com/change_mysql_data_directory_ubuntu/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://linuxhint.com/change_mysql_data_directory_ubuntu/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개발 일지</category>
      <category>data directory</category>
      <category>MySQL</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/42</guid>
      <comments>https://p-bear.tistory.com/42#entry42comment</comments>
      <pubDate>Wed, 19 Apr 2023 09:41:59 +0900</pubDate>
    </item>
    <item>
      <title>Gradle 이란</title>
      <link>https://p-bear.tistory.com/41</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zMvO9/btsaVdOXqgR/aHfg72lbAvk7GFtLZlAzS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zMvO9/btsaVdOXqgR/aHfg72lbAvk7GFtLZlAzS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zMvO9/btsaVdOXqgR/aHfg72lbAvk7GFtLZlAzS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzMvO9%2FbtsaVdOXqgR%2FaHfg72lbAvk7GFtLZlAzS1%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;876&quot; height=&quot;306&quot; data-origin-width=&quot;876&quot; data-origin-height=&quot;306&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;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gradle 이란&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Groovy 기반의 빌드 툴&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;장점&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #091e42; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자유로운 빌드 설정&lt;/li&gt;
&lt;li&gt;동적인 빌드 설정 가능&lt;/li&gt;
&lt;li&gt;손쉬운 wrapper&lt;/li&gt;
&lt;li&gt;빠른 성능&lt;/li&gt;
&lt;li&gt;(취향) 가독성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Build&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Life Cycle&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #091e42; text-align: start;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;초기화 단계
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;init.gradle, gradle.properties 환경설정파일을 read&lt;/li&gt;
&lt;li&gt;settings.gradle 파일에 포함된 하위 프로젝트(모듈) 설정&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;설정 단계
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;모든 빌드 스크립트를 평가 및 DAG Create&lt;/li&gt;
&lt;li&gt;실행순서 결정&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&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;/ol&gt;
&lt;/li&gt;
&lt;/ol&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;Task&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;183&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9lOfH/btsaTZKdg6z/xK83vrc6eQ4mJUSSSUd9Vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9lOfH/btsaTZKdg6z/xK83vrc6eQ4mJUSSSUd9Vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9lOfH/btsaTZKdg6z/xK83vrc6eQ4mJUSSSUd9Vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9lOfH%2FbtsaTZKdg6z%2FxK83vrc6eQ4mJUSSSUd9Vk%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;183&quot; height=&quot;426&quot; data-origin-width=&quot;183&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;빌드 단계&lt;/blockquote&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;DAG&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;533&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BDOIA/btsaVhRoWXy/NubA1kJgQRVb9qG2JvOwl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BDOIA/btsaVhRoWXy/NubA1kJgQRVb9qG2JvOwl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BDOIA/btsaVhRoWXy/NubA1kJgQRVb9qG2JvOwl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBDOIA%2FbtsaVhRoWXy%2FNubA1kJgQRVb9qG2JvOwl0%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;533&quot; height=&quot;442&quot; data-origin-width=&quot;533&quot; data-origin-height=&quot;442&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Directed Acyclic Graph로 빌드 순서등을 가진 것&lt;br /&gt;예시) java plugin DAG&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Repositories &amp;amp; dependencies&lt;/h2&gt;
&lt;h3 id=&quot;Gradle-Repositories&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Repositories&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;필요할 때 가져와서 쓸 수 있는 라이브러리의 집합&lt;br /&gt;로컬 캐시에 저장됨&lt;br /&gt;기본 디렉터리는 .gradle&lt;/blockquote&gt;
&lt;pre id=&quot;code_1681792654575&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 예시
mavenCentral(), jcenter(), mavenLocal()
maven {
    credentials {
        username 'username'
        password 'password'
    }
    url 'http://xxx.yyy.zzz/abc'
}
flatDir {
    dirs 'lib'
}&lt;/code&gt;&lt;/pre&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;Dependencies&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;사용하는 라이브러리들 명시&lt;br /&gt;라이브러리 그룹, 이름, 버전으로 찾아오며 의존성 설정 가능&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문법&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;전체 문법&lt;br /&gt;&amp;nbsp; [의존성] group: '[그룹]', name: '[이름]', version: '[버전]'&lt;br /&gt;&lt;br /&gt;간이 문법&lt;br /&gt;&amp;nbsp; [의존성] '[그룹]:[이름]:[버전]'&lt;br /&gt;&lt;br /&gt;버전은 1.+ 이런 식으로도 가능 (비추)&lt;br /&gt;&lt;br /&gt;라이브러리의 jar만 참조&lt;br /&gt;&amp;nbsp; 전체 : [의존성] group: '[그룹]', name: '[이름]', version: '[버전]' ext: 'jar'&lt;br /&gt;&amp;nbsp; 간이식 : [의존성] '[그룹]:[이름]:[버전]@jar'&lt;br /&gt;&lt;br /&gt;전이적 의존성 중 특정 라이브러리 제외 (이미 다른 곳에서 참조하여 받거나 특정 경로로 지정되었을 때)&lt;br /&gt;&amp;nbsp; compile xxxx:yyy:4.2 {&amp;nbsp; &amp;nbsp; exclude group: 'aaa.bbb.ccc'&amp;nbsp; &amp;nbsp; &amp;nbsp; }&lt;br /&gt;&lt;br /&gt;compile : 프로젝트 전체에서 사용가능&lt;br /&gt;runtime : 런타임 시에만 사용&lt;br /&gt;testCompile : src/test/java 소스 디렉터리에서만&lt;br /&gt;&lt;br /&gt;컴파일 시에만&lt;br /&gt;&amp;nbsp; compileOnly 'org.projectlombok:lombok'&lt;br /&gt;로컬 파일 시스템 파일 참조&lt;br /&gt;&amp;nbsp; compile files('libs/a.jar, 'libs/b.jar'')&lt;br /&gt;&amp;nbsp; compile fileTree(dir: 'libs', includes: ['*.jar'])&lt;br /&gt;&lt;br /&gt;전의적 의존성&lt;br /&gt;명시적으로 의존성을 가진 라이브러리가 가진 의존성들 dependencies 문법에 transitive: false로 비활성화 가능 (모든 라이브러리를 다 가지고 있어야 한다....)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Gradle Wrapper&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Gradle 실행 시 해당 환경에 Gradle 설치 없이 하기 위함&lt;br /&gt;빌드 설계자의 버전에 맞게 빌드 가능&lt;/blockquote&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;419&quot; data-origin-height=&quot;329&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7K5WV/btsaTf0TZh9/srFPBIMB1cEvVnYtpVYGok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7K5WV/btsaTf0TZh9/srFPBIMB1cEvVnYtpVYGok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7K5WV/btsaTf0TZh9/srFPBIMB1cEvVnYtpVYGok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7K5WV%2FbtsaTf0TZh9%2FsrFPBIMB1cEvVnYtpVYGok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;419&quot; height=&quot;329&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;329&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;gradle-wrapper.jar : 그레이들 래퍼 jar&lt;br /&gt;gradle-wrapper.properties : 그레이들 래퍼 버전 및 실행환경&lt;br /&gt;gradlew : Unix 계열에서 실행가능한 스크립트&lt;br /&gt;gradlew.bat : 윈도우 계열에서 실행가능한 스크립트&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;Gradle-gradle-wrapper.properties&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;gradle-wrapper.properties&lt;/h3&gt;
&lt;pre id=&quot;code_1681793046170&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;distributionUrl : 래퍼의 다운로드 버전이 확인 가능&lt;br /&gt;처음 실행 후 zipStoreBase 아래 zipStorePath 아래 저장되며 이후 캐시로 동작&lt;/blockquote&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;blockquote data-ke-style=&quot;style2&quot;&gt;기본 : 명령창에서 ./gradlew(유닉스) 또는 gradlew.bat 실행(윈도우)&lt;br /&gt;여러 타스크 실행 : ./gradlew clean init&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Task 설명&lt;/h2&gt;
&lt;pre id=&quot;code_1681793128813&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 빌드스크립트(build.gradle)에 외부 라이브러리가 필요할때 쓰는 메서드
// 항상 맨위
// 이 메서드로 추가시 현 스크립트에서 추가된 의존성, 저장소 및 메서드 사용가능
buildscript {
    repositories {
        mavenLocal()
    }
    dependencies {
    }
}
 
....
 
 
// &quot;&quot;은 변수 치환 사용, ''은 사용안함
 
task customTaskA {
    // 다른 task를 참조 (import)
    dependsOn 'customTaskB'
    // 전역변수 export
    ext.customValue = 'CUSTOM VALUE FROM TASK_A'
}
 
task customTaskB {
}
// task에서 실행될 일 추가
customTaskB.doLast {
    println 'add doLast to customTaskB'
}
 
// export된 변수 사용
task customTaskC {
    doLast {
        println customTaskA.customValue
    }
}
 
 
// 기본 타스크 지정
defaultTasks 'clean', 'devBuild'
 
 
// 변수 선언
def buildType = 'normal'
 
task buildProject {
    doLast {
        println '---build project start---'
        println &quot;build type=${buildType}&quot;
        println '---build project end---'
    }
}
 
task devBuild {
    dependsOn 'buildProject'
}
 
task releaseBuild {
    dependsOn 'buildProject'
}
 
// gradle 2단계(설정단계) 진행중 실행되는 메서드
// 등록된 타스크에 따라서 변수에 값 할당
gradle.taskGraph.whenReady {
    taskGrapth -&amp;gt;
        if (taskGrapth.hasTask(&quot;:releaseBuild&quot;)) {
            buildType = 'release'
            println 'release'
        } else if (taskGrapth.hasTask(&quot;:devBuild&quot;)) {
            buildType = 'dev'
            println 'dev'
        } else {
            buildType = 'normal'
            println 'normal'
        }
}&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;h2 data-ke-size=&quot;size26&quot;&gt;동작 원리&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;groovy와 코틀린으로 작성되어 있다.&lt;br /&gt;org.gradle.api의 Project라는 인터페이스를 사용하는 형식&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 id=&quot;Gradle-gradle소스분석(v6.7)&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;gradle 소스 분석 (v6.7)&lt;/h3&gt;
&lt;h4 id=&quot;Gradle-Project(Interface)&quot; style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Project (Interface)&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;main API이며 build file을 분석할 때 사용된다&lt;/li&gt;
&lt;li&gt;해당 인터페이스는 각 프로젝트의 build.gradle와 1:1 관계를 가진다&lt;/li&gt;
&lt;li&gt;Task Object의 집합으로 구성되어 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;life Cycle
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Settings 인스턴스를 생성한다 (Settings 인터페이스는 gradle.setting 파일과 1:1 관계, gradle.setting파일로 생성된다고 봐도 무방하다)&lt;/li&gt;
&lt;li&gt;Settings를 이용해서 프로젝트의 계층구조를 형성한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 계층구조로 인해서 project가 순차적으로 생성된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;build.gradle 파일을 이용해서 Project 인스턴스를 생성&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;style8&quot; /&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Task (Interface)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #091e42; text-align: start;&quot; data-ke-list-type=&quot;disc&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;어떠한 작업들을 가진 인터페이스 (ex: 컴파일 클래스, 유닛테스트 실행, WAR로 export 등등)&lt;/li&gt;
&lt;li&gt;gradle에서 실행가능 작업의 최소단위&lt;/li&gt;
&lt;li&gt;Action들로 이루어져 있다&lt;/li&gt;
&lt;li&gt;기본적으로 TaskContainer에 의해서 생성 관리 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;build.gradle 파일에서의 Task
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Dynamic Properties
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;setProperty(String, Object) 메서드를 이용하여 세팅&lt;/li&gt;
&lt;li&gt;파일 내에서 해당 프로퍼티의 key 또는 property(String)으로 get 해서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Dependency
&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;/ul&gt;
&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;pre id=&quot;code_1681793308007&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 예시 : buildscript 메서드 흐름

buildscript {
    repositories {
        mavenLocal()
    }
    dependencies {
    }
}


/*
Project 인터페이스의 buildscript(Closure configureClosure) 메서드를 실행
Closure 는 groovy의 Closure이다
	property, method등을 세팅 할 수 있으며, 정의된 메서드를 invoke 시킬 수 있는 기능을 가진 추상클래스
Closure가 repositories와 dependencies 메서드를 invoke 시킨다 (ScriptHandler)
ScriptHandler 인터페이스
	repositories(Closure configureClosure)
    	저장소를 설정한다
		사용가능 메서드 : https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/dsl/RepositoryHandler.html
	dependencies(Closure configureClosure)
    	의존성을 설정한다
		dependencies 사용가능 옵션 : https://developer.android.com/studio/build/dependencies?hl=ko
*/&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;h2 data-ke-size=&quot;size26&quot;&gt;REFERENCE&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gradle 레시피 책&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>개발 일지</category>
      <category>gradle</category>
      <category>gradle 동작원리</category>
      <author>북극곰은콜라</author>
      <guid isPermaLink="true">https://p-bear.tistory.com/41</guid>
      <comments>https://p-bear.tistory.com/41#entry41comment</comments>
      <pubDate>Tue, 18 Apr 2023 13:51:51 +0900</pubDate>
    </item>
  </channel>
</rss>