<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>cholog</title>
    <link>https://choicco.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sun, 28 Jun 2026 02:54:11 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>choicco</managingEditor>
    <item>
      <title>Understanding CPU Scheduling in Kubernetes</title>
      <link>https://choicco.tistory.com/8</link>
      <description>&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Managing container CPU limits machanism in Kubernetes&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kubernetes control limits of container CPU by `&lt;span style=&quot;color: #000000;&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;cpu.cfs_period_us&lt;/span&gt;`&lt;/i&gt;&lt;/span&gt; and&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt; `&lt;i&gt;cpu.cfs_quota_us`&lt;/i&gt;&lt;/span&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;`cpu.cfs_period_us`&lt;/i&gt;&lt;/span&gt; represents the time period over which CPU usage is measured and controlled.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;i&gt;`cpu.cfs_quota_us`&lt;/i&gt;&lt;/span&gt; represents the maximum amount of CPU time that processes in the container can use during each period.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;Let's assume a senario to help understanding. In this senario assume that `&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;cpu.cfs_period_us`&lt;/i&gt;&lt;/span&gt; is set to 100ms, `&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;spec.containers[].resources.limits.cpu`&lt;/span&gt; &lt;/i&gt;in the container setting is set to 1000m.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;It means the container can use 1 CPU core during each 100ms period. When we convert this CPU limit to its equvalent quota time(&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;cpu.cfs_quota_us&lt;/i&gt;&lt;/span&gt;), it results in 100ms. When &lt;span style=&quot;background-color: #ffffff; color: #000000; text-align: start;&quot;&gt;`&lt;/span&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;spec.containers[].resources.limits.cpu` &lt;/span&gt;&lt;/i&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;is set to 500m, quota time can be translated to 50ms.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;But, It may not behave as expected.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333;&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;Suppose the next situation.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kubernetes node has 4 CPU core&lt;/li&gt;
&lt;li&gt;Application in container designed multi-threaded model and use 4 CPU core&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In this senario, the application can use CPU for only 25ms and experiences CPU throttling for the remaining 75ms of the 100ms period.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Managing Container CPU requests mechanism in Kubernetes&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In this section, Let&amp;rsquo;s find out how manages Kubernetes container CPU requests.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;When the &lt;span style=&quot;font-family: 'Noto Sans Light';&quot;&gt;&lt;i&gt;spec.containers[].resources.requests.cpu&lt;/i&gt;&lt;/span&gt; property is set, Kubernetes traslates it to a correspending unit called a &quot;CPU share&quot;. This allows Kubernetes to fairly schedule CPU resources, even when containers request more than the actual CPU capacity.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Likewise, consider the following scenario for easier understanding.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kubernetes node has 4 CPU core&lt;/li&gt;
&lt;li&gt;Node has four containers (pods), each requesting 2 CPU cores (2000m), and all of them want to use the full amount they requested&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In this scenario, Kubernetes actually orchestrates scheduling CPU resource fairly across containers.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;As a result, each container can use up to 1 CPU core in period time, based on the CPU share ratio.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Also, if a container that requests a lot of CPU doesn&amp;rsquo;t use much CPU compared to its request, Kubernetes effeciently schedules the unused CPU for other containers.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Conclusion&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;We can know that following knowledge.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kubernetes schedules CPU even when containers request CPU more than the actual available CPU capacity&lt;/li&gt;
&lt;li&gt;Setting CPU limits can cause CPU throttling for your application&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Therefore, if application performance is important, it may be better not to set the CPU limit on the container.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;But if your application prioritizes stabillity, you should set the CPU limit appropriately.&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;Reference&lt;/h4&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://techblog.lycorp.co.jp/ko/efficiently-using-cpu-in-kubernetes&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://techblog.lycorp.co.jp/ko/efficiently-using-cpu-in-kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.outsider.ne.kr/1653&quot;&gt;https://blog.outsider.ne.kr/1653&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>English TW</category>
      <category>k8s</category>
      <author>choicco</author>
      <guid isPermaLink="true">https://choicco.tistory.com/8</guid>
      <comments>https://choicco.tistory.com/8#entry8comment</comments>
      <pubDate>Wed, 23 Apr 2025 14:58:24 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes 핵심 개념 - 1. Controller Pattern과 Pod 생성 흐름</title>
      <link>https://choicco.tistory.com/7</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 기본 개념과 핵심 컴포넌트를 Pod 생성 흐름과 함께 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;Kubernetes Objects&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;Resource와 Object&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 클러스터를 구성하기 위한 정보를 Object로 정의합니다. (&lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/overview/working-with-objects/kubernetes-objects/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object는 YAML 또는 JSON 형태의 menifest 파일을 통해 선언하고, etcd 라는 저장소를 통해 쿠버네티스 시스템에 영구적으로 저장합니다.&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;객체지향언어에 대입해볼 때 Resource는 Object를 생성하기 위해 정해둔 타입(Class)라고 볼 수 있고 Object는 사용자가 manifest 문서를 통해 정의한 설계도이며 쿠버네티스 시스템은 Object 정보를 통해 이를 실체화(인스턴스화) 하는 것이라고 이해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 Object의 종류, 즉 Resource에는 다음과 같은 것들이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 워크로드 리소스: Pod, ReplicaSet, StatefulSet, DaemeonSet, Deployment&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 네트워크 관련 리소스: Service, Ingress, NetworkPolicy&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 저장소 관련 리소스: PersistentVolume,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #222222; text-align: left;&quot;&gt;PersistentVolumeClaim&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 구성 관련 리소스: ConfigMap, Secret&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;spec과 status를 활용한 상태관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object는 사용자의 요구사항(manifest에 정의한 spec)과 쿠버네티스 클러스터 상에서의 실제 상태(status)로 구분됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 시스템은 Object의 spec을 저장해두고 status를 감시하여 spec과의 불일치가 일어나지 않도록 감시하고, 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉬운 예시로 ReplicaSet에서 지정한 container의 replicas 갯수가 3개(spec)인데, 어떤 장애로 인해 container가 중단되어 2개가 되었다면 ReplicaSet Controller 가 이를 감지하고 하나를 더 실행하여 3개가 유지되도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller는 아래의 쿠버네티스 시스템 컴포넌트 설명에서 더 자세히 설명하도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;br /&gt;Kubernetes Components&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;쿠버네티스 시스템을 구축하고 운영하는데 필요한 핵심적인 기능을 수행하는 요소들을 Components라고 부릅니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Components에 대한 개요는 &lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/overview/components/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;에도 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;etcd&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;etcd는 자주 변경되지 않는 분산 시스템의 메타데이터와 같은 값을 저장하기 위해 설계되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;etcd는 분산 환경에서 일관된 데이터를 저장하고 조회할 수 있도록 지원합니다. 또한 복잡한 쿼리보다는 메타데이터 정보에 대한 간단한 정보를 조회하는데 최적이고, key에 대한 변경사항을 Watch라는 기능을 통해 실시간으로 감시할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;etcd는 고가용성을 위해 클러스터로 구성될 수 있으며 리더-팔로워 구조를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAP관점에서 etcd는 CP (강한 일관성과 파티션 내성)을 보장합니다. etcd에 데이터를 쓰면 리더가 새로운 데이터를 기록하고 과반수 이상의 쿼럼에 데이터 저장된 것이 확인했을 때 트랜잭션을 커밋합니다. 리더는 항상 최신의 데이터를 유지하고 팔로워읽기를 수행하기 전에 리더에게 자신의 정보가 최신이 맞는지 확인한 뒤 응답합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;etcd 클러스터가 CP를 보장하는 방법은 &lt;a href=&quot;https://tech.kakao.com/posts/484&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이 글&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;blockquote data-ke-style=&quot;style3&quot;&gt;CAP 이론과 CP, AP 시스템&lt;br /&gt;&lt;br /&gt;CAP에서는 세가지 요소 중 두가지만 동시에 보장할 수 있다고 합니다. 하지만 현대 분산 시스템에서 네트워크 파티션은 필연적으로 일어나기 때문에 P(Partition Torlarance)는 사실상 반드시 보장해야 합니다.&lt;br /&gt;그렇기 때문에 현실적인 시스템은 CP와 AP 중 하나를 선택하는데 CA와 AP의 특징은 다음과 같습니다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;CP&lt;/b&gt;: 금융,결제 등 데이터의 일관성이 엄격하게 보장되어야 하는 환경&lt;br /&gt;&lt;b&gt;AP&lt;/b&gt;: 단순 조회를 위한 읽기 작업 등에서 데이터의 일관성보다는 가용성을 중시하는 환경&amp;nbsp;&lt;br /&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;쿠버네티스는 메타데이터를 저장하고 조회하는 단순한 쿼리를 요구하며, 클러스터의 안정성을 위해 일관적이고 고가용성이 지원되어야합니다. 또한 클러스터 정보와 Objects의 정보를 감시하면서, 변화가 발생했을 때 감지하고 이에 대한 핸들을 해야하기 때문에 etcd가 지향점이 일치한다고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 계층적으로 키 값을 저장하여 B+Tree를 효과적으로 사용하도록 설계하였습니다.&lt;/p&gt;
&lt;pre class=&quot;shell&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;# etctl 로 조회한 key 목록의 일부
/registry/namespaces/harbor
/registry/pods/harbor/harbor-core-6f9fcdb7ff-dgdbx
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;etcd에 저장된 키 목록 조회 결과 &lt;i&gt;&lt;b&gt;/registry/{Resource}/{namespace}/{name}&lt;/b&gt;&lt;/i&gt; 형태로 키 값이 구성된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 형태로 key값을 저장하여 특정 리소스를 조회하거나 특정 네임스페이스의 리소스를 조회하는 등 B+Tree를 효과적으로 활용하여 메타데이터를 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;kube-apiserver&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 클러스터 관리를 위한 모든 API를 Rest API로 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Server는 크게 두가지 역할을 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 오브젝트등의 CRUD를 위해 요청하는 엔드포인트 제공하여 CRUD 기능을 제공하고, 요청 정보를 etcd에 저장&lt;/li&gt;
&lt;li&gt;etcd의 Watch 기능을 통해 변경을 구독하고 변경 사항 발생 시 이를 처리할 수 있는 컴포넌트에 전달하여 작업 위임&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 Control Plane의 API Server는 동일한 etcd 클러스터를 바라보고 있기 때문에 수평 확장에도 유리하게 설계되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;kube-controller-manager&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller Manager는 모든 형태의 컨트롤러를 종합 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 컨트롤러 패턴을 먼저 이해하면 좋습니다. 쿠버네티스는 &lt;a href=&quot;https://kubernetes.io/docs/concepts/architecture/controller/#controller-pattern&quot;&gt;컨트롤러 패턴&lt;/a&gt;을 적용해서 리소스를 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러는 하나 이상의 리소스를 추적하고 리소스의 status가 spec과 일치하는지 지속적으로 확인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 ReplicaSet은 Pod라는 리소스를 관리하는 컨트롤러 리소스 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ReplicaSet은 ReplicaSet Controller라는 Controrller 하위 컴포넌트로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller Manager에는 이런 형태로 다양한 내장 컨트롤러로 구성되어 내부적으로 실행됩니다. 사용자 정의 타입 컨트롤러를 개발해서 추가하는 것 또한 지원합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러는 크게 두 가지 방식으로 제어됩니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;API Server를 통해 제어&lt;/li&gt;
&lt;li&gt;자체적으로 제어&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터 내부의 리소스를 관리는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;API Server를 통해 제어됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이 유형에서는 API 서버를 통해 현재 상태를 조회하고 spec과 status를 비교하여 의도한 상태와 현재 상태가 다르다면 api-server를 통해 조정을 요청합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스 API가 자체적으로 제공하는 Controller Resource는 이 방식으로 제어된다고 이해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Job, CronJob, ReplicaSet, Deployment ... )&lt;/p&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; 제어하는 Controller도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 유형은 외부 서비스와 직접 통신하여 의도한 상태 혹은 기대 상태를 파악하고 상태 변경이 필요하면 변경을 수행한뒤 api-server에게 변경을 보고합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 Cloud에서 제공하는 Node autoscaler같은 경우 Cloud의 API와 직접 상호작용하고 클러스터 부하 증가로 새로운 Node가 필요하다면 생성한 뒤 변경 사항을 api-server에 알리는 형태로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Scheduler&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scheduler는 Pod 생성시 어떤 노드에 배치할 지 계산합니다. 새로 생성되거나 스케쥴링 되지 않은 (어떤 노드에 배치되지 않은) 파드를 실행할 최적의 위치를 찾습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케쥴러는 파드를 배치하기 위해 필터링, 스코어링 단계를 거칩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필터링 단계에서는 CPU, Memory, Affinity, Taints &amp;amp; Tolerations, Priorty 등을 고려하여 파드가 배치될 수 있는 적절한 노드를 필터링합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스코어링 단계에서도 필터링 조건과 비슷한 조건들을 검사하여 노드 간 리소스 사용량 밸런스와 affinity에 설정에 의해 선호 배치 조건등을 점수로 계산하여 가장 적합한 노드에 배치하는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;kubelet&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kublet은 모든 노드 컴포넌트에서 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet의 주요 작업은 노드와 Pod의 상태를 관리하고 주기적으로 API Server에 보고하는 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kubelet이 수행하는 작업은 다음과 같은 것들이 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;API서버로부터 노드에 할당되는 Pod 정보를 감지하고 컨테이너 생성을 요청합니다. kubelet은 CRI (표준화된 Container Runtime Interface)를 통해 컨테이너 런타임과 통신합니다.&lt;/li&gt;
&lt;li&gt;CNI 플러그인을 호출해 Pod 네트워킹을 설정합니다.&lt;/li&gt;
&lt;li&gt;CSI 플러그인을 호출해 Pod의 볼륨을 설정합니다.&lt;/li&gt;
&lt;li&gt;Pod에 정의된 Probe를 실행합니다.&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;h3 data-ke-size=&quot;size23&quot;&gt;컨트롤러 리소스를 통한 Pod 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명한 컴포넌트들이 협력하여 Pod를 생성하는 과정을 정리해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;697&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4jhJk/btsNunfeo8T/pMClxoXzDFSBPGJCkH8cuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4jhJk/btsNunfeo8T/pMClxoXzDFSBPGJCkH8cuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4jhJk/btsNunfeo8T/pMClxoXzDFSBPGJCkH8cuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4jhJk%2FbtsNunfeo8T%2FpMClxoXzDFSBPGJCkH8cuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;653&quot; height=&quot;385&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;697&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 ReplicaSet manifest파일을 정의하고 api-server에 생성을 요청합니다.&lt;/li&gt;
&lt;li&gt;api-server는 Object 정보를 etcd에 저장합니다.&lt;/li&gt;
&lt;li&gt;api-server는 etcd의 데이터 변화를 감지하고 이를 적절한 컴포넌트에게 push 합니다. 위 상황에서는 ReplicaSet Controller에게 전달합니다.&lt;/li&gt;
&lt;li&gt;ReplicaSet Controller는 Controller Loop를 돌며 spec과 status를 비교하고 다르다면 api-server에게 생성을 요청합니다. 이 상황에는 Pod가 생성되지 않았으므로 Pod 생성을 요청합니다.&lt;/li&gt;
&lt;li&gt;api-server는 Pod 생성 정보를 etcd에 저장합니다.&lt;/li&gt;
&lt;li&gt;api-server는 etcd로부터 Pod 생성을 감지하고 scheduler에게 이를 전달합니다.&lt;/li&gt;
&lt;li&gt;scheduler는 필터링, 스코어링 과정을 거쳐 적절한 노드를 선택하고 api-server를 거쳐 etcd에 kublet에게 Pod 생성을 지시합니다.&lt;/li&gt;
&lt;li&gt;Pod를 배치하게되는 Node의 kubelet이 CRI를 통해 컨테이너를 생성하고 CNI, CSI등을 통해 Pod에 네트워크 정책과 볼륨 정책을 연결합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;참고자료&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/ko/docs/home/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://kubernetes.io/ko/docs/home/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://etcd.io/docs/v3.3/learning/data_model/&quot;&gt;https://etcd.io/docs/v3.3/learning/data_model/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://etloveguitar.tistory.com/159&quot;&gt;https://etloveguitar.tistory.com/159&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://gngsn.tistory.com/284&quot;&gt;https://gngsn.tistory.com/284&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.kakao.com/posts/484&quot;&gt;https://tech.kakao.com/posts/484&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>Server</category>
      <category>k8s</category>
      <category>Kubernetes</category>
      <author>choicco</author>
      <guid isPermaLink="true">https://choicco.tistory.com/7</guid>
      <comments>https://choicco.tistory.com/7#entry7comment</comments>
      <pubDate>Mon, 21 Apr 2025 17:26:05 +0900</pubDate>
    </item>
    <item>
      <title>VM과 Container 기술의 배경과 개념 (+ Docker, Kubernetes)</title>
      <link>https://choicco.tistory.com/6</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kubernetes 발전 배경&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;748&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXpB1C/btsNq922HuL/YtM1JUdb3oyKlriqDOOGbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXpB1C/btsNq922HuL/YtM1JUdb3oyKlriqDOOGbK/img.png&quot; data-alt=&quot;물리 서버에 애플리케이션을 직접 배치&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXpB1C/btsNq922HuL/YtM1JUdb3oyKlriqDOOGbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXpB1C%2FbtsNq922HuL%2FYtM1JUdb3oyKlriqDOOGbK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;339&quot; data-origin-width=&quot;820&quot; data-origin-height=&quot;748&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;물리 서버에 애플리케이션을 직접 배치&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거의 애플리케이션은 위와 같이 단순한 물리 서버에 배치하는 것에서 시작했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물리 서버가 준비 있다면 간단한 애플리케이션은 별도의 구성 없이 실행하고 서버를 구축할 수 있기 때문에 크게 문제가 되진 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현대의 애플리케이션이 점점 복잡해지고 많은 요구사항이 생겨나면서 다음과 같은 제약을 겪게 되었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;물리 자원을 효율적으로 사용하지 못함&lt;/li&gt;
&lt;li&gt;애플리케이션이 특정 OS에 의존적이라면 새로운 물리 서버를 하나 더 배치하는 등의 제약&lt;/li&gt;
&lt;li&gt;개발 환경, 배포 환경 등 애플리케이션 실행 환경 차이로 인한 예상치 못한 문제 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가상 머신 (Virtual Machine)&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TI9sf/btsNsisza9x/HcsyTY7QlBudMV2BkSOaJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TI9sf/btsNsisza9x/HcsyTY7QlBudMV2BkSOaJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TI9sf/btsNsisza9x/HcsyTY7QlBudMV2BkSOaJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTI9sf%2FbtsNsisza9x%2FHcsyTY7QlBudMV2BkSOaJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;343&quot; data-origin-width=&quot;1630&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 머신은 가상화 기술을 사용해 물리 서버의 비효율성을 해결하고 효율적으로 애플리케이션 실행 환경을 구성할 수 있도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상화라는 개념은 굉장히 오래 전부터 있었지만 애플리케이션이 복잡해짐에 따라서 가상화 기술이 주목받았고, 2000년 전후로 가상화 관련된 상용화되어 널리 사용되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Type-1 Hypervisor&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 머신을 설치할 때에는 물리 자원에 바로 OS를 설치하는 대신 Hypervisor 계층을 추가하여 물리 자원을 추상화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 머신은 Hypervisor가 제공하는 추상화된 물리 자원에 OS를 설치하고 그 위에 애플리케이션을 설치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 다음과 같은 장점을 얻을 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 물리 자원에 여러개의 OS 설치가 가능&lt;/li&gt;
&lt;li&gt;애플리케이션의 요구사항에 맞는 OS를 설치하고 하나의 물리 자원 위에서 다양한 요구사항을 가지는 애플리케이션 실행&lt;/li&gt;
&lt;li&gt;가상 머신 간의 물리 자원을 완전히 격리하여 효율적인 리소스 사용 (보안 측면에서도 강점)&lt;/li&gt;
&lt;li&gt;애플리케이션 실행을 위한 종속성 설치 및 환경 구성이 완료된 머신을 이미지로 만들어 손쉽게 Scale out, 개발 환경 구성하는 데 활용&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Type2 Hypervisor&lt;br /&gt;&lt;br /&gt;Type1 이후에 Type2 Hypervisor가 등장하여 OS가 이미 설치된 개인 PC에도 가상머신을 실행하고 개발 환경을 구축할 수 있게 되었습니다.&lt;br /&gt;하지만 Type2 Hypervisor는 추상화 계층을 여러번 거치게 되어 Type1 보다 성능이 좋지 않습니다.&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;하지만 가상머신은 Hypervisor 계층의 추가와 각각 별도의 OS를 가지기 때문에 다음에 설명할 컨테이너 기술보다 성능 측면에서는 상대적으로 낮은 퍼포먼스를 가집니다.&lt;/p&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;856&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ejIUu7/btsNsNr4gMj/McRu4NTV1kemBQnZTMaVy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ejIUu7/btsNsNr4gMj/McRu4NTV1kemBQnZTMaVy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ejIUu7/btsNsNr4gMj/McRu4NTV1kemBQnZTMaVy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FejIUu7%2FbtsNsNr4gMj%2FMcRu4NTV1kemBQnZTMaVy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;351&quot; height=&quot;357&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;870&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너 기술 또한 애플리케이션을 실행하기 위한 환경을 효율적으로 구성하고 실행할 수 있도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 컨테이너는 가상머신과 다르게 애플리케이션이 컨테이너라는 단위로 실행되고, 애플리케이션을 실행하기 위한 모든 구성요소를 이미지로 만들어 실행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 가상머신은 하나의 물리자원에 별도의 OS를 설치해 환경을 격리하지만 컨테이너는 호스트 머신의 OS를 공유하며 컨테이너 엔진을 통해 보다 가볍게 여러개의 애플리케이션을 실행할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 위에 언급한 가상머신의 장점은 컨테이너도 가지며 애플리케이션이 가상화 계층 없이 호스트 OS를 사용할 수 있도록 하기 때문에 성능도 상대적으로 더 뛰어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 단점으로 호스트 머신의 OS 커널에 의존성이 있어 일부 제약이 있을 수 있고, OS 수준에서 완전히 분리된 가상 머신과 다르게 하나의 커널을 공유하기 때문에 보안에 상대적으로 취약할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 컨테이너 기술로 Docker가 있습니다. Docker는 Linux 커널의 cgroup, chroot, namespace등을 통해 위와 같은 컨테이너간 격리 기술을 구현하였고, Linux 서버에서 호환성이 좋습니다.&lt;/p&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;h2 data-ke-size=&quot;size26&quot;&gt;Container Orchestration&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 컨테이너를 사용하면 높은 성능, 이식성 및 빠른 실행 가능성 등의 장점을 활용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 기본적으로 컨테이너는 하나의 머신에서의 동작을 지원하기 때문에, 자원을 나눠서 사용한다고 하더라도 물리적으로 한정된 자원에서 실행되기 때문에 애플리케이션 자체가 리소스를 많이 사용한다면 성능에 제약이 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해서는 Scale up 혹은 Scale out을 해주어야 합니다. Scalue up에는 일정 수준이상에서 한계가 있기 때문에 적당한 물리 자원 수준을 넘어가면 일반적으로 Scale out을 선택합니다. 하지만 Scale Out을 위해서 컨테이너가 배치된 서버의 상태를 관리해야 하고, 컨테이너간 통신을 위한 네트워크 구성과 다른 서버에 배치된 컨테이너의 상태를 관리해주는 등 복잡도가 늘어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 컨테이너 실행 서버간의 클러스터를 맺고 관리할 수 있도록 도와주는 것이 Container Orchestration 도구 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 많이 사용되는 도구로는 Kubernetes, Docker Swarm 등이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Docker Swarm vs Kubernetes&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Swarm과 Kubernetes는 모두 위에 언급한 것 처럼 다중 서버 환경에서의 컨테이너 실행, 관리, 통신 등을 쉽게 수행할 수 있도록 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Swarm은 Docker 생태계와 완전히 통합되어 제공되며, 비교적 단순하고 쉽게 클러스터를 구성할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kuberenetes는 보다 세밀하게 클러스터와 컨테이너를 관리할 수 있으며 시스템을 구성하거나 관리하기 위해 필요한 기술을 추상화된 형태로 제공합니다. 따라서 Docker Swarm보다 더욱 많은 기능을 제공하고 확장성이 높으며 그만큼 구조가 복잡하고 학습 곡선이 높습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 두 기술 모두 Container Ochestration 분야에서 높은 점유율을 가지고 있지만 Kubernetes가 압도적으로 높은 점유율을 가지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유로는 위에도 설명했듯 Docker Swarm은 Swarm 자체적으로 제공하는 기능으로 구성이 편한대신 확장성이 낮아 서로 다른 인프라 환경에서 구축하기에 제약이 있을 것이라고 생각됩니다. 반면에 Kubernetes는 대부분의 기술이 추상되어 제공되고 특히 Cloud 환경과의 통합이 굉장히 잘 되어있기 때문에 클라우드 환경에서도 이에 대한 관리형 서비스(EKS, NKS 등)를 제공하고 클라우드 기술의 발전과 함께 시너지를 내며 성장할 수 있었을 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 글에서는 Kubernetes의 핵심 개념과 동작 원리에 대해서 알아보도록 하겠습니다.&lt;/p&gt;</description>
      <category>Server</category>
      <category>CLOUD</category>
      <category>container</category>
      <category>Kubernetes</category>
      <category>vm</category>
      <author>choicco</author>
      <guid isPermaLink="true">https://choicco.tistory.com/6</guid>
      <comments>https://choicco.tistory.com/6#entry6comment</comments>
      <pubDate>Sun, 20 Apr 2025 22:23:02 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux 비동기 처리 흐름의 이해 - 2. Reactor 편</title>
      <link>https://choicco.tistory.com/5</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://choicco.tistory.com/3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;1. Spring Webflux 비동기 처리 흐름의 이해 - Netty 편&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://choicco.tistory.com/5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2. Spring Webflux 비동기 처리 흐름의 이해 - Reactor 편&lt;/a&gt;&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;이번 편에서는 &lt;a href=&quot;https://choicco.tistory.com/3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;1편&lt;/a&gt;에 이어서 Reactor의 동작을 알아보고, Netty와 Reactor가 Webflux에서 어떻게 연결되어 처리되는지 까지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;br /&gt;&lt;b&gt;Reactor의 동작&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netty는 저수준 네트워크 I/O를 다루는 반면, Reactor는 개발자에게 편리한 비동기 프로그래밍을 위해 비교적 고수준의 API를 제공하기 때문에 보다 이해가 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 Reactor의 Reactive Streams 표준, Publisher-Subcriber 구조 등 의 구체적인 이해보다는 예시 코드를 이용해 동작의 흐름을 이해하는데 집중합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Mono, Flux를 비유하여 이해하기&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor 에서 핵심이되는 클래스는 Mono와 Flux입니다. 이들은 데이터를 제공하는 객체로 Publisher라고 불립니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리에게 친숙한 Stream과 비교하며 공통점과 차이점을 알아보도록 하겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743487979689&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
public void example() {
    // Stream
    List&amp;lt;Integer&amp;gt; squaredData = Stream.of(1, 2, 3)
            .map(this::square)
            .collect(Collectors.toList());

    // Reactor
    List&amp;lt;Integer&amp;gt; numberData = List.of(1, 2, 3);

    Disposable disposable = Flux.fromIterable(numberData)
                .map(this::square)
                .subscribe(System.out::println);
}

private int square(int i) {
    int result = i * i;
    System.out.println(result);
    return i;
}&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;위 코드는 Stream과 Reactor Publisher를 활용하여 데이터를 제곱 처리하는 간단한 로직입니다. 위 코드를 통해 Stream과 Publisher API 의 공통점, 차이점을 분석해보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;공통점&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터를 선언하고, 중간 연산을 통해 처리하고, 최종 연산을 호출하는 형태&lt;/li&gt;
&lt;li&gt;최종 연산을 호출할 때 까지 데이터 연산이 실제로 수행되지 않음 (Lazy Evaluation)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;차이점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Stream과 Publisher의 최종연산의 성격에서 차이를 보입니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stream: 일반적으로 데이터 처리 결과를 특정 자료구조로 반환&lt;/li&gt;
&lt;li&gt;Publisher: 최종 연산으로 구독을 수행하고 데이터 처리 결과를 반환받는 대신 구독자가 데이터 스트림에 대한 구독을 취소하는 등의 제어를 위한 Disposable 객체를 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 차이점의 원인은 Reactor 비동기 프로그래밍 특성에 있습니다. 비동기 프로그래밍에서는 데이터 처리가 어떤 시점에 끝날지 모르기 때문입니다. 만약 연산 결과를 특정 타입으로 반환받게 된다면 이를 위해 Thread가 Blocking 되게 될 것입니다. 따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Publisher는 데이터 처리 결과를 이용한 로직을 콜백으로 등록하여 수행&lt;/b&gt;하도록 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 예시 코드에서도 데이터 처리가 완료 된 뒤 subscribe() 메소드에 System.out.println(data) 를 콜백으로 등록한 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Publisher - Subscriber 모델&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 언급된 Publisher, Subscriber (발행-구독) 모델에 대한 개념을 조금 더 살펴보겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Reactor는 비동기 데이터 흐름 처리를 Publisher-Subscriber 모델로 구현했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Publisher-Subscriber 이라는 용어때문에 Publish 하는 주체와 Subscribe 하는 주체가 시스템 내외부에 독립적으로 존재하는 듯한 착각을 부를 수 있지만, 실제로는 위에 첨부한 예시 코드 처럼 발행과 구독 모두 개발자가 프로그래밍하는 코드 안에서 일어나는 동작입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또한 위 설명에서 알 수 있듯이 구독이 일어나기 전까지는 아무런 작업이 일어나지 않습니다. Publisher는 데이터를 생성하고 연산을 수행하는 일련의 작업을 정의해 둘 뿐입니다. 이 작업은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;subscribe()&lt;/span&gt;&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 호출해서 구독자가 생성될 때 트리거되어 데이터 생성 및 연산을 처리하고 처리 결과를 구독자에게 전달합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;데이터 생성과 소비&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Publisher를 이용해 데이터를 생성하는 방법은 크게 즉시 평가와 지연 평가로 나눌 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Sink를 사용해서 프로그래밍 방식으로 데이터 생성 흐름을 제어하거나 실시간 데이터 스트리밍하는 방식도 있으나 이 주제는 따로 다뤄보도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉시 평가 (Eager Evaluation)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉시 평가는 일반적인 프로그래밍에서 사용하는 방식과 동일하게, 데이터를 즉시 계산하고 메모리에 할당하는 것 입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1743487979692&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 연산이 복잡하지 않으며 I/O 바운드 작업이 일어나지 않는 작업
List&amp;lt;Integer&amp;gt; numberData = List.of(1, 2, 3);

// 메모리에 있는 값을 즉시 사용
Flux.fromIterable(numberData)
    .subscribe();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉시 평가는 위 코드 예시 처럼 이미 준비된 데이터 소스를 사용하거나, 가벼운 데이터 소스를 사용할 때 활용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;또는, 구독을 여러번 해도 일관된 데이터를 제공해야 할 때도 활용할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, CPU 작업이 크지 않고 파일 읽기/쓰기, 네트워크 통신 등의 Blocking 작업이 없는 경우에 적합합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;사용할 수 있는 API는&amp;nbsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;&lt;i&gt;&lt;b&gt;Mono.just(),&lt;span&gt;&amp;nbsp;&lt;/span&gt;Flux.just(),&lt;span&gt;&amp;nbsp;&lt;/span&gt;Flux.fromIterable()&lt;/b&gt;&lt;/i&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;지연 평가 (Lazy Evaluation)&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지연 평가는 구독이 일어나는 순간에 데이터를 준비합니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743487979693&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
public void example2() {
    Mono.fromCallable(() -&amp;gt; readFile(&quot;example.txt&quot;))
            .subscribeOn(Schedulers.boundedElastic())
            .subscribe();
}

public List&amp;lt;String&amp;gt; readFile(String filePath) throws IOException {
	List&amp;lt;String&amp;gt; fileContents = new ArrayList&amp;lt;&amp;gt;();
    
    
    FileReader fileReader = new FileReader(filePath);
    StringBuilder content = new StringBuilder();

    //read file ...

    return content.toString();
}&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;지연 평가는 즉시 평가와 반대로 I/O 작업이 있거나, CPU 부하 작업이 있을 때 적합합니다. 또는 여러번의 구독이 일어날 때, 실시간 데이터를 제공해야 한다면 지연 평가를 사용해야 합니다. 위 코드 예시에서는 Disk I/O가 발생하는 readFile() 메소드를 지연평가 기능으로 수행하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, readFile() 메소드를 직접 호출한다면 파일을 읽는 동안 Thread가 Blocking 될 것입니다. 그리고 Webflux 환경에서 Blocking 된 Thread가 EventLoop Thread 였다면 애플리케이션 성능에도 큰 악영향을 줄 것입니다. 그렇기 때문에 구독할 때 파일 읽기를 수행할 수 있도록 callback 방식으로 데이터를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor는 주요 작업을 처리하는 Thread를 차단하지 않고 효율적인 비동기 작업을 수행할 수 있도록 합니다. 이를 돕는 Scheduler라는 객체가 있으며, 위 코드에서도 찾아볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scheduler는 작업 유형별로 알맞은 실행 정책 혹은 적절한 Thread를 선택할 수 있도록 돕는 역할을 하며, 다음과 같은 실행 컨텍스트를 선택할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;Schedulers.immediate()&lt;/b&gt;&lt;/i&gt;: 현재 스레드에서 작업 실행 (명시하지 않으면 기본적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;immediate()&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;와 같이 동작합니다.)&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;Schedulers.boundedElastic()&lt;/b&gt;&lt;/i&gt;: I/O 작업에 적합한 스레드 풀&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;Schedulers.single()&lt;/b&gt;&lt;/i&gt;: 단일 재사용 가능한 스레드에서 작업 실행&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;Schedulers.parallel()&lt;/b&gt;&lt;/i&gt;: 고정된 크기의 워커 스레드 풀에서 작업 실행 (CPU 코어 수에 맞게 최적화 되어 있음)&lt;/li&gt;
&lt;li&gt;&lt;i&gt;&lt;b&gt;Schedulers.fromExecutorService()&lt;/b&gt;&lt;/i&gt;: 커스텀 ExecutorService를 기반으로 스케줄러 생성&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;Webflux에서는 높은 CPU 부하 작업을 하는 것을 지양해야 하고, 주로 서버 간 통신 (Network I/O) 가 많이 발생하기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;i&gt;&lt;b&gt;boundedElastic()&lt;/b&gt;&lt;/i&gt;의 활용도가 높습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Schedulers가 제공하는 컨텍스트는 subscribesOn(), publishOn() 메소드로 설정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;비동기 작업의 작업 연결&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Publisher는 생성한 데이터 소스로부터 추가적인 작업을 하기 위해 작업 연결을 위한 API를 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webflux로 웹 서버 개발을 하게 되면 주로 외부 웹 서버, Database Server, Redis Server 와 통신합니다. 이러한 통신을 위해&amp;nbsp; WebClient, R2dbc, ReactiveRedis 등의 Reactive 라이브러리를 사용하고 이들은 비동기 통신을 이미 구현하여 제공합니다. 따라서 고수준에서 개발하는 입장에서는 비동기 통신을 구현을 통해 데이터 소스를 생성하는 작업보다는 제공되는 데이터 소스들을 잘 연결하고 활용하는 작업을 주로 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 다양한 연산자를 다루기 보다는 비동기 작업 연결에 대한 간단한 예시 코드를 통한 설명을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;사전 환경&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 코드를 제공하기 위해 RDBMS에서 일대다 매핑 관계를 가지는 Post, Comment 스키마를 가정합니다. 또한 R2DBC 를 사용해서 데이터베이스와 통신하는 상황을 가정합니다. R2DBC에 대한 배경지식이 없더라도 Spring data JPA를 사용해보셨다면 무리 없이 이해할 수 있을 것 입니다.&lt;/p&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;161&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bckVSS/btsM5l2ZZgq/KzwrKb0Cs7gFsCASkL82hk/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bckVSS/btsM5l2ZZgq/KzwrKb0Cs7gFsCASkL82hk/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bckVSS/btsM5l2ZZgq/KzwrKb0Cs7gFsCASkL82hk/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbckVSS%2FbtsM5l2ZZgq%2FKzwrKb0Cs7gFsCASkL82hk%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/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;161&quot; data-origin-width=&quot;507&quot; data-origin-height=&quot;161&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1743487979695&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Table(&quot;post&quot;)
public class Post {
    @Id
    @GeneratedValue
    private Long id;
    private String content;
}

@Table(&quot;comment&quot;)
public class Comment {
    @Id
    @GeneratedValue
    private Long id;
    private Long postId;
    private String content;
}&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;이에 대한 Mapping Class로 위와 같은 객체를 정의합니다. JPA의 Entity와 같이 데이터베이스와 매핑되는 객체라고 이해해주시면 됩니다. 대신 R2dbc는 ORM이 아니기 때문에 Order와 OrderItem간에 객체로 의존하지 않고, RDBMS 테이블과 같이 PK - FK 관계를 가집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;A 비동기 작업의 결과를 이용해서 B 비동기 작업을 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 비동기 작업 예시입니다. 여기서는 게시글 목록을 검색하고, 각 게시물에 대한 Id로 각각의 댓글 갯수를 검색하는 시나리오를 가정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743487979697&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;interface PostRepository extends R2dbcRepository&amp;lt;Post, Long&amp;gt; {
    // 검색 조건을 이용해 조건에 부합하는 게시글 목록을 조회합니다.
    Flux&amp;lt;Post&amp;gt; findAllBySomethingConditions(Conditions conditions);
}

interface CommentRepository extends R2dbcRepository&amp;lt;Comment, Long&amp;gt; {
    // 게시글의 아이디로 댓글 갯수를 조회합니다.
    Mono&amp;lt;Long&amp;gt; countByPostId(Long postId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 위와 같은 데이터 인터페이스를 정의했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1743487979698&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Flux&amp;lt;PostDto&amp;gt; get(Conditions conditions) {
    return postRepository.findAllByConditions(conditions)
             .flatMap(post -&amp;gt; commentRepository.countByPostId(post.getId())
             		.map(commentCount -&amp;gt; PostDto.from(post, commentCount)));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(위 코드는 예시일 뿐이므로 N+1과 같은 문제는 고려하지 않습니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 작업의 결과를 사용해서 다음 비동기작업을 수행하기 위해서는 flatMap() API를 사용할 수 있습니다. 위 코드에서는 게시글 목록을 조회하고, 각각의 게시글 Id로 게시글 댓글 갯수를 조회하여 게시글과 댓글 갯수 정보를 합쳐서 PostDto를 생성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀 더 자세한 흐름을 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;R2DBC는 findAllByConditions() 를 통한 쿼리 작업을 비동기로 처리할 수 있도록 하는 Publisher A를 반환합니다. R2DBC는 비동기 작업을 위해 boundedElastic() 스케쥴러(비동기 작업 스레드 풀)를 사용합니다.&lt;/li&gt;
&lt;li&gt;countByPostId() 작업 또한 비동기로 처리할 수 있는 Publisher B를 반환합니다. (여기서 Publisher B는 여러개의 인스턴스가 될 수 있습니다.)&lt;/li&gt;
&lt;li&gt;flatMap()에는 게시물 검색 결과로부터&amp;nbsp; 생성된 데이터 소스를 통해 처리할 새로운 비동기 작업을 등록합니다. flatMap() 내부적으로는 다음 비동기 작업으로 반환되는 Mono&amp;lt;PostDto&amp;gt; 를 구독합니다.&lt;/li&gt;
&lt;li&gt;모든 데이터 소스에 대해 작업이 완료되면 flatMap()이 이들을 다시 하나의 Flux&amp;lt;PostDto&amp;gt;로 합칩니다 (평면화 라는 용어가 주로 사용됨).&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1697&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGWzaO/btsM5suhpGY/mlsKNoyWEjciB4AVbNMkx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGWzaO/btsM5suhpGY/mlsKNoyWEjciB4AVbNMkx0/img.png&quot; data-alt=&quot;flatMap() 타임라인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGWzaO/btsM5suhpGY/mlsKNoyWEjciB4AVbNMkx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGWzaO%2FbtsM5suhpGY%2FmlsKNoyWEjciB4AVbNMkx0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;767&quot; height=&quot;231&quot; data-origin-width=&quot;1697&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;flatMap() 타임라인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 예시 작업이 구독된다면 위 그림과 같이 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 부분은 게시글 검색 결과에 대한 데이터는 순차적으로 전달되지만, flatMap()에서 처리하는 댓글 갯수 검색 작업은 비동기로 병렬적으로 처리되기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;순서가 보장되지 않는다&lt;/b&gt;는 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;만약 게시물 검색 기능에 정렬 쿼리가 포함되어 있었다면 원치 않는 결과가 나올수 있을 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;flatMap(), flatMapSequential(), concatMap()&amp;nbsp;&lt;br /&gt;&lt;br /&gt;Reactor는 비동기 작업에 대한 순서를 보장하기 위해 flatMapSequential(), concatMap() 등의 api 도 제공합니다.&lt;br /&gt;&lt;br /&gt;flatMapSequential()은 flatMap() 그림과 같이 비동기 작업(댓글 조회)을 병렬적으로 처리하지만, 이를 다시 병합할때 원본 데이터의 순서대로 병합해줍니다. 즉, 빠르게 수행하면서 순서를 보장할 수 있습니다.&lt;br /&gt;&lt;br /&gt;concatMap()은 원본 데이터의 순서대로 비동기 작업을 처리(게시물1번에 대한 댓글 갯수 조회 -&amp;gt; 게시물2에 대한 댓글 갯수 조회 -&amp;gt; .. ) 합니다. 이는 작업 결과 뿐만 아니라 작업 순서까지 엄격하게 지켜져야하는 트랜잭션 처리 등에 사용할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;flatMap() vs map()&lt;br /&gt;flatMap()은 Function&amp;lt;T, Mono&amp;lt;R&amp;gt;&amp;gt; 타입을 매개변수로 받습니다. 즉, 특정 타입을 전달받아 새로운 Publisher를 반환하는 콜백을 등록해야합니다. 위 예시에서 사용했듯이, 이전 작업의 결과로 새로운 비동기 작업(네트워크, 디스크 I/O등) 을 해야할 때 주로 사용합니다.&lt;br /&gt;&lt;br /&gt;map()은 Function&amp;lt;T, R&amp;gt; 타입을 매개변수로 받습니다. 이는 Stream의 map과 비슷한 동작을하며, 비동기 작업 호출이 아닌 단순 변환작업에 주로 사용합니다.&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;이외에도 정말 많은 종류의 연산자가 있지만, flatMap() 과 같은 기본적인 비동기 작업 흐름을 이해한다면 다른 연산자도 쉽게 이해하고 사용할 수 있을 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;Webflux에서 Netty + Reactor 전체 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 이전 편에서 다룬 Netty와 Reactor의 작업 흐름을 전체적으로 그려보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1843&quot; data-origin-height=&quot;671&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B1ZGm/btsM3m28r0M/O2Sp3MXGBYK5VfehKAJ3wK/tfile.dat&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B1ZGm/btsM3m28r0M/O2Sp3MXGBYK5VfehKAJ3wK/tfile.dat&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B1ZGm/btsM3m28r0M/O2Sp3MXGBYK5VfehKAJ3wK/tfile.dat&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB1ZGm%2FbtsM3m28r0M%2FO2Sp3MXGBYK5VfehKAJ3wK%2Ftfile.dat&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;764&quot; height=&quot;278&quot; data-origin-width=&quot;1843&quot; data-origin-height=&quot;671&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 다음 상황을 나타냅니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;X축은 타임라인, Y축은 서로 다른 스레드를 의미합니다.&lt;/li&gt;
&lt;li&gt;하나의 Boss Thread와 하나의 Worker Thread만 있는 환경을 가정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다음과 같이 작업을 처리합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 클라이언트가 게시글 목록 조회 API를 호출합니다.&lt;/li&gt;
&lt;li&gt;Boss Thread는 요청 수락만 처리하고 Worker Thread가 요청이 준비되면 요청을 컨트롤러로 전달합니다.&lt;/li&gt;
&lt;li&gt;Worker Thread에서 단순 작업을 처리하다가, I/O 작업이 발생하면 예약해둔 스케쥴러로 전환되어 (그림에서는 Bounded Elastic) 요청을 처리합니다.&lt;/li&gt;
&lt;li&gt;Boss Thread와 Worker Thread는 각각 작업을 다른 Thread에게 위임했으므로 다음 요청을 처리할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 구조로, Webflux는 다수의 요청을 적은 쓰레드로 처리할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;Webflux에서는 Controller가 반환한 Publisher (Mono, Flux)를 구독하고 클라이언트에게 결과를 응답합니다.&lt;br /&gt;&lt;br /&gt;따라서 비동기 작업이 제대로 수행되기 위해서는 모든 비동기 작업이 체인되어 결과적으로 하나의 Publisher를 반환해야 합니다. 내부적으로 체인되지 않은 Publisher가 있다면 메소드를 호출 했더라도(ex-데이터 저장) 실제로는 구독되지 않아 작업이 수행되지 않습니다.&lt;br /&gt;&lt;br /&gt;또한, Webflux에서 결과를 구독해서 클라이언트에게 응답하기 때문에 일반적으로는 직접 subscribe() 를 호출하지 않아도 됩니다.&lt;br /&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;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;참고자료&lt;/b&gt;&lt;/h3&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://d2.naver.com/helloworld/2771091&quot;&gt;https://d2.naver.com/helloworld/2771091&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mark-kim.blog/reactor_sinks/&quot;&gt;https://mark-kim.blog/reactor_sinks/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>Reactor</category>
      <category>Spring</category>
      <category>Webflux</category>
      <author>choicco</author>
      <guid isPermaLink="true">https://choicco.tistory.com/5</guid>
      <comments>https://choicco.tistory.com/5#entry5comment</comments>
      <pubDate>Tue, 1 Apr 2025 15:19:26 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux 비동기 처리 흐름의 이해 - 1. Netty 편</title>
      <link>https://choicco.tistory.com/3</link>
      <description>&lt;blockquote data-end=&quot;46&quot; data-start=&quot;0&quot; data-ke-style=&quot;style3&quot;&gt;&lt;a href=&quot;https://choicco.tistory.com/3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;1. Spring Webflux 비동기 처리 흐름의 이해 - Netty 편&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://choicco.tistory.com/5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2. Spring Webflux 비동기 처리 흐름의 이해 - Reactor 편&lt;/a&gt;&lt;/blockquote&gt;
&lt;p data-end=&quot;46&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;46&quot; data-start=&quot;0&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 WebFlux 기반 웹 서버의 비동기 동작 흐름을 중심으로 설명합니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;105&quot; data-start=&quot;48&quot; data-ke-size=&quot;size16&quot;&gt;요청 처리의 Thread 흐름을 중심으로 작성했으며, 해당 관점에서 벗어난 개념은 자세히 설명하지 않습니다.&lt;/p&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;105&quot; data-start=&quot;48&quot; data-ke-size=&quot;size16&quot;&gt;만약 자바 비동기 프로그래밍에 대한 사전 지식이 없다면 &lt;a href=&quot;https://mangkyu.tistory.com/258&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;해당 시리즈&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;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;1. Webflux 구성 (Netty, Reactor)&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webflux는 Reactor Netty를 기반으로 동작하는 비동기 웹 프레임워크 입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor Netty는 크게 Netty와 Reactor로 구성되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;Netty&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netty는 비동기 이벤트 루프 기반 네트워크 프레임워크 입니다. 비동기 I/O 작업을 처리할 수 있도록 설계되었으며 주로 HTTP 요청을 비동기적으로 읽고 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;Reactor&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reactor는 비동기 애플리케이션 프로그래밍을 고수준 API로 할 수 있도록 도와주는 라이브러리 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;Reactor Netty와 Spring Webflux&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netty와 Reactor는 서로 다른 모듈이며, Reactor Netty가 이 둘을 연결해주는 어댑터 혹은 브릿지와 같은 역할을 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Spring Webflux는 Reactor Netty를 기반으로 동작하는 웹 프레임워크 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개념인 내용은 위와 같지만 위 내용으로는 이들이 어떻게 동작하는건지 이해하기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 Netty의 동작과 Reactor의 개념, 두 기술이 어떻게 연결되어 Webflux가 동작하는지 알아보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #f89009;&quot;&gt;&lt;b&gt;2. Netty의 동작&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Netty가 HTTP 요청을 비동기로 처리할 수 있도록 하는 이벤트 루프 모델을 기반으로 합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #409d00;&quot;&gt;&lt;b&gt;Event Loop&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전통적인 Thread Per Request 모델&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전통적인 Thread Per Request 방식과 비교하면 클라이언트 요청 하나당 Thread 하나에서 처리하며, I/O 가 발생하면 해당 Thread가 Blocking되고 동기적으로 응답을 기다립니다. 따라서 CPU 관점에서는&amp;nbsp; Blocking된 Thread 대신 작업을 수행해야하는 Thread를 호출하며 Context Switching을 하면서 오버헤드가 생기게 됩니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Event Loop 모델&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Event Loop는 비동기 논블로킹 프로그램 모델을 구현하는 패턴입니다. Event Loop 모델은 네트워크 I/O를 이벤트로 감지하여 이벤트가 발생할 때만 작업을 수행합니다. 따라서 클라이언트와의 통신 간의 발생하는 네트워크 I/O가 발생하면 이를 Thread는 이를 대기하지 않고 다른 작업을 먼저 처리합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Netty에서는 멀티쓰레드 기반의 이벤트 루프 모델로 구현되었으며 기존 방식보다 훨씬 적은 쓰레드로 더 많은 요청을 수행할 수 있게 설계되었습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Netty는 다음과 같은 쓰레드를 이용해 요청 수신과 처리를 효율적으로 수행합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Boss Group&lt;/li&gt;
&lt;li&gt;Worker Group&lt;/li&gt;
&lt;/ul&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: #006dd7;&quot;&gt;&lt;b&gt; Boss Group&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;613&quot; data-start=&quot;530&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Boss Group&lt;/b&gt;은 클라이언트의 TCP 연결 요청을 수락하는 역할을 담당하며, 일반적으로 하나의 스레드가 이를 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Worker Group&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker Group에서는 Boss Group에서 수락한 요청의 내용을 읽고 처리하여 응답하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Webflux에서&lt;b&gt; 기본적으로 (core 개수 * 2) 개의 Thread를 할당&lt;/b&gt;합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Netty의 이벤트 루프는 Java nio 패키지의 Channel, Selector라는 주요 구성요소를 통해 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Channel&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-end=&quot;1531&quot; data-start=&quot;1450&quot; data-ke-size=&quot;size16&quot;&gt;Channel은 OS 커널의 I/O 서비스와 상호작용하며, 논블로킹 방식으로 동작하기 때문에 비동기적인 네트워크 읽기/쓰기 작업이 가능합니다.&lt;/p&gt;
&lt;p data-end=&quot;1692&quot; data-start=&quot;1538&quot; data-ke-size=&quot;size16&quot;&gt;Boss Group Thread는 하나의 &lt;b&gt;ServerSocketChannel&lt;/b&gt;을 관리하며, 클라이언트의 연결 요청을 감지하고 수락하는 역할을 합니다. 연결이 수락되면, 클라이언트와 연결된 SocketChannel을 생성하여 적절한 Event Loop에 전달합니다.&lt;/p&gt;
&lt;p data-end=&quot;1825&quot; data-start=&quot;1699&quot; data-ke-size=&quot;size16&quot;&gt;또한, Boss Group Thread는 각 Event Loop에 등록된 Channel의 개수를 확인하면서 로드 밸런싱을 수행하여 작업을 분산합니다. 즉, 하나의 Event Loop에는 여러 개의 Channel이 등록될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Selector&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Selector는 네트워크 이벤트가 발생했을 때 감지하고 작업 큐에 이벤트를 등록하는 역할을 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java NIO의 클래스로, OS 커널의 이벤트 통지 메커니즘(Linux의 epoll 등)을 활용합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Event Loop Group마다 하나의 Selector를 가지고 있으며 이를 통해 이벤트를 감지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트는 네가지 종류가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ACCEPT (OP_ACCEPT)&lt;/li&gt;
&lt;li&gt;CONNET (OP_CONNET)&lt;/li&gt;
&lt;li&gt;READ (OP_READ)&lt;/li&gt;
&lt;li&gt;WRITE (OP_WRITE)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ACCEPT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 서버로 요청을 할 때 발생하며 &lt;b&gt;Boss Thread가 처리&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACCEPT 이벤트가 발생하면 Boss Thread의 Selector가 이를 감지합니다. Boss Thread에서는 요청에 대한 Socket Channel을 생성하고 Woreker Thread에 전달하고 Worker Thread는 해당 채널을 Selector에 등록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CONNECT&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와의 연결이 성공적으로 수립됐을 때 발생하는 이벤트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CONNECT 이벤트가 발생하면 Channel 이 클라이언트와 통신할 수 있도록 초기화 작업을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;READ&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청을 읽을 준비가 됐을 때 발생하는 이벤트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;READ 이벤트가 발생하면 Http 요청을 해석해서 적절한 컨트롤러에 전달하고 요청을 수행하며 응답을 Channel에 Write 합니다. 이 작업은 Worker Thread에서 수행되기도 하지만, 애플리케이션 로직에 비동기 작업이 있었다면 별도의 Thread에서 수행될 수도 있습니다. 이 내용은 Reactor 목차 및 전체 흐름 예시에서 다시 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 소켓 버퍼가 가득 찬 경우에는 응답을 보류하고 Selector의 WRITE 이벤트가 발생했을 때 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WRITE&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WRITE 이벤트는 다음과 같은 조건을 만족할 때 발생합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;READ 단계에서 애플리케이션 로직을 모두 수행했으나 소켓 버퍼가 가득 차서 보류된 응답이 있음&lt;/li&gt;
&lt;li&gt;소켓 버퍼가 가득 찼다가 다시 공간이 생김&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우, 보류된 응답에 대해 애플리케이션 로직과 관계없이 Worker Thread가 소켓 버퍼에 응답 쓰기 작업을 진행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;b&gt;참고&lt;/b&gt;&lt;br /&gt;Boss Group Thread는 요청 수락 외에 다른 작업을 하지 않기 때문에 Selector에서 이벤트가 감지되지 않으면 Blocking 상태로 대기합니다.&lt;br /&gt;Worker Group Thread는 여러 채널의 이벤트를 감지해야 하고, 작업 큐 및 다른 스케쥴에 등록된 작업들도 처리해야 하기 때문에 Selector로부터 일정시간 이벤트가 감지되지 않으면 Time out 되고 다른 작업이 있는지 확인하고 수행합니다.&lt;br /&gt;&lt;br /&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;2030&quot; data-origin-height=&quot;954&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5kuc9/btsMNRIoxG6/UTbIUQIBCEDwBkxTqegjp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5kuc9/btsMNRIoxG6/UTbIUQIBCEDwBkxTqegjp1/img.png&quot; data-alt=&quot;Netty Diagram&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5kuc9/btsMNRIoxG6/UTbIUQIBCEDwBkxTqegjp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5kuc9%2FbtsMNRIoxG6%2FUTbIUQIBCEDwBkxTqegjp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;707&quot; height=&quot;332&quot; data-origin-width=&quot;2030&quot; data-origin-height=&quot;954&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Netty Diagram&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 그림은 위의 설명을 표현합니다. 그림과 함께 개념을 요약하면 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Boss Thread는 Selector를 통해&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;ServerSocketChannel에 요청이 있는지 감지합니다.&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;클라이언트 요청을 감지하면 이를 수락하고 SocketChannel을 생성합니다.&lt;/li&gt;
&lt;li&gt;생성한 SocketChannel을 균등하게 Worker Thread에게 전달합니다.&lt;/li&gt;
&lt;li&gt;Worker Thread는 전달받은 Channel을 Selector에 등록합니다.&lt;/li&gt;
&lt;li&gt;Worker Thread는 Selector로부터 Channel의 이벤트를 감지하고 요청을 처리하며, 처리 결과를 결과를 클라이언트에게 응답합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 Netty의 전반적인 동작 흐름을 알아봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 편에서는 Reactor의 동작을 다룹니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;참고자료&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&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;letter-spacing: 0px;&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/258&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mangkyu.tistory.com/258&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ghkdqhrbals.github.io/portfolios/docs/Java/6/&quot;&gt;https://ghkdqhrbals.github.io/portfolios/docs/Java/6/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;https://velog.io/@wnwjq462/Spring-Webflux-Netty-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@wnwjq462/Spring-Webflux-Netty-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring</category>
      <category>Netty</category>
      <category>Spring</category>
      <category>Webflux</category>
      <author>choicco</author>
      <guid isPermaLink="true">https://choicco.tistory.com/3</guid>
      <comments>https://choicco.tistory.com/3#entry3comment</comments>
      <pubDate>Mon, 24 Mar 2025 10:33:19 +0900</pubDate>
    </item>
  </channel>
</rss>