<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>북극 펭귄</title>
    <link>https://cla9.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Tue, 30 Jun 2026 00:53:52 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>cla9</managingEditor>
    <item>
      <title>13. [istio-internals] Pilot-agent - 사이드카 컨테이너</title>
      <link>https://cla9.tistory.com/197</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번 포스팅에서는 이전 포스팅 내용에 이어서 Sidecar Injector에 의해 주입된 사이드카 컨테이너에 대해서 살펴보겠습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 사이드카 컨테이너&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전 포스팅에서 Application Pod에 주입된&amp;nbsp;init-container에 대해서 살펴봤습니다. init-container는 말 그대로 원래 기동하고자하는 컨테이너 기동 전에 사전 작업 설정을 위해서 잠시 실행되는 임시 컨테이너입니다. 따라서 iptables 등을 조작해서 Application 컨테이너가 기동할 수 있는 환경을 만든 이후 해당 컨테이너는 종료됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이번에는 본격적으로 주입된 사이드카 컨테이너의 Yaml 내용을 살펴보면서, 어떤 설정이 추가되었는지 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;- args:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- proxy
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- sidecar
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --domain
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- $(POD_NAMESPACE).svc.cluster.local
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --proxyLogLevel=warning
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --proxyComponentLogLevel=misc:error
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --log_output_level=default:info
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --concurrency
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;2&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;env:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: JWT_POLICY
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: third-party-jwt
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: PILOT_CERT_PROVIDER
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: istiod
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: CA_ADDR
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: istiod.istio-system.svc:15012
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: POD_NAME
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueFrom:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: metadata.name
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: POD_NAMESPACE
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueFrom:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: metadata.namespace
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: INSTANCE_IP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueFrom:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: status.podIP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: SERVICE_ACCOUNT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueFrom:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: spec.serviceAccountName
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: HOST_IP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueFrom:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: status.hostIP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: PROXY_CONFIG
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: |
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{}&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_POD_PORTS
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: |-
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_APP_CONTAINERS
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_CLUSTER_ID
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: Kubernetes
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_INTERCEPTION_MODE
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: REDIRECT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_WORKLOAD_NAME
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_OWNER
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: kubernetes://apis/v1/namespaces/default/pods/nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_MESH_ID
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: cluster.local
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: TRUST_DOMAIN
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: cluster.local
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;image: docker.io/istio/proxyv2:1.14.2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-proxy
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ports:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- containerPort: 15090
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: http-envoy-prom
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protocol: TCP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;readinessProbe:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;failureThreshold: 30
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;httpGet:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;path: /healthz/ready
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;port: 15021
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;initialDelaySeconds: 1
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;periodSeconds: 2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;timeoutSeconds: 3
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resources:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;limits:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cpu: &quot;2&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;memory: 1Gi
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;requests:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cpu: 10m
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;memory: 40Mi
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;securityContext:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;allowPrivilegeEscalation: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;capabilities:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;drop:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- ALL
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;privileged: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;readOnlyRootFilesystem: true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsGroup: 1337
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsNonRoot: true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsUser: 1337&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 코드는 사이드카로 추가된 istio-proxy 부분만 발췌한 코드입니다.&amp;nbsp; 해당 코드에서 중요한 내용만 추려가면서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;image: docker.io/istio/proxyv2:1.14.2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-proxy&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;먼저 살펴볼 부분은 해당 컨테이너의 이미지입니다. 이는 이전에 init-container에서 사용했던 이미지와 동일함을 알 수 있습니다. 따라서 기동 시점에 pilot-agent 프로그램이 수행되는 것을 알 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;securityContext:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;allowPrivilegeEscalation: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;capabilities:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;drop:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- ALL
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;privileged: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;readOnlyRootFilesystem: true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsGroup: 1337
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsNonRoot: true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsUser: 1337&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;두 번째로 살펴볼 것은 securityContext입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;init-container에는 iptables를 조작해야했기 때문에 root와 NET_ADMIN, NET_RAW와 같은 capability가 필요했는데, 사이드카 컨테이너에서는 이미 iptables 변경이 완료되었기 때문에 더 이상 root로 실행시키지 않으며, capabilities도 모두 삭제하도록 되어있음을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;- args:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- proxy
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- sidecar
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --domain
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- $(POD_NAMESPACE).svc.cluster.local
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --proxyLogLevel=warning
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --proxyComponentLogLevel=misc:error
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --log_output_level=default:info
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --concurrency
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;2&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;세 번째로 살펴볼 것은 argument입니다. 인자를 통해서 해당 프로그램이 proxy로 기동됨을 전달합니다. 여기서 살펴볼 점은 argument를 통해서 sidecar 인자를 넘겨줌에 따라 해당 Container가 sidecar proxy로 기동될 것임을 전달합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두번째 중요한 정보는 --concurrency입니다. 이전에&amp;nbsp;&lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;envoy 아키텍처 포스팅&lt;/span&gt;&lt;/u&gt;에서 확인하였듯이. conccurency는 Worker 쓰레드의 개수를 의미합니다. 즉 해당 envoy proxy는 Master와 2개의 Worker 쓰레드가 구성되어 동작함을 유추할 수 있습니다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Pilot-Agent 기동 과정&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;지금까지 YAML 내용을 살펴보면서 sidecar injector에 의해 주입된 사이드카 컨테이너의 주요 설정에 대해서 알아봤습니다. 이번에는 주입된 컨테이너 내부를 살펴보면서 어떻게 동작하는지를 살펴보고자 합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1769&quot; data-origin-height=&quot;563&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CmIhg/btrNIKhAQ0Z/9xLdMmkzDdJS52b0ROSzcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CmIhg/btrNIKhAQ0Z/9xLdMmkzDdJS52b0ROSzcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CmIhg/btrNIKhAQ0Z/9xLdMmkzDdJS52b0ROSzcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCmIhg%2FbtrNIKhAQ0Z%2F9xLdMmkzDdJS52b0ROSzcK%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;229&quot; data-origin-width=&quot;1769&quot; data-origin-height=&quot;563&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;사이드카 컨테이너 내부에는 pilot-agent 프로그램이 존재합니다. 따라서 해당 프로그램이 먼저 기동됩니다. 이때 pilot-agent 내부에는 여러가지 컴포넌트가 존재하는데, 그 중 가장 핵심은 XDS Proxy 입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;950&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brl0L8/btrNAMHvi8m/I6KNUX7mTeVs4zKDvM0gPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brl0L8/btrNAMHvi8m/I6KNUX7mTeVs4zKDvM0gPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brl0L8/btrNAMHvi8m/I6KNUX7mTeVs4zKDvM0gPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbrl0L8%2FbtrNAMHvi8m%2FI6KNUX7mTeVs4zKDvM0gPk%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;320&quot; height=&quot;523&quot; data-origin-width=&quot;581&quot; data-origin-height=&quot;950&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;XDS Proxy는 Envoy Proxy와 istiod의 pilot-discovery의 통신을 중개하는 역할을 담당하는 컴포넌트로써 Envoy에서의 요청사항을 pilot-discovery에 전달하고 pilot-discovery로부터 전달되는 응답값을 Envoy에게 다시 전달하는 역할을 수행합니다. 이때 내부 Envoy와의 통신은 Unix Domain Socket으로 수행하며 pilot-discovery와는 gRPC를 통해 요청을 주고 받는 것이 특징입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;Local DNS Server는 istio의 ServiceEntry에서 DNS에 등록되지 않은 host명으로 입력한 경우 올바르게 Resolution되지 않습니다. 이때 내부 Local DNS Server를 둠으로써 Kubernetes 외부에 위치한 Service에 대해서도 DNS 결과를 제공하기 위해 제공되는 서버입니다. 해당 서버는 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;ISTIO_META_DNS_CAPTURE&lt;/span&gt;&lt;/b&gt; 환경 변수 여부에 따라 기동 여부가 결정되며, 기본적으로는 false이므로 기동되지 않습니다. 이와 관련한 자세한 내용은 아래 공식문서를 참고하시기 바랍니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;a href=&quot;https://istio.io/latest/docs/ops/configuration/traffic-management/dns-proxy/&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://istio.io/latest/docs/ops/configuration/traffic-management/dns-proxy/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;DNS Proxying&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;How to configure DNS proxying.&quot; data-og-host=&quot;istio.io&quot; data-og-source-url=&quot;https://istio.io/latest/docs/ops/configuration/traffic-management/dns-proxy/&quot; data-og-image=&quot;&quot; data-og-url=&quot;https://istio.io/latest/docs/ops/configuration/traffic-management/dns-proxy/&quot;&gt;&lt;a href=&quot;https://istio.io/latest/docs/ops/configuration/traffic-management/dns-proxy/&quot; target=&quot;_blank&quot; data-source-url=&quot;https://istio.io/latest/docs/ops/configuration/traffic-management/dns-proxy/&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;DNS Proxying&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;How to configure DNS proxying.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;istio.io&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;SDS Server는 istio에서 제공하는 mTLS 기능 구현을 위해 CA에게 인증서를 요청하고 제공된 인증서를 관리하기 위한 서버입니다. 해당 컴포넌트에 대해서는 추후 다른 포스팅을 통해 다루어보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;Pilot-Agent 내부에 속한 주요 컴포넌트에 대해 간략하게 살펴봤습니다. 지금부터는 envoy와 xds proxy를 중점으로 구동 과정을 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1 Proxy 초기화&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1423&quot; data-origin-height=&quot;847&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckukTu/btrNDU6ePlk/CX2GOVToqc6KkZJ99rWtV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckukTu/btrNDU6ePlk/CX2GOVToqc6KkZJ99rWtV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckukTu/btrNDU6ePlk/CX2GOVToqc6KkZJ99rWtV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckukTu%2FbtrNDU6ePlk%2FCX2GOVToqc6KkZJ99rWtV0%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;1423&quot; height=&quot;847&quot; data-origin-width=&quot;1423&quot; data-origin-height=&quot;847&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;pilot-agent가 기동될 때 가장 먼저 수행하는 것은 Proxy 생성을 위한 초기화 작업입니다. 사이드카 컨테이너의 YAML을 살펴보면 위 그림과 같이 Pod의 IP와 이름 그리고 namespace가 환경변수로 주입된 것을 확인할 수 있는데, 해당 값등을 조합하여 Proxy 설정을 진행합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;- args:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- proxy
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- sidecar
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --domain
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- $(POD_NAMESPACE).svc.cluster.local
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --proxyLogLevel=warning
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --proxyComponentLogLevel=misc:error
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --log_output_level=default:info
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --concurrency
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;2&quot;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Proxy 초기화 과정에서는 입력된 IP와 namespace를 지정하는 것 외에도 ID와 DNS Domain을 수행하는데, ID는 입력된 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;POD_NAME + &quot;.&quot; + POD_NAMESPACE&lt;/span&gt;&lt;/b&gt;로 지정됩니다. 반면 DNS Domain의 경우 위와 같이 Argument에 --domain 인자로 값이 입력되었으면, 해당 값이 사용되고 입력되지 않았을 경우에는 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;POD_NAMESPACE + &quot;.svc.cluster.loocal&quot;&lt;/span&gt;&lt;/b&gt; 이 사용됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2 Proxy Config 설정&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;1254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JH6wO/btrNIKojLzc/iliM0DZMGi5vHKkbIgy50k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JH6wO/btrNIKojLzc/iliM0DZMGi5vHKkbIgy50k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JH6wO/btrNIKojLzc/iliM0DZMGi5vHKkbIgy50k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJH6wO%2FbtrNIKojLzc%2FiliM0DZMGi5vHKkbIgy50k%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;1572&quot; height=&quot;1254&quot; data-origin-width=&quot;1572&quot; data-origin-height=&quot;1254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Proxy 초기화 이후 수행 작업은 Proxy Config를 설정하는 것입니다. 이때 Proxy Config를 구성하기 위해 세 군데에서 입력되는 정보들을 조합하기 위해 값을 읽어들입니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;Pod에 입력한 annotation 정보들은 kubernetes의 downwardAPI에 의하여 Pod 내부 /etc/istio/pod/annotations 경로에 Mount 됩니다. 해당 정보는 pilot-agent가 기동하면서 참조할 수 있으며, 이를 통해 annotation 정보를 추출할 수 있습니다. 여기서 annotation 중 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;proxy.istio.io/config&lt;/span&gt;&lt;/b&gt; 값은 Pod의 Proxy Config를 설정할 수 있습니다. 따라서 Proxy Config를 구성하는 과정에서 해당 정보를 참고합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두 번째는 Pod 내부에 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;/etc/istio/config/mesh&lt;/span&gt;&lt;/b&gt;에 Proxy Config 정보를 위한 파일을 mount 했다면, 해당 정보를 읽어들여서 Proxy Config를 구성합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;세 번째는 환경 변수로 입력한 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;PROXY_CONFIG &lt;/span&gt;&lt;/b&gt;정보를 참조하는 것입니다. 해당 정보는 sidecar injector에 의해서 주입된 정보이며, Global Proxy Config 설정을 했다면, sidecar injector가 Pod의 YAML을 조작하는 과정에서 해당 정보가 주입됩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2176&quot; data-origin-height=&quot;102&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kRu05/btrNKfBzxm1/llUqPhm0TEi8qFzR1nR9k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kRu05/btrNKfBzxm1/llUqPhm0TEi8qFzR1nR9k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kRu05/btrNKfBzxm1/llUqPhm0TEi8qFzR1nR9k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkRu05%2FbtrNKfBzxm1%2FllUqPhm0TEi8qFzR1nR9k0%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;2176&quot; height=&quot;102&quot; data-origin-width=&quot;2176&quot; data-origin-height=&quot;102&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 세 정보를 추출한 이후에는 Proxy Config를 구성합니다. 이때 pilot-agent 내부에 별도 Default Config가 존재하며, Default Config 외에 입력된 세 가지 Config 정보를 차례 차례로 merge 하여 구성합니다. 이때 위 그림과 같은 순서대로 merge 작업을 수행하며, 같은 값이 존재하는 경우에는 덮어씁니다. 따라서 우선순위 기준으로 보면 Pod에 부착된 어노테이션이 가장 높습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/197</guid>
      <comments>https://cla9.tistory.com/197#entry197comment</comments>
      <pubDate>Tue, 3 Mar 2026 11:51:15 +0900</pubDate>
    </item>
    <item>
      <title>12. [istio-internals] Pilot-agent - init-container</title>
      <link>https://cla9.tistory.com/196</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전 포스팅에서는 istiod의 수동 방식과 pilot-discovery Sidecar Injector에 의해 자동 주입된 envoy proxy 과정에 대해서 살펴봤습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;517&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wduii/btrKOI9gHnd/2d64uxNostufD5ualMwr1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wduii/btrKOI9gHnd/2d64uxNostufD5ualMwr1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wduii/btrKOI9gHnd/2d64uxNostufD5ualMwr1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwduii%2FbtrKOI9gHnd%2F2d64uxNostufD5ualMwr1k%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;180&quot; height=&quot;272&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;517&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Sidecar Injector에 의해 주입된 변경된 Pod를 살펴보면 istio-init이라는 init-container와 istio-proxy라는 사이드카 컨테이너 총 2개가 주입되는 것을 확인할 수 있습니다. 이번 포스팅에서는 그 중 init-container 부분에 대해서 다루어 보고자합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;우선 본론에 들어가기 앞서 간단히 해당 컨테이너의 역할에 대해서 살펴보면, envoy가 application과 트래픽을 주고 받고 istio control plane과 통신을 원활하게 수행하기 위한 iptables를 조작합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;지금부터 본격적으로 iptables 조작 과정이 어떻게 이루어지는지에 대해서 알아보겠습니다. 다만 필자가 네트워크나 서버 전문가가 아니기 때문에 틀린 부분이 있을 수 있습니다. 틀린 부분이 있다면, 꼭 알려주시기 바랍니다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. iptables&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Kubernetes에서 네트워크 트래픽을 다루는데 있어 핵심은 iptables와 Netfilter 입니다. 이를 통해서 외부로 트래픽을 전달하기도 하고 내부 컨테이터로 트래픽을 전달합니다. istio 또한 inbound와 outbound 트래픽을 envoy로 전달하고 외부로 보내야하기 때문에 Netfilter에서 제공하는 hook을 사용하여 패킷의 흐름을 변경하며 이를 위해서 사이드카 컨테이너를 주입할 때 iptables 체인을 만들고 라우팅 규칙을 설정하는 작업을 선행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;iptables와 Netfilter에 대해서는 내용이 방대하고 깊으므로 별도 학습을 권장드리며, 본 포스팅에서는 envoy의 설정과 네트워크 조작에 대해 알아보기 이전에 가볍게 iptables에서 사용되는 네트워크 체인에 대해서만 가볍게 다루어보고 넘어가겠습니다.&lt;br&gt;&amp;nbsp;&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;737&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHed1f/btrKLaJFPHp/OxxfmqMdUrXD40BeA3er6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHed1f/btrKLaJFPHp/OxxfmqMdUrXD40BeA3er6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHed1f/btrKLaJFPHp/OxxfmqMdUrXD40BeA3er6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHed1f%2FbtrKLaJFPHp%2FOxxfmqMdUrXD40BeA3er6k%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;737&quot; data-origin-width=&quot;1282&quot; data-origin-height=&quot;737&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;iptables에는 기본적으로 위와같이 5개의 체인이 등록되어있습니다. 각각의 체인은 역할이 존재하는데, 체인들의 관계와 흐름을 이해하는 것이 중요합니다. 따라서 위 그림을 기반으로 각 체인의 특징에 대해 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;1. PREROUTING&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;PREROUTING&lt;/b&gt;은 Packet이 처음 도달하는 체인으로 패킷 내용을 조사하여 목적지가 로컬 주소인지 아닌지를 판단합니다. 이때 만약 로컬이라면, &lt;b&gt;INPUT&lt;/b&gt; 체인으로 전달하고 다른 host로 전달되어야한다면 &lt;b&gt;FORWARDING&lt;/b&gt;으로 전달합니다. 그리고&amp;nbsp; 해당 체인을 통해서 목적지의 주소 변경(DNAT)이 가능한 것이 주요 특징 중 하나입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇다면 DNAT는 왜 할까요?&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH2UJU/btrKLfYqdyF/Aj8bBg18JCavCyOeHctt71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH2UJU/btrKLfYqdyF/Aj8bBg18JCavCyOeHctt71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH2UJU/btrKLfYqdyF/Aj8bBg18JCavCyOeHctt71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH2UJU%2FbtrKLfYqdyF%2FAj8bBg18JCavCyOeHctt71%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;480&quot; height=&quot;284&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;446&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;가령 Docker를 예로 들자면, 생성한 도커 컨테이너를 외부에서 접속 가능하게 하기 위해 흔히 포트 포워딩을 수행합니다. 이때 위 그림과 같이 특정 서버 포트(8080)에 대해서 컨테이너 내부 포트와 연결시키면, 위와 같이 PREROUTING을 통해 들어온 트래픽은 DOCKER 체인으로 전달되고 그 과정에서 8080 포트로 들어온 연결에 대해서 목적지 주소를 변경하는 작업을 수행해서 이후 컨테이너에 트래픽이 전달됩니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;2. INPUT&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;INPUT&lt;/b&gt;은 &lt;b&gt;PREROUTING&lt;/b&gt;을 통해 전달되는 Packet이 로컬 주소의 목적지를 향할 경우에 해당 체인으로 라우팅됩니다. 만약 해당 체인을 통해 최종 목적지를 지정하면 그쪽으로 트래픽을 전달할 수 있습니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;3. OUTPUT&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;OUTPUT&lt;/b&gt;은 외부에서 들어오는 패킷이 아니라 서버에서 생성되어 나가는 패킷이 발생될 때 트리거링되는 체인입니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;4. FORWARDING&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;FORWARDING&lt;/b&gt;은 해당 서버로 보낸 패킷은 아니지만&amp;nbsp; 외부 패킷을 외부로 전달하는 경우 설정 여부에 따라 패킷을 전달할지 여부를 결정합니다. 사례를 통해 조금 더 자세히 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dCXx0K/btrKF7g0LuS/9WdRKHzuORknFDcGkdA5Ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dCXx0K/btrKF7g0LuS/9WdRKHzuORknFDcGkdA5Ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dCXx0K/btrKF7g0LuS/9WdRKHzuORknFDcGkdA5Ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdCXx0K%2FbtrKF7g0LuS%2F9WdRKHzuORknFDcGkdA5Ik%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;1422&quot; height=&quot;347&quot; data-origin-width=&quot;1422&quot; data-origin-height=&quot;347&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;위와 같이 2개의 서버를 연결하는 라우터로써 사용될 때 중간에 있는 서버는 외부에서 들어온 패킷을 외부로 전달하는 역할을 수행합니다. 이때 패킷을 다른 서버에 전달하기 위해서는 sysctl.conf 파일에서 net.ipv4.ip_forward 값을 1로 설정이 필요합니다. 설정이 완료되면, 해당 체인을 통해서 지정된 Gateway를 통해 목적지로 전달될 것입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXZ6Uz/btrKHaxibB7/qdbdHXbOx3UECfCgEYTAW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXZ6Uz/btrKHaxibB7/qdbdHXbOx3UECfCgEYTAW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXZ6Uz/btrKHaxibB7/qdbdHXbOx3UECfCgEYTAW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXZ6Uz%2FbtrKHaxibB7%2FqdbdHXbOx3UECfCgEYTAW0%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;480&quot; height=&quot;390&quot; data-origin-width=&quot;776&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;br&gt;두 번째 사례는 위 그림과 같이 단일 서버내라도 Bridge interface로 구성되어있고 하위에 별도 Network namespace와 서로 다른 네트워크 주소로 구성된 환경에서 namespace간 통신을 수행할 때 해당 체인 설정이 필요할 수 있습니다. 가령 FORWARD 체인에 대해서 Policy가 DROP으로 되어있을 경우 출발지 IP에 대해 ACCEPT 하도록 정책을 등록할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;위 상황에서 같은 서버내 통신인데도 FORWARDING 체인으로 전달되는 이유는 서로 다른 네트워크 주소를 가지고 있기 때문에 host 입장에서는 출발지와 목적지 모두가 외부 패킷이기 때문입니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size18&quot;&gt;5. POSTROUTING&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;POSTROUTING&lt;/b&gt;은 OUTPUT에서 전달된 패킷이나 FORWARDING을 통해서 전달된 패킷을 통해 네트워크 인터페이스를 통해 나갈 패킷에 대한 처리를 수행할 수 있습니다. 이때 전달된 패킷에 대하여 출발지 주소를 변경(SNAT)할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1581&quot; data-origin-height=&quot;358&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VDJyP/btrKGQsoEr3/hSXkEFz7kobPyEVAd7C3ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VDJyP/btrKGQsoEr3/hSXkEFz7kobPyEVAd7C3ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VDJyP/btrKGQsoEr3/hSXkEFz7kobPyEVAd7C3ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVDJyP%2FbtrKGQsoEr3%2FhSXkEFz7kobPyEVAd7C3ek%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;1581&quot; height=&quot;358&quot; data-origin-width=&quot;1581&quot; data-origin-height=&quot;358&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;SNAT의 경우는 공인 IP와 사설 IP로 구성되었을 경우 사설 IP 대역에서 외부 인터넷 접속 시 돌아올 목적지를 공인 IP로 지정해야되기 때문에 출발지 주소를 서버의 IP로 변경해서 나갑니다. 따라서 이 경우 해당 체인을 통해 출발지 주소를 변경할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. init conatiner&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;지금까지 iptables에서 사용되는 체인에 대해서 살펴봤습니다. 지금부터는 이전 포스팅에서 살펴본 istio에 의해서 주입된 사이드카 컨테이너 설정을 차근 차근 살펴보면서 어떤 것이 적용되었는지를 분석해보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;initContainers:
&amp;nbsp;&amp;nbsp;- args:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- istio-iptables
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -p
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;15001&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -z
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;15006&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -u
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;1337&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -m
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- REDIRECT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -i
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- '*'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -x
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -b
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- '*'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -d
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 15090,15021,15020
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;image: docker.io/istio/proxyv2:1.14.2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-init
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resources:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;limits:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cpu: &quot;2&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;memory: 1Gi
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;requests:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cpu: 10m
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;memory: 40Mi
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;securityContext:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;allowPrivilegeEscalation: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;capabilities:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;add:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- NET_ADMIN
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- NET_RAW
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;drop:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- ALL
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;privileged: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;readOnlyRootFilesystem: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsGroup: 0
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsNonRoot: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsUser: 0&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;istio에 의해서 변경된 Pod의 yaml을 살펴보면, 위와 같이 istio-init으로 명명된 init container가 포함되었으며 이 단계에서 iptables를 조작한다는 것을 알 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;위 코드 내용을 분할하여 자세히 살펴보겠습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;securityContext:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;allowPrivilegeEscalation: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;capabilities:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;add:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- NET_ADMIN
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- NET_RAW
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;drop:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- ALL
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;privileged: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;readOnlyRootFilesystem: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsGroup: 0
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsNonRoot: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsUser: 0&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;먼저 iptables를 조작하기 위해서 securityContext에서 NET_ADMIN과 NET_RAW에 대한 capabilites를 추가하고 그 외 나머지는 drop 하도록 하였습니다. 그리고 해당 container를 root 유저로 기동하는 것을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;image: docker.io/istio/proxyv2:1.14.2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-init&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;두번째로 살펴볼 것은 proxyv2 이미지입니다. 해당 컨테이너 이미지명이 proxyv2라고 되어있지만 실제로는 envoy proxy를 기반으로 만들어진 이미지입니다. 해당 이미지에 대한 Dockerfile 명세는 아래 URL을 통해서 확인해볼 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;a href=&quot;https://github.com/istio/istio/blob/master/pilot/docker/Dockerfile.proxyv2&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://github.com/istio/istio/blob/master/pilot/docker/Dockerfile.proxyv2&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;GitHub - istio/istio: Connect, secure, control, and observe services.&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Connect, secure, control, and observe services. Contribute to istio/istio development by creating an account on GitHub.&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/istio/istio/blob/master/pilot/docker/Dockerfile.proxyv2&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/cSntkZ/hyPAz5bDQL/AAAAAAAAAAAAAAAAAAAAAPHTVazrYTD08WEiliBliVEW0_45pHML8EWaWWyqxyzg/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1774969199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=LThXFtSuDGgsryNRUVE2XA3J4Ho%3D&quot; data-og-url=&quot;https://github.com/istio/istio&quot;&gt;&lt;a href=&quot;https://github.com/istio/istio&quot; target=&quot;_blank&quot; data-source-url=&quot;https://github.com/istio/istio/blob/master/pilot/docker/Dockerfile.proxyv2&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/cSntkZ/hyPAz5bDQL/AAAAAAAAAAAAAAAAAAAAAPHTVazrYTD08WEiliBliVEW0_45pHML8EWaWWyqxyzg/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1774969199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=LThXFtSuDGgsryNRUVE2XA3J4Ho%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;GitHub - istio/istio: Connect, secure, control, and observe services.&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Connect, secure, control, and observe services. Contribute to istio/istio development by creating an account on GitHub.&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;github.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;해당 이미지는 pilot-agent를 기동하는데, 이때 argument를 통해서 해당 프로그램이 어떤 동작을 수행하는지를 정의할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;- args:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- istio-iptables
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -p
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;15001&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -z
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;15006&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -u
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;1337&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -m
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- REDIRECT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -i
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- '*'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -x
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -b
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- '*'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -d
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 15090,15021,15020&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;세번째로 살펴볼 것은 argument 입니다. argument로 istio-iptables 옵션을 전달하면 내부에서 istio-iptables.sh을 수행해서 iptables를 조작하는 작업을 수행합니다. 위 코드에서 입력된 옵션에 대해서 살펴보면 다음과 같습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 144px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 19.4186%; height: 16px; text-align: center;&quot;&gt;옵션&lt;/td&gt;&lt;td style=&quot;width: 80.5814%; height: 16px; text-align: center;&quot;&gt;설명&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 19.4186%; height: 16px; text-align: justify;&quot;&gt;-p&lt;/td&gt;&lt;td style=&quot;width: 80.5814%; height: 16px; text-align: justify;&quot;&gt;Traffic을 redirect 받을 envoy의 포트&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 19.4186%; height: 16px; text-align: justify;&quot;&gt;-z&lt;/td&gt;&lt;td style=&quot;width: 80.5814%; height: 16px; text-align: justify;&quot;&gt;Inbound TCP traffic에 대해서 redirect 포트&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 19.4186%; height: 16px; text-align: justify;&quot;&gt;-u&lt;/td&gt;&lt;td style=&quot;width: 80.5814%; height: 16px; text-align: justify;&quot;&gt;proxy container의 UID&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 19.4186%; height: 16px; text-align: justify;&quot;&gt;-m&lt;/td&gt;&lt;td style=&quot;width: 80.5814%; height: 16px; text-align: justify;&quot;&gt;envoy로 redirect되는 Inbound 연결에 사용되는 모드로써 REDIRECT 혹은 TPROXY 중 선택 가능&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 19.4186%; height: 16px; text-align: justify;&quot;&gt;-i&lt;/td&gt;&lt;td style=&quot;width: 80.5814%; height: 16px; text-align: justify;&quot;&gt;envoy로 redirect 시킬 CIDR IP Range로 복수개 입력 시 &quot;,&quot; 로 구분하여 list 형태로 입력한다. 해당 값이 *일 경우 모두 outbound로 redirect 하며, 값이 없을 경우에는 모든 outbound 트래픽을 disable 한다.&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 19.4186%; height: 16px; text-align: justify;&quot;&gt;-x&lt;/td&gt;&lt;td style=&quot;width: 80.5814%; height: 16px; text-align: justify;&quot;&gt;envoy로 redirect 제외 시킬 CIDR IP Range로 복수개 입력 시 &quot;,&quot; 로 구분하여 list 형태로 입력함&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 19.4186%; height: 16px; text-align: justify;&quot;&gt;-b&lt;/td&gt;&lt;td style=&quot;width: 80.5814%; height: 16px; text-align: justify;&quot;&gt;envoy로 redirect 시킬 Inbound Port를 명시하는 것으로써 복수개 입력 시 &quot;,&quot; 로 구분하여 list 형태로 입력함&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 19.4186%; height: 16px; text-align: justify;&quot;&gt;-d&lt;/td&gt;&lt;td style=&quot;width: 80.5814%; height: 16px; text-align: justify;&quot;&gt;envoy로 redirect 제외시킬 Inbound Port를 명시하는 것으로써 복수개 입력 시 &quot;,&quot; 로 구분하여 list 형태로 입력함&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 예제에서 작성한 옵션을 기반으로 설명하면, 위 args는 다음과 같은 명령을 요구하였음을 알 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1) Inboud의 경우 15006&amp;nbsp; 그 외는 15001 포트로 전달한다.&lt;br&gt;2) 해당 proxy를 사용하는 컨테이너의 UID는 1337이다&lt;br&gt;3) 모든 IP로부터 전달되는 트래픽은 모두 envoy로 redirect한다.&lt;br&gt;4) envoy redirect 하는데 있어 제외대상 IP는 없다.&lt;br&gt;5) 15090, 15021, 15020 포트를 제외한 나머지 포트를 통해 들어오는 트래픽은 envoy로 redirect한다.&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 내용에서 15090, 15021, 15020 포트 등은 envoy의 헬스체크와 prometheus 연결 등에 사용하기 위한 포트로 사용되기 때문에 제외하였습니다. 그 밖에도 istio에서 사용하는 여러 포트가 있는데, 해당 내용은 &lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;공식문서&lt;/span&gt;&lt;/u&gt;를 참고하시기 바랍니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;참고로 위 istio-iptables의 실제 동작 과정이나 위에 설명한 옵션 외에 다른 옵션이 무엇이 있는지 궁금하신 분은 &lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;istio github&lt;/span&gt;&lt;/u&gt;을 참고하시기 바랍니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. iptables 룰 변경 내역 확인&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전 내용들을 토대로 init container 안에서 iptables를 조작함을 확인했습니다. 이번에는 init container를 통해서 변경된 내용을 확인하기 위해 istio-init container의 로그를 확인해보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[cla9@DESKTOP-FBK64T0]$ kubectl logs -c istio-init nginx
2022-08-26T00:34:38.813305Z&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; info&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Istio iptables environment:
ENVOY_PORT=
INBOUND_CAPTURE_PORT=
ISTIO_INBOUND_INTERCEPTION_MODE=
ISTIO_INBOUND_TPROXY_ROUTE_TABLE=
ISTIO_INBOUND_PORTS=
ISTIO_OUTBOUND_PORTS=
ISTIO_LOCAL_EXCLUDE_PORTS=
ISTIO_EXCLUDE_INTERFACES=
ISTIO_SERVICE_CIDR=
ISTIO_SERVICE_EXCLUDE_CIDR=
ISTIO_META_DNS_CAPTURE=
INVALID_DROP=

2022-08-26T00:34:38.813346Z&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; info&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Istio iptables variables:
PROXY_PORT=15001
PROXY_INBOUND_CAPTURE_PORT=15006
PROXY_TUNNEL_PORT=15008
PROXY_UID=1337
PROXY_GID=1337
INBOUND_INTERCEPTION_MODE=REDIRECT
INBOUND_TPROXY_MARK=1337
INBOUND_TPROXY_ROUTE_TABLE=133
INBOUND_PORTS_INCLUDE=*
INBOUND_PORTS_EXCLUDE=15090,15021,15020
OUTBOUND_OWNER_GROUPS_INCLUDE=*
OUTBOUND_OWNER_GROUPS_EXCLUDE=
OUTBOUND_IP_RANGES_INCLUDE=*
OUTBOUND_IP_RANGES_EXCLUDE=
OUTBOUND_PORTS_INCLUDE=
OUTBOUND_PORTS_EXCLUDE=
KUBE_VIRT_INTERFACES=
ENABLE_INBOUND_IPV6=false
DNS_CAPTURE=false
DROP_INVALID=false
CAPTURE_ALL_DNS=false
DNS_SERVERS=[],[]
OUTPUT_PATH=
NETWORK_NAMESPACE=
CNI_MODE=false
EXCLUDE_INTERFACES=

2022-08-26T00:34:38.813822Z&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; info&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Writing following contents to rules file: /tmp/iptables-rules-1661474078813392800.txt3716182416
* nat
-N ISTIO_INBOUND
-N ISTIO_REDIRECT
-N ISTIO_IN_REDIRECT
-N ISTIO_OUTPUT
-A ISTIO_INBOUND -p tcp --dport 15008 -j RETURN
-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006
-A PREROUTING -p tcp -j ISTIO_INBOUND
-A ISTIO_INBOUND -p tcp --dport 15090 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15021 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15020 -j RETURN
-A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT
-A OUTPUT -p tcp -j ISTIO_OUTPUT
-A ISTIO_OUTPUT -o lo -s 127.0.0.6/32 -j RETURN
-A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --gid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
-A ISTIO_OUTPUT -j ISTIO_REDIRECT
COMMIT&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 코드는 istio-init container의 로그 일부를 발췌하였습니다. 많은 내용이 있지만 이 중에서 유의미한 내용만 추려서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;PROXY_PORT=15001
PROXY_INBOUND_CAPTURE_PORT=15006
PROXY_UID=1337
PROXY_GID=1337
INBOUND_INTERCEPTION_MODE=REDIRECT
INBOUND_PORTS_INCLUDE=*
INBOUND_PORTS_EXCLUDE=15090,15021,15020
OUTBOUND_OWNER_GROUPS_INCLUDE=*
OUTBOUND_OWNER_GROUPS_EXCLUDE=
OUTBOUND_IP_RANGES_INCLUDE=*
OUTBOUND_IP_RANGES_EXCLUDE=&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 내용들을 살펴보면, 이전에 arg 인자로 전달했던 옵션들이 정상적으로 매칭되었음을 확인할 수 있습니다. 먼저 inbound 트래픽은 15006 포트로 전달되도록 되어있고 outbound의 경우는 15001로 지정되었음을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;PROXY의 UID와 GID는 1337로 이 또한 인자로 지정한 값입니다. PORT 전달 대상은 15090, 15021, 15020을 제외한 나머지 PORT를 모두 INBOUND로 전달하도록 되어있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;* nat
-N ISTIO_INBOUND
-N ISTIO_REDIRECT
-N ISTIO_IN_REDIRECT
-N ISTIO_OUTPUT
-A ISTIO_INBOUND -p tcp --dport 15008 -j RETURN
-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006
-A PREROUTING -p tcp -j ISTIO_INBOUND
-A ISTIO_INBOUND -p tcp --dport 15090 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15021 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15020 -j RETURN
-A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT
-A OUTPUT -p tcp -j ISTIO_OUTPUT
-A ISTIO_OUTPUT -o lo -s 127.0.0.6/32 -j RETURN
-A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --gid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
-A ISTIO_OUTPUT -j ISTIO_REDIRECT&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Argument로 전달된 값을 기반으로 생성된 NAT 체인 룰을 보면 위와 같이 지정된 것을 로그를 통해 확인할 수 있습니다. 그렇다면 구체적으로 어떤 값이 어떻게 변경되었을까요? 이에 대해서 조금 자세히 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;로그 내용을 살펴보면 먼저 4개의 체인을 생성한 것을 확인할 수 있습니다. 여기서 각각의 체인은 다음과 같은 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;ISTIO_INBOUND&lt;/b&gt; : PREROUTING으로 전달된 트래픽 중 tcp 트래픽 전달받음&lt;br&gt;&lt;b&gt;ISTIO_REDIRECT&lt;/b&gt; : Outbound Traffic을 envoy의 Outbound Handler인 15001 포트로 전달함&lt;br&gt;&lt;b&gt;ISTIO_IN_REDIRECT&lt;/b&gt; : Inbound Traffic을 envoy의 Inbound Handler인 15006 포트로 전달함&lt;br&gt;&lt;b&gt;ISTIO_OUTPUT&lt;/b&gt; : envoy traffic을 결정하는 가장 핵심적인 체인으로 traffic 전달과 관련한 규칙이 정의되어있음.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&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;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4VK32/btrKIrd4CDk/0DQpGZW3MuYekVR5T0s25K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4VK32/btrKIrd4CDk/0DQpGZW3MuYekVR5T0s25K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4VK32/btrKIrd4CDk/0DQpGZW3MuYekVR5T0s25K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4VK32%2FbtrKIrd4CDk%2F0DQpGZW3MuYekVR5T0s25K%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;524&quot; data-origin-width=&quot;954&quot; data-origin-height=&quot;524&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;체인을 생성하고 난 이후에는 체인 호출 및 트래픽 라우팅과 관련된 규칙을 설정하고 있습니다. 위 설정 중에서 가장 중요한 것은 ISTIO_OUTPUT이며, 해당 체인에 적용된 규칙에 의하여 그 다음에 전달할 체인이 결정됩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;-A ISTIO_OUTPUT -o lo -s 127.0.0.6/32 -j RETURN
-A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --gid-owner 1337 -j ISTIO_IN_REDIRECT
-A ISTIO_OUTPUT -o lo -m owner ! --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN
-A ISTIO_OUTPUT -j ISTIO_REDIRECT&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;참고로 ISTIO_OUTPUT에 적용된 규칙은 위와 같으며 해당 규칙을 조금 더 풀어서 설명하면 다음과 같습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 192px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;&lt;tbody&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;width: 23.2558%; height: 20px; text-align: center;&quot;&gt;처리 결과&lt;/td&gt;&lt;td style=&quot;width: 21.0465%; height: 20px; text-align: center;&quot;&gt;Out Interface&lt;/td&gt;&lt;td style=&quot;width: 39.8837%; height: 20px; text-align: center;&quot;&gt;조건&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 20px;&quot;&gt;&lt;td style=&quot;width: 23.2558%; height: 20px; text-align: justify;&quot;&gt;RETURN&lt;/td&gt;&lt;td style=&quot;width: 21.0465%; height: 20px; text-align: justify;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;loopback 인터페이스&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 39.8837%; height: 20px; text-align: justify;&quot;&gt;출발지가 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;127.0.0.6&lt;/span&gt;&lt;/b&gt; 일 경우&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 40px;&quot;&gt;&lt;td style=&quot;width: 23.2558%; height: 40px; text-align: justify;&quot;&gt;ISTIO_IN_REDIRECT&lt;/td&gt;&lt;td style=&quot;width: 21.0465%; height: 40px; text-align: justify;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;loopback 인터페이스&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 39.8837%; height: 40px; text-align: justify;&quot;&gt;목적지가 localhost(127.0.0.1)가 아니면서 UID가 1337일 경우&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 23.2558%; height: 16px; text-align: justify;&quot;&gt;RETURN&lt;/td&gt;&lt;td style=&quot;width: 21.0465%; height: 16px; text-align: justify;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;loopback 인터페이스&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 39.8837%; height: 16px; text-align: justify;&quot;&gt;UID가 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;1337&lt;/span&gt;&lt;/b&gt;가 아닐 경우&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 23.2558%; height: 16px; text-align: justify;&quot;&gt;RETURN&lt;/td&gt;&lt;td style=&quot;width: 21.0465%; height: 16px; text-align: justify;&quot;&gt;*&lt;/td&gt;&lt;td style=&quot;width: 39.8837%; height: 16px; text-align: justify;&quot;&gt;UID가 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;1337&lt;/span&gt;&lt;/b&gt;일 경우&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 23.2558%; height: 16px; text-align: justify;&quot;&gt;ISTIO_IN_REDIRECT&lt;/td&gt;&lt;td style=&quot;width: 21.0465%; height: 16px; text-align: justify;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;loopback 인터페이스&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 39.8837%; height: 16px; text-align: justify;&quot;&gt;목적지가 localhost(127.0.0.1)가 아니면서 GID가 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;1337&lt;/span&gt;&lt;/b&gt;일 경우&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 23.2558%; height: 16px; text-align: justify;&quot;&gt;RETURN&lt;/td&gt;&lt;td style=&quot;width: 21.0465%; height: 16px; text-align: justify;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;loopback 인터페이스&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;&lt;td style=&quot;width: 39.8837%; height: 16px; text-align: justify;&quot;&gt;GID가 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;1337&lt;/span&gt;&lt;/b&gt;가 아닐 경우&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 23.2558%; height: 16px; text-align: justify;&quot;&gt;RETURN&lt;/td&gt;&lt;td style=&quot;width: 21.0465%; height: 16px; text-align: justify;&quot;&gt;*&lt;/td&gt;&lt;td style=&quot;width: 39.8837%; height: 16px; text-align: justify;&quot;&gt;GID가 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;1337&lt;/span&gt;&lt;/b&gt;일 경우&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 23.2558%; height: 16px; text-align: justify;&quot;&gt;RETURN&lt;/td&gt;&lt;td style=&quot;width: 21.0465%; height: 16px; text-align: justify;&quot;&gt;*&lt;/td&gt;&lt;td style=&quot;width: 39.8837%; height: 16px; text-align: justify;&quot;&gt;출발지가 localhost(127.0.0.1)일 경우&lt;/td&gt;&lt;/tr&gt;&lt;tr style=&quot;height: 16px;&quot;&gt;&lt;td style=&quot;width: 23.2558%; height: 16px; text-align: justify;&quot;&gt;ISTIO_REDIRECT&lt;/td&gt;&lt;td style=&quot;width: 21.0465%; height: 16px; text-align: justify;&quot;&gt;*&lt;/td&gt;&lt;td style=&quot;width: 39.8837%; height: 16px; text-align: justify;&quot;&gt;*&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 규칙에 따라 외부에서 접속하는 요청을 envoy에 전달하고 다시 envoy inbound handler에서 나온 트래픽을 application에 전달합니다. 그리고 반대로 application에서 나오는 트래픽을 외부로 전달하는 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;체인의 내용만 봐서는 트래픽이 어떤 흐름으로 어떻게 도달되는지 감이 잡히지는 않습니다. 다만 이번 포스팅은 설정과 관련된 내용만을 다루기 때문에 트래픽 전달에 대해서는 추후에 다루어보도록 하고 여기서는 위 설정 관련해서 알아야할 주요 포인트 3가지 (loopback 인터페이스, 127.0.0.6 IP 존재 이유, 1337 UID/GID)에 대해서 알아보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1 loopback 인터페이스&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;491&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nU7ra/btrKJ6HHbku/eEU8r5yxkB9pMsGvgzqWG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nU7ra/btrKJ6HHbku/eEU8r5yxkB9pMsGvgzqWG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nU7ra/btrKJ6HHbku/eEU8r5yxkB9pMsGvgzqWG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnU7ra%2FbtrKJ6HHbku%2FeEU8r5yxkB9pMsGvgzqWG0%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;520&quot; height=&quot;247&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;491&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;일반적으로 쿠버네티스를 통해 1개의 컨테이너가 포함된 POD를 배포하면 왼쪽과 같이 배포된다고 생각하지만, 실제로는 오른쪽과 같이 사용자에게는 보이지 않는 Pause 컨테이너가 포함된 형태로 배포됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;여기서 Pause 컨테이너는 네트워크, IPC namespace를 생성하고 다른 컨테이너와 공유하는 역할을 수행하며, init process의 역할을 수행해서 좀비 프로세스를 방지하는 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;577&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kFTP2/btrKHdgOrUk/lLTQBGoN0xjUgJbv73ps90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kFTP2/btrKHdgOrUk/lLTQBGoN0xjUgJbv73ps90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kFTP2/btrKHdgOrUk/lLTQBGoN0xjUgJbv73ps90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkFTP2%2FbtrKHdgOrUk%2FlLTQBGoN0xjUgJbv73ps90%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;520&quot; height=&quot;388&quot; data-origin-width=&quot;774&quot; data-origin-height=&quot;577&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그 결과 POD 내부에 여러개의 컨테이너가 생성되었을 때 내부적으로는 loopback 인터페이스를 통해 컨테이너간에 localhost로 통신이 가능합니다. 따라서 위 ISTIO_OUTPUT 규칙에서 Out Interface가 loopback인 것은 envoy proxy와 Application 통신을 위해 localhost를 사용함을 알 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2 127.0.0.6&amp;nbsp;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;해당 IP의 의미에 대해 알기 전에 envoy의 cluster 타입 중 하나인 ORIGINAL_DST와 passthroughcluster에 대한 내용 이해가 필요합니다. 따라서 해당 개념 사전학습 이후에 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-2-1 ORIGINAL_DST&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;ORIGINAL_DST는 envoy의 cluster 타입 중 하나로써, 트래픽이 들어왔을 때 목적지 주소가 envoy에 등록된 Cluster와 Endpoint 중 일치하는 것이 없을 경우에 해당 목적지 주소를 다시 매핑해서 Forwarding할 수 있도록 지원하는 기능입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이를 위해서 &lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;envoy 공식 문서&lt;/span&gt;&lt;/u&gt;에 따르면 2가지 컴포넌트가 상호 작용하는 것을 알 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;b&gt;1. Original Destination Listener Filter&lt;/b&gt;&lt;br&gt;&lt;b&gt;2. Original Destination Cluster&lt;/b&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WkkZx/btrKZ08xa37/wByByloSNdQAtbQIiAvQ91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WkkZx/btrKZ08xa37/wByByloSNdQAtbQIiAvQ91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WkkZx/btrKZ08xa37/wByByloSNdQAtbQIiAvQ91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWkkZx%2FbtrKZ08xa37%2FwByByloSNdQAtbQIiAvQ91%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;1899&quot; height=&quot;558&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Original Destination Listener Filter는 &lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;envoy 아키텍처 포스팅&lt;/span&gt;&lt;/u&gt;에서 다룬 내용으로 Listener Filters에 해당합니다. 해당 Filter는 &lt;b&gt;SO_ORIGINAL_DST&lt;/b&gt; 소켓 정보를 읽어서 iptables에 의해 목적지 주소가 바뀌기 이전 사용자의 원래 목적지 정보를 읽는 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;842&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKT18J/btrK2ongOXG/0nNN14kKJEbBQ2TP4S94m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKT18J/btrK2ongOXG/0nNN14kKJEbBQ2TP4S94m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKT18J/btrK2ongOXG/0nNN14kKJEbBQ2TP4S94m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKT18J%2FbtrK2ongOXG%2F0nNN14kKJEbBQ2TP4S94m0%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;1802&quot; height=&quot;842&quot; data-origin-width=&quot;1802&quot; data-origin-height=&quot;842&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;iptables를 지나게되면 envoy proxy로 트래픽이 전달되어야 되기 때문에 목적지 주소가 바뀌게 되는데, Original Destination Filter를 통과하면서 redirect된 소켓의 주소 정보를 해당 Filter에서 읽습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1477&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J93ug/btrKZ7mfzC2/EWYXUCs67cKnfkVKVIPd9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J93ug/btrKZ7mfzC2/EWYXUCs67cKnfkVKVIPd9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J93ug/btrKZ7mfzC2/EWYXUCs67cKnfkVKVIPd9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ93ug%2FbtrKZ7mfzC2%2FEWYXUCs67cKnfkVKVIPd9k%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;1477&quot; height=&quot;488&quot; data-origin-width=&quot;1477&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이후 해당 정보는 Listener를 지나 Original Destination Cluster에 전해지는데, 이때 해당 트래픽의 목적지 주소를 iptables를 지나기 이전 목적지 주소로 재설정하는데 사용됩니다. 이를 통해 upstream address에 대해서 동적으로 런타임에 주소 변경이 가능한 것이 특징입니다. 그외 Original Destination Cluster에 대한 부가적인 설명은 &lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;envoy 공식 문서&lt;/span&gt;&lt;/u&gt;를 통해 참고 바랍니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-2-2 PassthroughCluster&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;796&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRtzqz/btrKZoaVcl0/TyPnVGCswZMheYGmEJcchk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRtzqz/btrKZoaVcl0/TyPnVGCswZMheYGmEJcchk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRtzqz/btrKZoaVcl0/TyPnVGCswZMheYGmEJcchk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRtzqz%2FbtrKZoaVcl0%2FTyPnVGCswZMheYGmEJcchk%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;1212&quot; height=&quot;796&quot; data-origin-width=&quot;1212&quot; data-origin-height=&quot;796&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;istio의 Passthrough는 서비스 메시 바깥으로 나가는 egress 트래픽에 대해서 ALLOW_ANY(default)로 지정된 경우 Service Entry로 등록하지 않은 트래픽을 envoy proxy를 통해 외부로 전달하기 위해 사용됩니다. 이를 위해서 PassthroughCluster라는 Virtual Cluster를 envoy 내부에 생성하고 envoy가 보유한 cluster 혹은 endpoint와 매치되지 않는 목적지 트래픽에 대해 해당 Cluster로 트래픽을 전달하고 후속 작업을 처리합니다. 이때 해당 PassthroughCluster의 타입은 이전에 설명한 &lt;b&gt;ORIGINAL_DST&lt;/b&gt;로 지정되어 있어 목적지의 IP를 유지한채로 외부에 전달할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;지금까지 ORIGINAL_DST와 PassthroughCluster에 대해서 설명했습니다. 그렇다면, 이제 127.0.0.6 IP가 왜 등장했는지에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1477&quot; data-origin-height=&quot;488&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LPxS0/btrKVw8Lhew/DaIcCB2daarkvfP8PpKvNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LPxS0/btrKVw8Lhew/DaIcCB2daarkvfP8PpKvNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LPxS0/btrKVw8Lhew/DaIcCB2daarkvfP8PpKvNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLPxS0%2FbtrKVw8Lhew%2FDaIcCB2daarkvfP8PpKvNk%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;1477&quot; height=&quot;488&quot; data-origin-width=&quot;1477&quot; data-origin-height=&quot;488&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;14443 github 이슈&lt;/span&gt;&lt;/u&gt;를 살펴보면, 이전에는 Service로 등록하지 않은 Pod의 port를 외부에서 호출했을 경우 iptables과 envoy간에 무한 루프로 인하여 트래픽이 정상적으로 전달되지 못하고 계속 반복되는 현상이 있었습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두 번째 문제로는 Pod 내부 호출을 위해서 Wildcard bind 혹은 localhost를 입력하거나 Kubernetes의 downward API를 사용하여 Pod IP만을 지정하게되면, Port가 명시되어있지 않기 때문에 cluster에서 매칭되는 결과를 찾을 수가 없습니다. 따라서 이 경우에는 InboundPassthroughCluster의 경로를 따르게 되는데, 이러한 이유로 Istio가 적용된 클러스터와 그렇지 않은 클러스터에서 Pod간 통신 할 때 제약사항이 존재하게 되었습니다. 여기서 제약사항에 대한 자세한 내용은 &lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;Inbound Forwarding 문서&lt;/span&gt;&lt;/u&gt;를 참고하시기 바랍니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서, 위 두 가지 문제를 해결하기 위해서 다음과 같은 결정을 하게 됩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cChniU/btrKVUV4dby/jeMqnzT6xKe6V3hHhA36S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cChniU/btrKVUV4dby/jeMqnzT6xKe6V3hHhA36S0/img.png&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;Inbound Forwarding - Google Docs&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cChniU/btrKVUV4dby/jeMqnzT6xKe6V3hHhA36S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcChniU%2FbtrKVUV4dby%2FjeMqnzT6xKe6V3hHhA36S0%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;805&quot; height=&quot;230&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;amp;nbsp;Inbound Forwarding - Google Docs&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;735&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXaBbv/btrKZB2hfIl/OxMZVKoKNk2QSNoCA82jk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXaBbv/btrKZB2hfIl/OxMZVKoKNk2QSNoCA82jk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXaBbv/btrKZB2hfIl/OxMZVKoKNk2QSNoCA82jk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXaBbv%2FbtrKZB2hfIl%2FOxMZVKoKNk2QSNoCA82jk1%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;635&quot; height=&quot;735&quot; data-origin-width=&quot;635&quot; data-origin-height=&quot;735&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;내용을 살펴보면 Inbound Cluster 타입은 ORIGINAL_DST로 변경했으며, UpstreamBindConfig를 통해 upstream 값을 127.0.0.6으로 지정하여, localhost에 위치한 애플리케이션으로 트래픽을 전달할 수 있도록 하였습니다. 이를 통해 iptables과 envoy간의 무한 루프를 탈피하고자 했습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[cla9@DESKTOP-FBK64T0]$ ip route show table local | grep 127.0.0.0/8
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;여기서 127.0.0.6은 127.0.0.0/8 즉 로컬호스트 루프백 주소에 사용하기 위한 수많은 주소 중 하나이며, 컨트리뷰터가 밝힌 수 많은 주소 중 127.0.0.&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;6&lt;/span&gt;&lt;/b&gt;이 채택된 이유는 envoy의 Inbound 트래픽을 전달받는 포트가 1500&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;6&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이기 때문입니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;정리하자면 &lt;/span&gt;127.0.0.6는 envoy proxy에 트래픽이 진입한 이후에 바인딩되는 주소로써, envoy 밖으로 나간 이후 Outbound Handler를 우회하여 Pod의 Application으로 전달될 수 있도록 하는 역할을 수행합니다. 이러한 과정이 없다면, envoy에서 upstream으로 전달을 해야하는데, iptables를 지날 때 다시 envoy로 전달될 수 있기 때문에 해당 magic 주소를 임의로 추가하여 iptables에서 envoy가 아닌 application으로 트래픽을 전달하는데 목적이 있습니다. 해당 주소는 istio 코드에 &lt;b&gt;IboundPassthroughClusterIpv4&lt;/b&gt;로 하드코딩되어있는 값이며, 자세한 내용은 &lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;github 이슈 내용&lt;/span&gt;&lt;/u&gt;을 참고 바랍니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3&amp;nbsp; 1337 UID, GID&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;iptables에서 UID 혹은 GID가 1337로 구분되어 있는 이유는 envoy proxy와 application 트래픽을 구분하기 위한 용도입니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마무리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번 포스팅에서는 사이드카 컨테이너 주입 설정에서 init-container에 주입된 설정에 대해서 살펴봤습니다. init-container에서는 inbound 트래픽을 envoy proxy에게 전달하고 이를 다시 application에게 전달하기 위한 설정과 outbound 트래픽 흐름을 조작하기 위한 iptables 설정에 대해서 살펴봤습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;실제 트래픽이 들어왔을 때 어떤 체인을 통해 어떻게 전달되는지에 대해서는 살펴보지 않았기 때문에 해당 iptables 설정이 어떻게 쓰이는지에 대해서는 아직 잘 모를 수 있습니다. 이는 차후에 몇 가지 사례를 통해서 어떤 체인을 통해 전달되는지 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;다음 포스팅에서는 사이드카 컨테이너인 envoy proxy 설정과 내부 구조에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <category>Envoy</category>
      <category>envoy 구조</category>
      <category>iptables</category>
      <category>Istio</category>
      <category>istio architecture</category>
      <category>istio init container</category>
      <category>istio 구조</category>
      <category>istio 동작과정</category>
      <category>passthroughcluster</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/196</guid>
      <comments>https://cla9.tistory.com/196#entry196comment</comments>
      <pubDate>Tue, 3 Mar 2026 11:50:59 +0900</pubDate>
    </item>
    <item>
      <title>11. [istio-internals] Pilot-discovery 사이드카 컨테이너 주입</title>
      <link>https://cla9.tistory.com/199</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;지금까지 pilot-discovery에 대해서 학습하면서, k8s의 리소스 변화를 감지하고 이를 pilot-agent에게 전달하는 과정에 대해서 살펴봤습니다. 이번에는 애플리케이션 서비스에 pilot-agent를 배포하는 과정을 살펴보며, 이때 pilot-discovery는 어떤 역할을 수행하는지 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. pilot-agent 배포&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ldKql/btrM8htlisa/90rFlVs65t6J6SgK7gvLw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ldKql/btrM8htlisa/90rFlVs65t6J6SgK7gvLw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ldKql/btrM8htlisa/90rFlVs65t6J6SgK7gvLw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FldKql%2FbtrM8htlisa%2F90rFlVs65t6J6SgK7gvLw0%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;1515&quot; height=&quot;810&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;istio를 사용하지 않는 상황에서 단일 서비스에 envoy proxy를 사용한다면,&amp;nbsp; envoy proxy와 service 그리고 proxy와 service 연결을 위한 설정 파일을 한데 묶어 배포하면 되었습니다. 하지만 이러한 방식은 사용자 입장에서 CI/CD 단계에서 service 관리 뿐만 아니라 envoy proxy 설정을 관리해야하는 불편함이 존재했습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1361&quot; data-origin-height=&quot;803&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G8H30/btrM7ol9cZL/r3HOxLvBpyvky68UQqOTw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G8H30/btrM7ol9cZL/r3HOxLvBpyvky68UQqOTw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G8H30/btrM7ol9cZL/r3HOxLvBpyvky68UQqOTw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG8H30%2FbtrM7ol9cZL%2Fr3HOxLvBpyvky68UQqOTw0%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;1361&quot; height=&quot;803&quot; data-origin-width=&quot;1361&quot; data-origin-height=&quot;803&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;더욱이 istio와 같은 control plane이 추가되면, 이제는 control plane과 연결을 위한 속성까지 추가해야하므로 service 관리자의 불편은 더욱 가중됩니다. 만약 배포되는 istio의 설정 정보가 바뀌기라도 한다면, service 관리자는 전체 service에 속한 config 정보를 바꿔야할 지도 모릅니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;istio에서는 이러한 문제를 해결하고자 사용자는 CI/CD를 위해서 개별 Service 설정에만 집중하도록 하고 pilot-agent 추가와 설정 정보 변경은 istio가 대신 해주기 위한 2가지 방법을 제공합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;첫 번째 방법은 수동 방식으로 사용자가 작성한 yaml 파일을 읽어들여 istio 설정이 적용된 형태로 yaml 형태를 변경해주는 스크립트를 제공해주는 방식입니다. 두 번째 방식은 자동으로 Pod 생성 시점에 istio 설정이 적용된 형태로 배포하는 것입니다. 지금부터 이 두 가지 방법에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1 수동 배포(istioctl CLI)&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;첫번째 방법은 istioctl을 통해서 기존에 생성된 pod.yaml을 조작하는 방법입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;nginx.yaml&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
&amp;nbsp;&amp;nbsp;creationTimestamp: null
&amp;nbsp;&amp;nbsp;labels:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;run: nginx
&amp;nbsp;&amp;nbsp;name: nginx
spec:
&amp;nbsp;&amp;nbsp;containers:
&amp;nbsp;&amp;nbsp;- image: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resources: {}
&amp;nbsp;&amp;nbsp;dnsPolicy: ClusterFirst
&amp;nbsp;&amp;nbsp;restartPolicy: Always
status: {}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;가령 위와 같은 Pod를 만들기 위한 yaml 파일을 만들었다고 가정해봅시다. 해당 Pod를 배포할 때 envoy proxy를 사이드카로 배포하기 위해서는 istioctl을 통해 yaml 파일을 조작할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;istioctl kube-inject -f nginx.yaml&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;istioctl에서 제공하는 kube-inject 명령어를 활용하면, 기존에 생성한 pod의 yaml 파일을 읽어서 envoy proxy를 주입한 yaml 파일을 제공합니다. 제공된 결과를 살펴보면 다음과 같습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
&amp;nbsp;&amp;nbsp;annotations:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;kubectl.kubernetes.io/default-container: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;kubectl.kubernetes.io/default-logs-container: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;prometheus.io/path: /stats/prometheus
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;prometheus.io/port: &quot;15020&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;prometheus.io/scrape: &quot;true&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sidecar.istio.io/status: '{&quot;initContainers&quot;:[&quot;istio-init&quot;],&quot;containers&quot;:[&quot;istio-proxy&quot;],&quot;volumes&quot;:[&quot;workload-socket&quot;,&quot;workload-certs&quot;,&quot;istio-envoy&quot;,&quot;istio-data&quot;,&quot;istio-podinfo&quot;,&quot;istio-token&quot;,&quot;istiod-ca-cert&quot;],&quot;imagePullSecrets&quot;:null,&quot;revision&quot;:&quot;default&quot;}'
&amp;nbsp;&amp;nbsp;creationTimestamp: null
&amp;nbsp;&amp;nbsp;labels:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;run: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;security.istio.io/tlsMode: istio
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;service.istio.io/canonical-name: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;service.istio.io/canonical-revision: latest
&amp;nbsp;&amp;nbsp;name: nginx
spec:
&amp;nbsp;&amp;nbsp;containers:
&amp;nbsp;&amp;nbsp;- image: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resources: {}
&amp;nbsp;&amp;nbsp;- args:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- proxy
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- sidecar
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --domain
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- $(POD_NAMESPACE).svc.cluster.local
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --proxyLogLevel=warning
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --proxyComponentLogLevel=misc:error
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --log_output_level=default:info
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- --concurrency
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;2&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;env:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: JWT_POLICY
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: third-party-jwt
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: PILOT_CERT_PROVIDER
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: istiod
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: CA_ADDR
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: istiod.istio-system.svc:15012
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: POD_NAME
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueFrom:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: metadata.name
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: POD_NAMESPACE
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueFrom:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: metadata.namespace
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: INSTANCE_IP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueFrom:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: status.podIP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: SERVICE_ACCOUNT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueFrom:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: spec.serviceAccountName
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: HOST_IP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;valueFrom:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: status.hostIP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: PROXY_CONFIG
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: |
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;{&quot;envoyAccessLogService&quot;:{&quot;address&quot;:&quot;skywalking-oap.skywalking:11800&quot;}}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_POD_PORTS
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: |-
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;]
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_APP_CONTAINERS
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_CLUSTER_ID
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: Kubernetes
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_INTERCEPTION_MODE
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: REDIRECT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_WORKLOAD_NAME
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_OWNER
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: kubernetes://apis/v1/namespaces/default/pods/nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: ISTIO_META_MESH_ID
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: cluster.local
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- name: TRUST_DOMAIN
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;value: cluster.local
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;image: docker.io/istio/proxyv2:1.14.2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-proxy
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ports:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- containerPort: 15090
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: http-envoy-prom
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;protocol: TCP
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;readinessProbe:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;failureThreshold: 30
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;httpGet:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;path: /healthz/ready
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;port: 15021
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;initialDelaySeconds: 1
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;periodSeconds: 2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;timeoutSeconds: 3
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resources:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;limits:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cpu: &quot;2&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;memory: 1Gi
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;requests:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cpu: 10m
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;memory: 40Mi
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;securityContext:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;allowPrivilegeEscalation: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;capabilities:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;drop:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- ALL
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;privileged: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;readOnlyRootFilesystem: true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsGroup: 1337
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsNonRoot: true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsUser: 1337
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;volumeMounts:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- mountPath: /var/run/secrets/workload-spiffe-uds
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: workload-socket
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- mountPath: /var/run/secrets/workload-spiffe-credentials
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: workload-certs
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- mountPath: /var/run/secrets/istio
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istiod-ca-cert
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- mountPath: /var/lib/istio/data
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-data
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- mountPath: /etc/istio/proxy
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-envoy
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- mountPath: /var/run/secrets/tokens
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-token
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- mountPath: /etc/istio/pod
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-podinfo
&amp;nbsp;&amp;nbsp;dnsPolicy: ClusterFirst
&amp;nbsp;&amp;nbsp;initContainers:
&amp;nbsp;&amp;nbsp;- args:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- istio-iptables
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -p
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;15001&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -z
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;15006&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -u
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;1337&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -m
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- REDIRECT
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -i
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- '*'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -x
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -b
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- '*'
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- -d
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 15090,15021,15020
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;image: docker.io/istio/proxyv2:1.14.2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-init
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resources:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;limits:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cpu: &quot;2&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;memory: 1Gi
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;requests:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cpu: 10m
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;memory: 40Mi
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;securityContext:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;allowPrivilegeEscalation: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;capabilities:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;add:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- NET_ADMIN
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- NET_RAW
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;drop:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- ALL
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;privileged: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;readOnlyRootFilesystem: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsGroup: 0
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsNonRoot: false
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;runAsUser: 0
&amp;nbsp;&amp;nbsp;restartPolicy: Always
&amp;nbsp;&amp;nbsp;volumes:
&amp;nbsp;&amp;nbsp;- name: workload-socket
&amp;nbsp;&amp;nbsp;- name: workload-certs
&amp;nbsp;&amp;nbsp;- emptyDir:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;medium: Memory
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-envoy
&amp;nbsp;&amp;nbsp;- emptyDir: {}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-data
&amp;nbsp;&amp;nbsp;- downwardAPI:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;items:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: metadata.labels
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;path: labels
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- fieldRef:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;fieldPath: metadata.annotations
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;path: annotations
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-podinfo
&amp;nbsp;&amp;nbsp;- name: istio-token
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;projected:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sources:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- serviceAccountToken:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;audience: istio-ca
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;expirationSeconds: 43200
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;path: istio-token
&amp;nbsp;&amp;nbsp;- configMap:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istio-ca-root-cert
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istiod-ca-cert
status: {}
---&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 출력 결과물을 보면, 이전에 작성한 Pod.yaml 과 비교해서 꽤나 복잡해진 것을 확인할 수 있습니다. 이는 istiod와 통신을 위한 iptables 변경과 pilot-agent 주입을 위한 기본 설정이 포함되었기 때문입니다. 만약 사용자가 직접 이를 작성해야한다면 꽤나 불편했을텐데, istioctl을 통해서 위와같이 변경된 yaml 파일을 얻을 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;변경된 yaml 파일을 토대로 kubernetes에 배포하면 istio control plane과 통신할 수 있는 envoy proxy가 내장된 Pod를 사용할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2 자동 배포&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번에는 자동 배포에 대해서 알아보겠습니다. 자동 배포에는 두 가지 방법이 있습니다. 첫번째 방법은 Pod.yaml에 istio에서 요구하는 label을 추가하는 것입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apiVersion: v1
kind: Pod
metadata:
&amp;nbsp;&amp;nbsp;creationTimestamp: null
&amp;nbsp;&amp;nbsp;labels:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;run: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sidecar.istio.io/inject: &quot;true&quot;
&amp;nbsp;&amp;nbsp;name: nginx
spec:
&amp;nbsp;&amp;nbsp;containers:
&amp;nbsp;&amp;nbsp;- image: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: nginx
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resources: {}
&amp;nbsp;&amp;nbsp;dnsPolicy: ClusterFirst
&amp;nbsp;&amp;nbsp;restartPolicy: Always
status: {}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전에 살펴본 nginx pod yaml 파일에&amp;nbsp;&lt;span style=&quot;color: #EE2323;&quot;&gt;sidecar.istio.io/inject: &quot;true&quot;&lt;/span&gt;&amp;nbsp;label을 추가하면, Pod 생성 시점에 envoy proxy가 내장된 형태로 Pod yaml이 변경되고 배포가 이루어집니다. 참고로 해당 annotation의 값은 true 이외에도 y, yes, on 중 하나가 입력되면 사이드카 배포가 이루어집니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두번째 방법은 배포하려는 namespace에 label을 추가하는 것입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[cla9@DESKTOP-FBK64T0]$ kubectl label ns default istio-injection=enabled
[cla9@DESKTOP-FBK64T0]$ kubectl get ns default -L istio-injection
NAME&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;STATUS&amp;nbsp;&amp;nbsp; AGE&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ISTIO-INJECTION
default&amp;nbsp;&amp;nbsp; Active&amp;nbsp;&amp;nbsp; 334d&amp;nbsp;&amp;nbsp; enabled&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위와 같이 Pod를 배포하려는 namespace에&amp;nbsp;&lt;span style=&quot;color: #EE2323;&quot;&gt;istio-injection=enabled&lt;/span&gt;&amp;nbsp;label을 추가하면, 이후 해당 namespace에 배포되는 Pod에는 모두 envoy proxy가 내장된 형태로 배포가 이루어집니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;지금까지 envoy proxy를 배포하기 위한 수동 배포와 자동 배포에 대해서 살펴봤습니다. 여기서 자동 배포의 경우 Pod 혹은 namespace에 label을 추가하는 것만으로 Pod 생성시에 자동으로 pilot-agent가 배포되는 것을 확인했는데요. 해당 작업은 어떻게 이루어지는 것일까요?&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Mutation Webhook&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;사이드카 컨테이너의 자동 주입과정에 대해서 이해하려면, 먼저 kubernetes의 API Server를 활용한 배포 과정에서 어떠한 일이 일어나는지에 대해서 알아야합니다. 따라서 istio의 자동 배포 프로세스 과정을 살펴보기 전에 kubernetes의 Resource 생성 과정에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&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;292&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wi4pV/btrM7L2vR27/uusygSQdwPoQDkOgG6dCak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wi4pV/btrM7L2vR27/uusygSQdwPoQDkOgG6dCak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wi4pV/btrM7L2vR27/uusygSQdwPoQDkOgG6dCak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwi4pV%2FbtrM7L2vR27%2FuusygSQdwPoQDkOgG6dCak%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;292&quot; data-origin-width=&quot;1452&quot; data-origin-height=&quot;292&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;사용자가 Kubectl을 통해서 Pod를 생성하였을 때, 우리는 일반적으로 위와같이 Kubectl 명령어를 통해서 Kube ApiServer에 전달하면, Pod가 생성된다고 생각합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;449&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tJCwU/btrM29jv0dD/uzpaGHWgTuOiAhGItofH6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tJCwU/btrM29jv0dD/uzpaGHWgTuOiAhGItofH6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tJCwU/btrM29jv0dD/uzpaGHWgTuOiAhGItofH6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtJCwU%2FbtrM29jv0dD%2FuzpaGHWgTuOiAhGItofH6K%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;1674&quot; height=&quot;449&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;449&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이때 내부 과정을 조금 더 자세히 살펴보면, 실제로는 위 그림과 같은 단계를 거쳐 Pod가 생성됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;1. kubectl로 명령어를 Kube ApiServer로 전달할 때 명령어를 실행하는 컴퓨터에 존재하는 config파일 정보를 같이 전달합니다. 해당 config 파일에는 인증서 정보와 사용자 정보가 같이 포함되어있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;2. Kube ApiServer는 제일 첫번째로 인증(Authentication) 과정을 수행합니다. 해당 과정은 사용자가 전달한 인증서 정보를 토대로 해당 서버에 접속 가능한 요청인지를 분석하고 승인하는 과정입니다. 마치 ID/Password를 입력했을 때, 유효한 계정인지를 검증하는 것과 같습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;3. 두번째로는 인가(Authorization) 과정을 수행합니다. 해당 과정은 접속이 완료된 이후에 해당 사용자가 요청한 작업에 대해서 수행 권한이 있는지를 분석하고 승인하는 과정입니다. 만약 Pod 생성을 요청했는데, 해당 권한이 존재하지 않는다면 이 단계에서 실패합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;4. 마지막 단계에서는 Admission Controllers 체인을 거치면서 사용자가 전달한 yaml 파일내에서 validation을 진행하거나 부가적인 내용을 추가하거나 변경하는 등의 기능을 제공합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;5. 모든 단계가 완료되면 Pod가 생성됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;여기서 주목할 부분은 Admission Controllers입니다. Admission Controller는 이전에 설명했듯이 인증과 인가외에 검증(Validation)이나 내용을 변경(Mutation)하기 위해 사용됩니다. 또한 Admission Controller는 하나만 존재하는 것이 아니라&lt;br&gt;여러 Admission Controller가 Chain 형태로 엮여있고 해당 Chain을 순회하면서 validation 혹은 mutation을 진행합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pQQNs/btrM6bnBCtI/SmGLRt7Anc5GT9kCxQbP30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pQQNs/btrM6bnBCtI/SmGLRt7Anc5GT9kCxQbP30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pQQNs/btrM6bnBCtI/SmGLRt7Anc5GT9kCxQbP30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpQQNs%2FbtrM6bnBCtI%2FSmGLRt7Anc5GT9kCxQbP30%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;1674&quot; height=&quot;812&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;812&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이쯤되면 istio가 어떻게 자동으로 Pod에 사이드카 컨테이너를 주입시킬 수 있는지 눈치를 챌 수 있는데요. Admission controller 중 하나인 MutatingAdmission Webhook을 활용하여 사용자의 요청을 변경시킵니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[cla9@DESKTOP-FBK64T0]$ kubectl get mutatingwebhookconfigurations
NAME&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; WEBHOOKS&amp;nbsp;&amp;nbsp; AGE
istio-revision-tag-default&amp;nbsp;&amp;nbsp; 4&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;2d
istio-sidecar-injector&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 4&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;26d&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이를 위해서 kubernetes 상에 위와 같이 찾아보면, istio에서 사이드카를 주입하기 위한 MutatingWebhookConfiguration이 존재하는 것을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
&amp;nbsp;&amp;nbsp;labels:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;app: sidecar-injector
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;install.operator.istio.io/owning-resource: unknown
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;install.operator.istio.io/owning-resource-namespace: istio-system
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;istio.io/rev: default
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;operator.istio.io/component: Pilot
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;operator.istio.io/managed: Reconcile
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;operator.istio.io/version: 1.12.2
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;release: istio
&amp;nbsp;&amp;nbsp;name: istio-sidecar-injector
webhooks:
- admissionReviewVersions:
&amp;nbsp;&amp;nbsp;- v1beta1
&amp;nbsp;&amp;nbsp;- v1
&amp;nbsp;&amp;nbsp;clientConfig:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvRENDQWVTZ0F3SUJBZ0lRZWhpd2hmTTFLaDNTUGUvVC9qZ29tVEFOQmdrcWhraUc5dzBCQVFzRkFEQVkKTVJZd0ZBWURWUVFLRXcxamJIVnpkR1Z5TG14dlkyRnNNQjRYRFRJeU1Ea3hPREF6TURreE5sb1hEVE15TURreApOVEF6TURreE5sb3dHREVXTUJRR0ExVUVDaE1OWTJ4MWMzUmxjaTVzYjJOaGJEQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFNaXliT3A5SVhpZEpGbU5yUGlVbFVMeVN0RVoyU1BZV3ZVdWlNUWwKRFo2NG44cEI2TmVIL2ZGMFB0eXJneTlxemhtNkZSOCtBTkpFTXBWSDlmTWlHcHpwMVNQeVQ1RXVyaHYwdGVpWQp3ZzRKa2RINEQ2bm03ZUJCTkxkdkszMzFqR0o1RU5WZjlOTVIxR05wdUZZMXgyT045NExkUGxjWHhMOHlFd2NHClVOY2k3aFlyd1JuSHl0VTlFSjVNSGZCKzIydW5KR3ZuVzI1L2RNTllBN0ZMVkd2VHdST0d1RkZzR29UejRQNkMKYUI5bGd1MFZwbkVDb3B1bmJZLys2QW5NMHN5UHJTdjZQVmkvcUVjdzV1cmlYM1BiNFhxSzRkTDI0Vk1aVGNTSgpUQkUwTUpLazEwQjFMNlNqa0JZOGRNeDBCblR5VEJMUnJ6RFNuQlY4cjRiRldsa0NBd0VBQWFOQ01FQXdEZ1lEClZSMFBBUUgvQkFRREFnSUVNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdIUVlEVlIwT0JCWUVGTDk2eG1pUmRTbGgKVlNFZUtEbXhvdVhYajJnNk1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ2pwYTJ5N2U3SkVNQlplVTRxRHFVUwp6aG40cFVpSE1XelJSLzRTU0NwRnZQVzVwRkRLd21ZY2NwUFp2MllCLzg3L2JKU2ZFakk5N0Ewa1NoN2J3aHRPCmNTUm5RdHBmdkp2dlQ2U1BQRGJzWEg1NnVwWXd6Z1dPVElPOEY5b3p1RmpoMHFXQ0VuTFcvcHN5ZHNTbWlyNVMKNHppclVXazVGZjhYTVdMWllYS29wYnpqK0VvNUd2TVVSRDg5cHY3Yng3dE1sOGxGU0MwZkkwVGVLc3d6OUJsbwpPVXExZkp0bStJaDFPdU9aQTFOZGZmYmNzOTZCOGdFSVM2bVFEUjBUVmVEayt4NWk0Zk8xSG56SnZMbEJhVmtpClJneGxiUExFei81VlY1RHBoMzRpTTBPcVZrampiT05RNUlBd211YUZ5RC9jNmJvN2tqcEMyWFpUNndybDI5SXkKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;service:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;name: istiod
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;namespace: istio-system
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;path: /inject
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;port: 443
&amp;nbsp;&amp;nbsp;failurePolicy: Fail
&amp;nbsp;&amp;nbsp;matchPolicy: Equivalent
&amp;nbsp;&amp;nbsp;name: namespace.sidecar-injector.istio.io
&amp;nbsp;&amp;nbsp;namespaceSelector:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;matchExpressions:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- key: istio-injection
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;operator: In
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;values:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- enabled
&amp;nbsp;&amp;nbsp;objectSelector:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;matchExpressions:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- key: sidecar.istio.io/inject
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;operator: NotIn
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;values:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;false&quot;
&amp;nbsp;&amp;nbsp;reinvocationPolicy: Never
&amp;nbsp;&amp;nbsp;rules:
&amp;nbsp;&amp;nbsp;- apiGroups:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- &quot;&quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;apiVersions:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- v1
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;operations:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- CREATE
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;resources:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;- pods
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;scope: '*'
&amp;nbsp;&amp;nbsp;sideEffects: None
&amp;nbsp;&amp;nbsp;timeoutSeconds: 10&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;해당 설정을 조금 더 자세히 살펴보겠습니다. 위 코드는 istio-sidecar-injector ConfigMap 일부를 발췌한 부분으로 먼저 살펴볼 지점은 rules 부분에 Pod를 생성할 때 해당 Webhook이 동작하는 것을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;또한 namespaceSelector와 objectSelector를 통해서 특정 label의 값이 존재할 경우에만 동작하도록 되어있는 것을 확인할 수 있습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;마지막으로 살펴볼 지점은 조건들이 부합되었을 때, istiod의 /inject URL로 요청을 전달하여 후속 작업 처리를 요청한다는 점입니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;812&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zKxAQ/btrM7jd6dUQ/P9IPHyPGKj8g1bCmxG8tvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zKxAQ/btrM7jd6dUQ/P9IPHyPGKj8g1bCmxG8tvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zKxAQ/btrM7jd6dUQ/P9IPHyPGKj8g1bCmxG8tvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzKxAQ%2FbtrM7jd6dUQ%2FP9IPHyPGKj8g1bCmxG8tvK%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;1674&quot; height=&quot;812&quot; data-origin-width=&quot;1674&quot; data-origin-height=&quot;812&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이를 토대로 살펴보면, Kubernetes는 사용자가 요청한 내용을 분석하여 istio를 통한 Mutation 변경이 필요하다고 판단될 때 istiod에게 요청하여 mutation 작업을 수행하도록 요청합니다. 그 이후에는 istiod 내부에서 istio 설정에 맞추어 yaml을 변경한 다음 이를 다시 Kubernetes로 전달하여 후속 작업 진행 후 배포하는 과정을 거쳐 Pod가 생성됩니다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Sidecar Injector&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전 내용을 통해 Pod가 생성되는 시점에 istio pilot-discovery에 요청하여 istio 연결에 필요한 설정이 적용되는 것을 확인할 수 있었습니다. 그렇다면 istio 내부에 어떤 컴포넌트가 어떤 과정을 거쳐 이러한 과정이 수행될까요? pilot-discovery에 위치한 Sidecar Injector 컴포넌트를 살펴보면서,&amp;nbsp;사이드카 컨테이너를 어떻게 주입하는지 과정을 살펴보겠습니다. 먼저 살펴볼 것은 Sidecar Injector의 기동 과정입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;처음에 pilot-discovery가 기동될 당시에&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt; INJECT_ENABLED &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;환경 변수 값을 확인합니다. 해당 값이 true이면, Sidecar Injector의 초기화 과정을 수행합니다. 참고로 해당 환경 변수의 기본 값은 true입니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;초기화 과정을 살펴보면 다음과 같습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;525&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DnsKx/btrM7LVGTBn/ycnklu3ZwxAHeFf3cXdZr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DnsKx/btrM7LVGTBn/ycnklu3ZwxAHeFf3cXdZr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DnsKx/btrM7LVGTBn/ycnklu3ZwxAHeFf3cXdZr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDnsKx%2FbtrM7LVGTBn%2Fycnklu3ZwxAHeFf3cXdZr0%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;178&quot; data-origin-width=&quot;525&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;먼저 pilot-discovery가 실행되는 Pod 내의 /var/lib/istio/inject 하위 디렉토리에 config 파일이 존재하는지를 살펴봅니다. 만약 해당 디렉토리에 config 파일이 위치한다면, config와 values를 FileWatcher에 매핑시킵니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약 해당 디렉토리에 파일이 위치하지 않으면, 그 다음으로 확인하는 것은 k8s의 Configmap을 확인하고 관련 ConfigMapWatcher 컴포넌트를 생성하여 바인딩하는 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1551&quot; data-origin-height=&quot;1048&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buFHoW/btrM7M78dcq/sf3LlZtlkCXrEkWbb6D7tK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buFHoW/btrM7M78dcq/sf3LlZtlkCXrEkWbb6D7tK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buFHoW/btrM7M78dcq/sf3LlZtlkCXrEkWbb6D7tK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuFHoW%2FbtrM7M78dcq%2Fsf3LlZtlkCXrEkWbb6D7tK%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;1551&quot; height=&quot;1048&quot; data-origin-width=&quot;1551&quot; data-origin-height=&quot;1048&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;먼저 살펴볼 것은 ConfigMap Watcher 내부입니다. 내부에는 Controller가 존재하고 ConfigMap 이벤트를 전달받기 위해서 informer를 등록하고 변경 사항을 전달 받습니다. 이때 ConfigMap Watcher가 감시하는 대상은 istio-system 네임스페이스에 존재하는 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;istio-sidecar-injector&lt;/span&gt;&lt;/b&gt;입니다. 참고로 해당 ConfigMap은 istio 설치 시에 자동 등록되는 리소스로 내부에는 config와 values 두 Key에 해당하는 데이터가 존재합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;informer에 등록된 이후 해당 Configmap에 변경이 발생하면, 다음과 같은 과정을 거칩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;1. informer에서 queue에 이벤트 정보를 전달합니다.&lt;br&gt;2. queue에는 등록된 callback을 호출합니다.&amp;nbsp;&lt;br&gt;3. Controller에 등록된 callback의 역할은 Configmap 정보를 읽고 거기에서 등록된 config 데이터와 values 두 개의 데이터를 읽어들이는 작업을 수행하고, ConfigMap Watcher에 등록된 handler에 전달하여 후속 작업을 위임합니다.&lt;br&gt;4. handler의 역할은 Webhook 구조체에 위치한 updateConfig 메소드를 호출하는 것입니다.&amp;nbsp;&lt;br&gt;5. updateConfig 메소드의 역할은 ConfigMap을 읽어들여 Webhook 구조체 멤버에 저장합니다. 이때 config의 경우는 configMap의 config key의 값을 그대로 저장하지만, values의 경우는 해당 데이터를 map으로 변환 후 ValuesConfig 구조체로 감싸서 Webhook의 valuesconfig에 저장하는 차이점이 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;586&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qCDB3/btrNyEnQKSX/pZcxMH5EBRFAx6Z2AlNbyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qCDB3/btrNyEnQKSX/pZcxMH5EBRFAx6Z2AlNbyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qCDB3/btrNyEnQKSX/pZcxMH5EBRFAx6Z2AlNbyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqCDB3%2FbtrNyEnQKSX%2FpZcxMH5EBRFAx6Z2AlNbyK%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;440&quot; height=&quot;781&quot; data-origin-width=&quot;330&quot; data-origin-height=&quot;586&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 내용을 보면, istio-sidecar-injector ConfigMap의 values 데이터를 Map 형태로 Parsing하여 저장하는 것을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;5번 단계까지 진행되면, pilot-discovery 내부에 사이드카 주입을 위한 템플릿 정보를 취득할 수 있으며, ConfigMap 변경에 따라서 이를 감지하고 반영할 수 있게 되었습니다. 이후 진행되는 6번 단계에서는 외부에서 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;/inject&lt;/span&gt;&lt;/b&gt; 혹은 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;/inject/&lt;/span&gt; &lt;/b&gt;URI를 통해서 접근할 경우 요청을 처리하기 위한 handler 메소드를 등록하는데, 이때 담당을 webhook에 위치한 serverInject 메소드가 담당합니다. 즉 사이드카 컨테이너 주입 역할은 serverInject 메소드에 있습니다. 지금부터 해당 메소드에서 수행되는 주요 과정에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1 사이드카 주입 가능 여부 확인&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;1797&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NNsMM/btrNwnn3qna/cJRse3vsNPpqqRP6pyDib1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NNsMM/btrNwnn3qna/cJRse3vsNPpqqRP6pyDib1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NNsMM/btrNwnn3qna/cJRse3vsNPpqqRP6pyDib1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNNsMM%2FbtrNwnn3qna%2FcJRse3vsNPpqqRP6pyDib1%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;2042&quot; height=&quot;1797&quot; data-origin-width=&quot;2042&quot; data-origin-height=&quot;1797&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;serveInject 메소드내에서 가장 먼저 수행하는 작업은 위 그림과 같습니다. 먼저 Client의 요청 타입은 JSON으로 전달되기 때문에 해당 타입이 JSON인지 확인하고 Body에서 데이터를 추출합니다. 그 후 수행하는 작업은 해당 Pod의 사이드카 주입 요청이 적절한지를 확인하는 것입니다. 해당 과정은 위 그림과 같은 과정을 거칩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;1. Pod의 spec.hostNetwork 값이 true인가?&lt;br&gt;2. 사이드카 생성 요청 대상 Namespace가 Ignored Namespace에 해당하는가?&lt;br&gt;3. sidecar.istio.io/inject 어노테이션이 존재하거나 대상 Namespace에 자동 주입 활성화 Label이 존재하는가?&lt;br&gt;&amp;nbsp;&lt;br&gt;먼저 첫번째 조건에 대해서 살펴보면, hostNetwork 값이 true라면, 이는 Pod의 네트워크 설정이 해당 Pod가 동작하는 host 노드의 네트워크 설정을 따라가는 것을 의미합니다. istio에서는 Pod 컨테이너의 iptables를 조작하여 네트워크 트래픽을 변경하는데, 해당 설정이 true일 경우 노드 전체에 장애가 발생할 여지가 있습니다. 따라서 Pod의 hostNetwork가 true인 경우에는 사이드카 주입을 수행하지 않습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두 번째 조건은 사이드카 요청 대상 Namespace가 시스템에서 내부적으로 사용하는 Namespace인 경우에는 요청을 거절합니다. 대상 Namespace는 위 그림과 같이 총 4개입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;세 번째 조건은 Pod 요청에 사이드카 주입 어노테이션이 존재하는지 확인하는 것입니다. 해당 어노테이션이 존재하면서 값 주입을 요청하거나 생성 대상 Namespace에 사이드카 자동 주입 Label이 활성화되어있는 경우에는 사이드카 주입을 허가하지만 그렇지 않을 경우에는 주입을 수행하지 않습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2 Pod Annotation 검증&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번에는 Pod 내에 존재하는 Annotation 중 istio 동작에 관여하는 Annotation이 지정되어있을 경우 해당 Annotation이 istio에서 요구하는 형태로 작성되어있는지 검증하는 단계입니다. 이전 단계에서는 특정 조건에 부합되지 않은 경우에는 사이드카 주입은 수행하지 않더라도 Pod 생성은 진행되었지만, 해당 단계에서는 Validation을 통과하지 않은 경우에 Pod 생성이 되지 않는 것이 차이점입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;965&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tnAvx/btrNB4UO9cj/INnZOOcSfOSgXsP2ffGSG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tnAvx/btrNB4UO9cj/INnZOOcSfOSgXsP2ffGSG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tnAvx/btrNB4UO9cj/INnZOOcSfOSgXsP2ffGSG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtnAvx%2FbtrNB4UO9cj%2FINnZOOcSfOSgXsP2ffGSG1%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;965&quot; height=&quot;466&quot; data-origin-width=&quot;965&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위와같이 pilot-discovery 내에는 사용자가 Pod에 입력한 Annotation의 이름이 위 Map에서 제공하는 Annotation과 일치할 경우 입력값이 올바른지 검증하는 함수가 매핑되어있는 것을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서 Pod 내부 Annotation을 순회하면서 위 조건에 부합하는 Key Annotation이 존재할 경우 해당 값에 매핑된 검증 함수를 수행하여 올바른 값이 입력되지 않았을 경우 Pod 생성 요청을 거절합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3 Concurrency 계산&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;envoy 구조 포스팅&lt;/span&gt;&lt;/u&gt;에서 다루어봤듯이 envoy 내부에는 Master 쓰레드와 Worker 쓰레드가 분리되어 있고 해당 쓰레드 개수의 설정은 --concurency 인자에 의해 결정되는 것을 확인했습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;istio에서 사이드카를 주입할 때 내부적으로 envoy proxy를 기동시켜야되기 때문에 해당 과정에서는 몇 개의 Worker 쓰레드를 기동시키는 지 계산하여 사이드카 컨테이너 템플릿을 생성할 때 이 과정에서 계산된 값이 주입됩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1713&quot; data-origin-height=&quot;948&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/erqPt8/btrNCfPw3vE/hiorNQsc0nKON4SjgaIYhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/erqPt8/btrNCfPw3vE/hiorNQsc0nKON4SjgaIYhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/erqPt8/btrNCfPw3vE/hiorNQsc0nKON4SjgaIYhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FerqPt8%2FbtrNCfPw3vE%2FhiorNQsc0nKON4SjgaIYhk%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;1713&quot; height=&quot;948&quot; data-origin-width=&quot;1713&quot; data-origin-height=&quot;948&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이때 Concurrency가 계산되는 과정을 살펴보면, 위 그림과 같이 Proxy Config 설정 여부에 따라 달라집니다. Proxy Config는 istio 전체에 전역적으로 설정하거나 특정 Workload에만 적용하거나 아니면 특정 Pod에 대해서 Annotation에서 설정 변경이 가능합니다. 이때 우선순위는 Pod &amp;gt; Workload &amp;gt; Global 순이기 때문에 우선순위에 따라 값이 Override 됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약 Concurrency 값이 양수 값이 입력되어있다면, 계산 과정에서는 해당 값이 적용됩니다. 하지만 Concurrency 값이 0일 경우에는 계산 과정이 달라집니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;1498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccaugq/btrNyDDA3n6/obKU8UUmWYc1YwTL78g5d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccaugq/btrNyDDA3n6/obKU8UUmWYc1YwTL78g5d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccaugq/btrNyDDA3n6/obKU8UUmWYc1YwTL78g5d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fccaugq%2FbtrNyDDA3n6%2FobKU8UUmWYc1YwTL78g5d0%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;1768&quot; height=&quot;1498&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;1498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;가장 먼저 확인하는 것은 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;sidecar.istio.io/proxyCPULimit&lt;/span&gt;&lt;/b&gt; Annotation 값이 존재하는지 확인하는 것입니다. 해당 Annotation은 Envoy를 위해 사용되는 CPU의 Limit을 지정하는 값으로 해당 Annotation이 존재한다면, 그 값을 기준으로 Concurrency 값을 계산합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약 해당 Annotation이 존재하지 않는다면, &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;sidecar.istio.io/proxyCPU &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;값이 존재하는지 확인합니다. 해당 Annotation은 Envoy를 위해 사용되는 CPU를 의미합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 위 두 Annotation이 존재하지 않는다면, 해당 Pod에 지정되어있는 CPU Resource의 Request와 Limit을 확인합니다. 만약 지정된 값이 있으면, 그 값을 기준으로 Concurrency를 계산합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막으로 어떠한 조건에도 부합하지 않는다면, 기본 값인 2를 지정합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고로 Concurrency 계산에 필요한 값이 존재할 경우에는 입력된 값을 1000으로 나눈 값을 올림하여 요구 Concurrency를 계산합니다. 가령 500m이 입력되었다면, CEIL(500/1000) = 1이므로 1개가 지정됩니다.&lt;/span&gt;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4 Template Yaml 생성&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;istio-system namespace에 존재하는 istio-sidecar-injector Configmap을 살펴보면, 사이드카 주입을 위한 Template이 Yaml 형식으로 정의된 것을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;args:
&amp;nbsp;&amp;nbsp;- istio-iptables
&amp;nbsp;&amp;nbsp;- &quot;-p&quot;
&amp;nbsp;&amp;nbsp;- {{ .MeshConfig.ProxyListenPort | default &quot;15001&quot; | quote }}
&amp;nbsp;&amp;nbsp;- &quot;-z&quot;
&amp;nbsp;&amp;nbsp;- &quot;15006&quot;
&amp;nbsp;&amp;nbsp;- &quot;-u&quot;
&amp;nbsp;&amp;nbsp;- &quot;1337&quot;
&amp;nbsp;&amp;nbsp;- &quot;-m&quot;
&amp;nbsp;&amp;nbsp;- &quot;{{ annotation .ObjectMeta `sidecar.istio.io/interceptionMode` .ProxyConfig.InterceptionMode }}&quot;
&amp;nbsp;&amp;nbsp;- &quot;-i&quot;
&amp;nbsp;&amp;nbsp;- &quot;{{ annotation .ObjectMeta `traffic.sidecar.istio.io/includeOutboundIPRanges` .Values.global.proxy.includeIPRanges }}&quot;
&amp;nbsp;&amp;nbsp;- &quot;-x&quot;
&amp;nbsp;&amp;nbsp;- &quot;{{ annotation .ObjectMeta `traffic.sidecar.istio.io/excludeOutboundIPRanges` .Values.global.proxy.excludeIPRanges }}&quot;
&amp;nbsp;&amp;nbsp;- &quot;-b&quot;
&amp;nbsp;&amp;nbsp;- &quot;{{ annotation .ObjectMeta `traffic.sidecar.istio.io/includeInboundPorts` .Values.global.proxy.includeInboundPorts }}&quot;
&amp;nbsp;&amp;nbsp;- &quot;-d&quot;
{{- if excludeInboundPort (annotation .ObjectMeta `status.sidecar.istio.io/port` .Values.global.proxy.statusPort) (annotation .ObjectMeta `traffic.sidecar.istio.io/excludeInboundPorts` .Values.global.proxy.excludeInboundPorts) }}
&amp;nbsp;&amp;nbsp;- &quot;15090,15021,{{ excludeInboundPort (annotation .ObjectMeta `status.sidecar.istio.io/port` .Values.global.proxy.statusPort) (annotation .ObjectMeta `traffic.sidecar.istio.io/excludeInboundPorts` .Values.global.proxy.excludeInboundPorts) }}&quot;
{{- else }}
&amp;nbsp;&amp;nbsp;- &quot;15090,15021&quot;
{{- end }}
&amp;nbsp;&amp;nbsp;{{ if or (isset .ObjectMeta.Annotations `traffic.sidecar.istio.io/includeOutboundPorts`) (ne (valueOrDefault .Values.global.proxy.includeOutboundPorts &quot;&quot;) &quot;&quot;) -}}
&amp;nbsp;&amp;nbsp;- &quot;-q&quot;
&amp;nbsp;&amp;nbsp;- &quot;{{ annotation .ObjectMeta `traffic.sidecar.istio.io/includeOutboundPorts` .Values.global.proxy.includeOutboundPorts }}&quot;
&amp;nbsp;&amp;nbsp;{{ end -}}
&amp;nbsp;&amp;nbsp;{{ if or (isset .ObjectMeta.Annotations `traffic.sidecar.istio.io/excludeOutboundPorts`) (ne (valueOrDefault .Values.global.proxy.excludeOutboundPorts &quot;&quot;) &quot;&quot;) -}}
&amp;nbsp;&amp;nbsp;- &quot;-o&quot;
&amp;nbsp;&amp;nbsp;- &quot;{{ annotation .ObjectMeta `traffic.sidecar.istio.io/excludeOutboundPorts` .Values.global.proxy.excludeOutboundPorts }}&quot;
&amp;nbsp;&amp;nbsp;{{ end -}}
&amp;nbsp;&amp;nbsp;{{ if (isset .ObjectMeta.Annotations `traffic.sidecar.istio.io/kubevirtInterfaces`) -}}
&amp;nbsp;&amp;nbsp;- &quot;-k&quot;
&amp;nbsp;&amp;nbsp;- &quot;{{ index .ObjectMeta.Annotations `traffic.sidecar.istio.io/kubevirtInterfaces` }}&quot;
&amp;nbsp;&amp;nbsp;{{ end -}}
&amp;nbsp;&amp;nbsp; {{ if (isset .ObjectMeta.Annotations `traffic.sidecar.istio.io/excludeInterfaces`) -}}
&amp;nbsp;&amp;nbsp;- &quot;-c&quot;
&amp;nbsp;&amp;nbsp;- &quot;{{ index .ObjectMeta.Annotations `traffic.sidecar.istio.io/excludeInterfaces` }}&quot;
&amp;nbsp;&amp;nbsp;{{ end -}}
&amp;nbsp;&amp;nbsp;- &quot;--log_output_level={{ annotation .ObjectMeta `sidecar.istio.io/agentLogLevel` .Values.global.logging.level }}&quot;
&amp;nbsp;&amp;nbsp;{{ if .Values.global.logAsJson -}}
&amp;nbsp;&amp;nbsp;- &quot;--log_as_json&quot;
&amp;nbsp;&amp;nbsp;{{ end -}}
&amp;nbsp;&amp;nbsp;{{ if .Values.istio_cni.enabled -}}
&amp;nbsp;&amp;nbsp;- &quot;--run-validation&quot;
&amp;nbsp;&amp;nbsp;- &quot;--skip-rule-apply&quot;
&amp;nbsp;&amp;nbsp;{{ end -}}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 예시는 Template 내부에 있는 Yaml 중 init-container의 Argument를 설정하는 부분을 발췌했습니다. 자세히 살펴보면 Template에는 사용자가 지정한 값을 기반으로 사이드카 주입을 위한 Yaml의 최종 내용이 결정되도록 디자인 되었음을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;해당 Template은 이미 서버 기동시점에 주입이 되었으므로 사용자 요청을 분석하여 Template을 만드는 작업을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-5 Template 적용&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;사이드카 컨테이너 주입을 위한 Template을 만들었으면, 이제 사용자가 요청한 Pod에 Template을 병합하는 과정을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1285&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcpLz2/btrNyD37kwe/v8zxbonbkXRHcrjiXipcN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcpLz2/btrNyD37kwe/v8zxbonbkXRHcrjiXipcN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcpLz2/btrNyD37kwe/v8zxbonbkXRHcrjiXipcN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcpLz2%2FbtrNyD37kwe%2Fv8zxbonbkXRHcrjiXipcN1%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;1285&quot; height=&quot;336&quot; data-origin-width=&quot;1285&quot; data-origin-height=&quot;336&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이를 위해서 첫번째로 하는 작업은 병합을 원활하게 수행하기 위해 Original Pod의 요청 Spec과 Template Yaml을 Map으로 변환합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lCSLG/btrNzE2RJ8o/xncC3eDpkqtrGqfzXtG6b0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lCSLG/btrNzE2RJ8o/xncC3eDpkqtrGqfzXtG6b0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lCSLG/btrNzE2RJ8o/xncC3eDpkqtrGqfzXtG6b0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlCSLG%2FbtrNzE2RJ8o%2FxncC3eDpkqtrGqfzXtG6b0%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;440&quot; height=&quot;796&quot; data-origin-width=&quot;622&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;위 그림은 Pod의 요청을 Map으로 변환한 originalMap과 Template Yaml을 Map으로 변환한 patchMap의 결과입니다. 내용을 살펴보면, patchMap에는 init-container 한개와 Container한개가 존재하는 것을 확인할 수 있습니다. 위 두 컨테이너의 이미지는 pilot-agent이며, 사이드카 컨테이너 주입을 통해 기본적으로 1개의 init-container와 Container가 추가로 삽입되는 것을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;개별 Container가 어떤 역할을 수행하는지는 다음 포스팅에서 살펴보도록하며, 지금은 위 그림을 통해 서로 다른 요청 Spec을 병합하기 위해 Map으로 만들었음을 이해하면 좋을 것 같습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두 Map을 만들고나면, 그 다음은 두 Map의 Key, Value를 비교하면서 병합하는 작업을 거칩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;681&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fpz1Y/btrNxe4DogY/KwjQgqpM4fnbeGFZ9jL6k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fpz1Y/btrNxe4DogY/KwjQgqpM4fnbeGFZ9jL6k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fpz1Y/btrNxe4DogY/KwjQgqpM4fnbeGFZ9jL6k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFpz1Y%2FbtrNxe4DogY%2FKwjQgqpM4fnbeGFZ9jL6k0%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;1982&quot; height=&quot;681&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;681&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위와같이 병합을 완료하고나면 최종적으로 하나의 Map이 완성됩니다. 내용을 살펴보면 Template에 존재하던 init-container 추가와 Template Container 더해져서 컨테이너 수가&amp;nbsp; 2가 되었음을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;병합이 완료된 이후에는 부가적인 작업을 수행하기 위해 postProcess 과정을 거칩니다. 이때 만약 Prometheus 설정이 존재한다면, 병합된 결과에 추가적으로 Prometheus 통합을 위한 어노테이션 등이 추가되는 작업을 거칩니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고 k8s에 반영을 요청하기 위해 JSON으로 데이터를 다시 변환 후 AdmissionResponse를 통해 응답을 반환하여 사이드카가 주입된 Pod 생성을 요청하는 것으로 마무리됩니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마치며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번 포스팅까지 해서 pilot-discovery의 가장 핵심인 Service Discovery와 사이드카 컨테이너 주입에 대해서 살펴봤습니다. Pilot-discovery는 위 두가지 기능 외에도 Multi Cluster 관리, 인증서 관리 등 중요한 기능과 핵심 컴포넌트가 여럿 존재합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;하지만 istio-internals 시리즈의 목표는 pilot-discovery와 pilot-agent 그리고 envoy가 어떻게 상호 작용하는지를 살펴보는 것이기 때문에 흐름을 유지하기 위해 우선 pilot-discovery에 대한 탐구는 여기서 마치고 나머지는 추후 포스팅을 통해 다루어 보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;다음 포스팅에서는 Sidecar Injector에 의해서 주입된 사이드카 컨테이너의 init-container에 대해서 살펴보겠습니다.&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <category>Envoy</category>
      <category>envoy architecture</category>
      <category>envoy 구조</category>
      <category>Istio</category>
      <category>istio architecture</category>
      <category>istio 구조</category>
      <category>istio 사이드카 컨테이너</category>
      <category>istio 아키텍처</category>
      <category>mutation webhook</category>
      <category>이스티오</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/199</guid>
      <comments>https://cla9.tistory.com/199#entry199comment</comments>
      <pubDate>Tue, 3 Mar 2026 11:50:44 +0900</pubDate>
    </item>
    <item>
      <title>10. [istio-internals] Pilot-discovery - XDS Server</title>
      <link>https://cla9.tistory.com/201</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전 포스팅을 통해 envoy를 관리하기 위해서 등장한 istio의 pilot 아키텍처와 Service Discovery 전파를 위한 informer 구조에 대해서 살펴봤습니다. 이번 포스팅에서는 pilot 아키텍처 중 pilot-discovery의 내부 컴포넌트 중 XDS Server에 대해서 살펴보면서 외부로 부터 전달된 Service 및 설정 변경이 어떻게 내부 과정을 거쳐 개별 pilot-agent의 envoy에 전파되는지 알아보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. XDS Server&amp;nbsp; 개요&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;1064&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcYPQg/btrMjPZHNNn/fcYjSstlKojm6SWhvMcad1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcYPQg/btrMjPZHNNn/fcYjSstlKojm6SWhvMcad1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcYPQg/btrMjPZHNNn/fcYjSstlKojm6SWhvMcad1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcYPQg%2FbtrMjPZHNNn%2FfcYjSstlKojm6SWhvMcad1%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;480&quot; height=&quot;591&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;1064&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이전 포스팅에서 pilot-discovery는 Informer로부터 Kubernetes의 Resource 정보를 전달받으면, Controller의 Handler를 통해서 관련 내용이 XDS Server에게 전달된다고 설명했습니다. 그리고 이를 전달받은 XDS Server의 역할은 개별 Service 들에게 xDS API 형태로 제공하여 Service 정보를 갱신하는 역할을 수행합니다. 이를 위해 XDS Server 내부에는 Service들의 Connection Pool을 관리하고 envoy가 이해할 수 있도록 xDS API로 변환하여 전달하는 컴포넌트 등이 존재합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇다면 XDS Server는 어떻게 구성되어있을까요?&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;624&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brDYpu/btrMvNe3YJD/0XvoRqvfb7iyKKj9Z0XbmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brDYpu/btrMvNe3YJD/0XvoRqvfb7iyKKj9Z0XbmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brDYpu/btrMvNe3YJD/0XvoRqvfb7iyKKj9Z0XbmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrDYpu%2FbtrMvNe3YJD%2F0XvoRqvfb7iyKKj9Z0XbmK%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;480&quot; height=&quot;399&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;624&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;핵심 컴포넌트에 대해서만 살펴보자면, 위와 같은 속성을 지니고 있습니다. 그중 특히 informer와 연계되어 Controller의 역할을 처리하는 것은 Enviroment에 속한 ServiceDiscovery입니다. 해당 컴포넌트에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. ServiceDiscovery&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;763&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t06jE/btrMjWdvjd1/UzM1newejIkDCAMsipkKL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t06jE/btrMjWdvjd1/UzM1newejIkDCAMsipkKL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t06jE/btrMjWdvjd1/UzM1newejIkDCAMsipkKL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft06jE%2FbtrMjWdvjd1%2FUzM1newejIkDCAMsipkKL1%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;320&quot; height=&quot;613&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;763&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;ServiceDiscovery의 주요 역할은 istio의 주요 관심 대상 Resource에 대해서 변경이 감지되었을 때, 이를 수신 받아 XDS Server로 전달하는&amp;nbsp;Controller&amp;nbsp;역할을 수행합니다. 위 그림을 살펴보면 총 21개가 관심 대상이며, 파란색으로 표시한 Resource는 istio에서 제공하는 CRD가 아니라 Kubernetes Gateway API에 해당하는 항목입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;참고로 Kubernetes Gateway API는 Kubernetes의 Network 서비스에 대한 표준 스펙을 정의하기 위한 Spec으로&amp;nbsp;자세한 내용은 아래 공식 문서를 참고하시기 바랍니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;a href=&quot;https://gateway-api.sigs.k8s.io/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://gateway-api.sigs.k8s.io/&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Introduction - Kubernetes Gateway API&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Introduction What is the Gateway API? Gateway API is an open source project managed by the SIG-NETWORK community. It is a collection of resources that model service networking in Kubernetes. These resources - GatewayClass,Gateway, HTTPRoute, TCPRoute, Serv&quot; data-og-host=&quot;gateway-api.sigs.k8s.io&quot; data-og-source-url=&quot;https://gateway-api.sigs.k8s.io/&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/bI1mmw/hyPOIgWdrX/AAAAAAAAAAAAAAAAAAAAAMEmbcckKIS8QBQjX1g8upQnJ4l7Rm_4aBsRAuddprky/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1774969199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=IOejdwvcb7uyy6abOF1EEDvm0C0%3D&quot; data-og-url=&quot;https://gateway-api.sigs.k8s.io/&quot;&gt;&lt;a href=&quot;https://gateway-api.sigs.k8s.io/&quot; target=&quot;_blank&quot; data-source-url=&quot;https://gateway-api.sigs.k8s.io/&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/bI1mmw/hyPOIgWdrX/AAAAAAAAAAAAAAAAAAAAAMEmbcckKIS8QBQjX1g8upQnJ4l7Rm_4aBsRAuddprky/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1774969199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=IOejdwvcb7uyy6abOF1EEDvm0C0%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;Introduction - Kubernetes Gateway API&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;Introduction What is the Gateway API? Gateway API is an open source project managed by the SIG-NETWORK community. It is a collection of resources that model service networking in Kubernetes. These resources - GatewayClass,Gateway, HTTPRoute, TCPRoute, Serv&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;gateway-api.sigs.k8s.io&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;istio에서는 Kubernetes Gateway API Resource가 생성/수정/삭제되면, 해당 내용을 분석하여 xDS API 변환하여 전달하도록 구현되어있습니다. 다만 Kubernetes Gateway API는 Kubernetes 설치 시 기본적으로 등록된 API가 아니기 때문에 별도로 설치해야합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1743&quot; data-origin-height=&quot;1078&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMFZjy/btrMjXQ5tQA/KRkSTAwyvDCCxX8y0eGyDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMFZjy/btrMjXQ5tQA/KRkSTAwyvDCCxX8y0eGyDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMFZjy/btrMjXQ5tQA/KRkSTAwyvDCCxX8y0eGyDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMFZjy%2FbtrMjXQ5tQA%2FKRkSTAwyvDCCxX8y0eGyDK%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;1743&quot; height=&quot;1078&quot; data-origin-width=&quot;1743&quot; data-origin-height=&quot;1078&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;istio에서는 Kubernetes Gateway API 지원을 위해서 위 그림과 같이 환경 변수를 통해 pilot-discovery 프로그램을 수행합니다. 이때&amp;nbsp;&lt;span style=&quot;color: #EE2323;&quot;&gt;&lt;b&gt;PILOT_ENABLE_GATEWAY_API&lt;/b&gt;&amp;nbsp;&lt;/span&gt;환경 변수가 true로 지정되어있는지 여부를 확인합니다. 그 결과 값이 true로 지정된 경우에는 위와 같이 istio에서 제공하는 CRD와 더불어 Kuberntes Gateway API의 Resource 정보를 관심 Collection에 추가합니다. 반대로 해당 값이 false인 경우에는 istio에서 제공하는 CRD만 관심 Collection에 추가하도록 구성되어있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;위와 같은 과정을 거치게되면, istio에서 주요 관심 대상에 대한 Resource 추출은 끝나게됩니다. 그 다음 수행해야하는 일은 Controller를 구성하고 Informer에 등록하는 작업입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2248&quot; data-origin-height=&quot;837&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ykg8W/btrMoyoxakb/VggFKxPybTKyzVy5u9EbAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ykg8W/btrMoyoxakb/VggFKxPybTKyzVy5u9EbAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ykg8W/btrMoyoxakb/VggFKxPybTKyzVy5u9EbAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fykg8W%2FbtrMoyoxakb%2FVggFKxPybTKyzVy5u9EbAk%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;2248&quot; height=&quot;837&quot; data-origin-width=&quot;2248&quot; data-origin-height=&quot;837&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;우선 Controller를 구성했다고 가정하고, 이전 과정에서 추출한 관심 대상을 기반으로 Informer와 연결하는 부분부터 살펴보겠습니다. 이전에 추출한 관심 대상 Resource를 등록하는 작업을 수행합니다. 해당 과정은 총 4가지 절차를 통해 수행됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;1. 먼저 kubernetes에 등록된 CRD 정보를 조회합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;2. 조회한 CRD 정보 중에서 istio의 관심대상 Resource에 부합되는 정보만 Filtering 합니다. 이때 Kubernetes Gateway API는 Kubernetes를 설치했다고 해서 기본적으로 등록되는 Resource 정보가 아닙니다. 따라서 별도로 Gateway API CRD 설치(&lt;a href=&quot;https://istio.io/latest/docs/tasks/traffic-management/ingress/gateway-api/#setup&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://istio.io/latest/docs/tasks/traffic-management/ingress/gateway-api/#setup&lt;/span&gt;&lt;/a&gt;)를 수행하지 않았을 경우에는 등록된 CRD 정보가 없기 때문에 Filtering 됩니다. 반면 해당 CRD가 설치되어있으면 Filtering 결과에 포함될 것입니다. 참고로 위 예시는 설치가 안되어있다는 가정하에 작성하였습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;3. 관심 대상 Resource 간에 서로 Group이 다르기 때문에 Group 별로 매칭되는 Informer를 찾습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;373&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FmjmE/btrMlkkcrYZ/gfDu9VPqbfe2asEZPU1Gi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FmjmE/btrMlkkcrYZ/gfDu9VPqbfe2asEZPU1Gi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FmjmE/btrMlkkcrYZ/gfDu9VPqbfe2asEZPU1Gi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFmjmE%2FbtrMlkkcrYZ%2FgfDu9VPqbfe2asEZPU1Gi0%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;679&quot; height=&quot;373&quot; data-origin-width=&quot;679&quot; data-origin-height=&quot;373&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;4.&amp;nbsp; Resource Group에 해당하는 Informer에 Resource 생성/수정/삭제가 발생했을 경우 Callback을 위한 Handler를 등록합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;761&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/98WMF/btrMjgKtjtL/WIKoklp6lNtVoxKPHR9b2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/98WMF/btrMjgKtjtL/WIKoklp6lNtVoxKPHR9b2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/98WMF/btrMjgKtjtL/WIKoklp6lNtVoxKPHR9b2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F98WMF%2FbtrMjgKtjtL%2FWIKoklp6lNtVoxKPHR9b2k%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;580&quot; height=&quot;670&quot; data-origin-width=&quot;659&quot; data-origin-height=&quot;761&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 코드내용을 살펴보면, 개별 Callback 메소드에서 수행하는 작업은 Controller Queue에 Event 타입과 전달받은 Object 내용을 전달하도록 구성되어있음을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&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;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nKhGz/btrMjiheR1K/gRxz9jFNaOJZ9VfLkr96Bk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nKhGz/btrMjiheR1K/gRxz9jFNaOJZ9VfLkr96Bk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nKhGz/btrMjiheR1K/gRxz9jFNaOJZ9VfLkr96Bk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnKhGz%2FbtrMjiheR1K%2FgRxz9jFNaOJZ9VfLkr96Bk%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;362&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이번에는 ServiceDiscovery 즉 Controller를 구성하고 있는 주요 컴포넌트에 대한 소개와 더불어 해당 컴포넌트와 이전에 설명한 informer 연계 과정이 어떻게 연결되는지를 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;ServiceDiscovery에는 3가지 주요 속성이 존재합니다. 하나씩 살펴보면 다음과 같습니다. 먼저 queue는 informer로부터 Event가 발생하였을 때 이를 전달하는 중간 버퍼의 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;두번째로 살펴볼 것은 handlers입니다. istio의 관심 대상 Resource Group이 여러개이다 보니 GVK(Group Version Kind)별로 대상 informer가 다르고 이를 처리해야하는 Handler 로직 또한 달리 작성해야할 수 있습니다. 따라서 Service Discovery에서는 GVK 별로 Handler를 매핑하여 Map으로 관리하고 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;859&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blhhKy/btrMnLaEeeK/UNHfVm9TJzCgahx4YWjiMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blhhKy/btrMnLaEeeK/UNHfVm9TJzCgahx4YWjiMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blhhKy/btrMnLaEeeK/UNHfVm9TJzCgahx4YWjiMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblhhKy%2FbtrMnLaEeeK%2FUNHfVm9TJzCgahx4YWjiMk%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;640&quot; height=&quot;515&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;859&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이때 GVK 별로 생성되는 Handler의 모습은 위와 같으며, 가장 중요한 부분은 informer로 부터 전달받은 설정을 기반으로 PushRequest 요청을 만들고 이를 XDSServer.ConfigUpdate 메소드를 호출함으로써, XDSServer와 연결 되어있는 모든 Client에게 설정 정보를 동기화하도록 요청하는 것입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d9upMI/btrMoS1GylG/95iTz0rFR71Gd2QDr4a5tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d9upMI/btrMoS1GylG/95iTz0rFR71Gd2QDr4a5tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d9upMI/btrMoS1GylG/95iTz0rFR71Gd2QDr4a5tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd9upMI%2FbtrMoS1GylG%2F95iTz0rFR71Gd2QDr4a5tk%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;294&quot; height=&quot;133&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;133&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;세번째로 살펴볼 것은 kinds로 GVK 별로 매핑되는 informer 정보가 다르기 때문에, 해당 정보를 관리하기 위한 용도로 사용됩니다. handlers와 마찬가지로 Map으로 구성되어있으며 GVK 별로 어떤 Resource가 있으며, 어떤 informer와 매칭되어있는지 정보를 구조화하여 담고 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1933&quot; data-origin-height=&quot;1269&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIYSuA/btrMlU6ObRV/cFPLPWjML3KtSbjJnkxxUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIYSuA/btrMlU6ObRV/cFPLPWjML3KtSbjJnkxxUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIYSuA/btrMlU6ObRV/cFPLPWjML3KtSbjJnkxxUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIYSuA%2FbtrMlU6ObRV%2FcFPLPWjML3KtSbjJnkxxUk%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;1933&quot; height=&quot;1269&quot; data-origin-width=&quot;1933&quot; data-origin-height=&quot;1269&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;지금까지 설명한 내용을 바탕으로 Service Discovery와 istio Kube Client의 연계 과정을 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;1. 관심 대상 Resource를 순회하면서, GVK 별로 대상 informer를 찾습니다.&lt;br&gt;2. informer를 찾았으면 event 발생할 경우 정보를 전달받기 위한 Handler를 등록합니다. 이때 해당 Handler는 GVK를 Key로 가지고 Callback Function을 Value로 가지는 handlers Map에서 GVK에 매칭되는 Function을 등록합니다. 이때 등록되는 Function에는 XDSServer로 Update하는 코드가 포함되어있습니다.&lt;br&gt;3. queue에서는 지속적으로 하나씩 processing 하면서 informer로 부터 전달받은 오브젝트 정보를 기반으로 매칭되는 Handler 실행을 통해 XDS Server로 Update를 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;여기까지가 XDS Server 내에 위치한 ServiceDiscovery의 역할입니다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Event 전파&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번에는 이전 내용에서 XDSServer.ConfigUpdate 메소드를 호출했을 때, XDS Server 내부에서 어떻게 처리하는지를 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;595&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mQcHD/btrMnnaUfLN/qOPL888apj0ESLPTcI38S1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mQcHD/btrMnnaUfLN/qOPL888apj0ESLPTcI38S1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mQcHD/btrMnnaUfLN/qOPL888apj0ESLPTcI38S1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmQcHD%2FbtrMnnaUfLN%2FqOPL888apj0ESLPTcI38S1%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;1600&quot; height=&quot;595&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;595&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;ConfigUpdate 메소드가 호출되면, 가장 먼저 하는 일은 XDSServer에 있는 PushChannel에 요청을 전달합니다. 위 그림에서 확인할 수 있듯이 해당 속성은 PushRequest를 전달받아 다른 루틴으로 전달하는 Channel임을 알 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이후 Channel로 전달된 데이터는 내부적으로는 debounce 메소드가 별도의 go routine으로 동작하여 해당 데이터를 처리합니다. 그렇다면 debounce 작업은 왜 하는 것일까요?&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1 debounce&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqVHrQ/btrMmgpr5Cd/QdSrbEK1OS7yJ6wS4gI6h1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqVHrQ/btrMmgpr5Cd/QdSrbEK1OS7yJ6wS4gI6h1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqVHrQ/btrMmgpr5Cd/QdSrbEK1OS7yJ6wS4gI6h1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqVHrQ%2FbtrMmgpr5Cd%2FQdSrbEK1OS7yJ6wS4gI6h1%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;640&quot; height=&quot;327&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;debounce는 이벤트가 연이어 발생했을 때, 이를 개별적으로 하나씩 처리하지 않고 그룹핑하여 전달하는 것에 목적이 있습니다. 이에 대한 이해를 돕기위해 최대 5명 운송 가능한 엘리베이터를 이용하는 상황을 가정해봅시다.&lt;br&gt;&amp;nbsp;&lt;br&gt;만약 엘리베이터에 대기하는 사람이 6명이 존재하는 상황에서 최대 5명까지 이용 가능하지만 이용객 한명씩 순차적으로 엘리베이터를 사용할 수 있다면 어떻게 될까요? 당연히 운송 시간은 증가할 것이며, 엘리베이터 가동 비용 측면에서 봤을 때에도 굉장히 비효율입니다. 이때 가장 효율적인 방법은 엘리베이터가 허용 가능한 인원만큼 탑승해서 한번에 운반하는 것입니다. 따라서 위 경우에서는 처음 5명을 태운 승강기를 한번 운행하고 그 다음 한번 더 운행 즉 총 2번의 운행하는 것이 가장 좋습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvFOPg/btrMoSVXMG0/eTGcEhIZpdlbTV7w0wkmn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvFOPg/btrMoSVXMG0/eTGcEhIZpdlbTV7w0wkmn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvFOPg/btrMoSVXMG0/eTGcEhIZpdlbTV7w0wkmn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvFOPg%2FbtrMoSVXMG0%2FeTGcEhIZpdlbTV7w0wkmn1%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;360&quot; height=&quot;360&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번에는 엘리베이터에 한명이 현재 탑승한 상황이라고 가정해봅시다. 이때 시설 관리자 입장에서는 5명까지 이용한 엘리베이터에서 한명만 운행하는 것은 금전적으로 비효율입니다. 따라서 시설 관리자 입장에서만 생각해본다면, 다섯명이 탑승할 때까지 엘리베이터를 기다리게 하는 것이 가장 좋은 선택입니다. 하지만 이 경우에는 다섯명이 탑승할 때까지 엘리베이터가 작동하지 않으므로 탑승객 입장에서는 엄청난 불편함을 초래할 것입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vFOMJ/btrMljNyqxX/EpKwd8z9G3Pp9NOK6e4uS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vFOMJ/btrMljNyqxX/EpKwd8z9G3Pp9NOK6e4uS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vFOMJ/btrMljNyqxX/EpKwd8z9G3Pp9NOK6e4uS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvFOMJ%2FbtrMljNyqxX%2FEpKwd8z9G3Pp9NOK6e4uS1%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;360&quot; height=&quot;360&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;따라서 두 이해 당사자간의 입장을 고려하여 설계된 일반적인 엘리베이터의 모습은 엘리베이터에 탑승객이 탑승하고나서 일정시간이 지날 때까지 탑승하는 인원이 없으면 문이 닫히고 동작하도록 되어있습니다. 또한 중간에 엘리베이터에 탑승하려는 승객이 추가로 발생할 경우에는 센서가 감지하여 엘리베이터 문이 닫히지 않도록 하며, 그때부터 다시 일정 시간 동안 대기하도록 되어있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;위와 같은 엘리베이터는 탑승객 입장에서는 정원이 다 차지 않아도 일정 시간동안 승객의 유입이 없으면 동작하기 때문에 다소 불편함을 감소할 수 있습니다. 또한 시설 관리자 입장에서도 엘리베이터가 아직 닫히지 않는 이상 추가 이용 승객이 발생하면 탑승 가능하기 때문에 합리적인 방법입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;799&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oyKQh/btrMozvmMy9/pfyHUNeEC1zHnpAW6kvYv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oyKQh/btrMozvmMy9/pfyHUNeEC1zHnpAW6kvYv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oyKQh/btrMozvmMy9/pfyHUNeEC1zHnpAW6kvYv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoyKQh%2FbtrMozvmMy9%2FpfyHUNeEC1zHnpAW6kvYv0%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;640&quot; height=&quot;327&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;799&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이번에는 100명이 이용할 수 있는 엘리베이터가 있고 최초 승객이 탑승 이후 약 10초 이후에 엘리베이터가 자동으로 닫힌다고 가정해봅시다. 이러한 상황에서 최초 승객이 탑승한 이후에 10초마다 새로운 탑승객이 발생한다면 어떻게 될까요? 처음 탑승한 승객은 이전과 같이 엘리베이터가 수용 가능한 모든 인원이 탑승할때까지 대기하게되는 문제가 발생합니다. 따라서 이러한 경우에 승객의 불편함을 최소화할 수 있는 방법은 처음 엘리베이터에 탑승객이 탑승한 이후로부터 최대 대기시간을 설정하여 최대 대기시간을 넘기게되면 추가 승객이 탑승하더라도 타이머가 더 이상 동작하지 않고 닫히도록 구현하는 것입니다. 이렇게 되면 처음 탑승한 승객 입장에서는 최대 대기시간 만큼만 대기하면 되므로 불편함은 최소화 될 수 있을겁니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;지금까지 엘리베이터 시나리오를 설명했는데, istio에서는 위 시나리오 내용과 같은 이유로 동일하게 debounce를 적용했습니다. istio pilot-discovery에는 모든 pilot-agent와 Connection을 맺고 있는데, 이벤트 하나하나씩 발생할 때마다 broadcast하는 것은 굉장히 통신 비용을 증가하는 요인입니다.&lt;br&gt;&amp;nbsp;&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;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eCReL9/btrMkNBmprn/hVtpiodDnlaRuca05NeXCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eCReL9/btrMkNBmprn/hVtpiodDnlaRuca05NeXCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eCReL9/btrMkNBmprn/hVtpiodDnlaRuca05NeXCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeCReL9%2FbtrMkNBmprn%2FhVtpiodDnlaRuca05NeXCK%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;640&quot; height=&quot;277&quot; data-origin-width=&quot;821&quot; data-origin-height=&quot;355&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;따라서 이때 debounce를 지정하면, 메시지를 효과적으로 처리할 수 있습니다. 가령 최초 PushReqest 요청을 전달받았을 때로 부터 일정 시간 내에 메시지가 유입되는 경우 해당 메시지와 이전 메시지를 병합합니다. 참고로 이때 병합되는 메시지는 PushRequest내에 있는 Reason Slice에 기존 메시지가 추가됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이후 또 다시 일정 시간 이내에 메시지가 추가 유입되면 병합하고 만약 그 시간동안 메시지가 유입되지 않는다면 지금까지 병합한 메시지를 한번에 전송합니다. 다만 이러한 경우 메시지가 계속 추가로 유입되는 경우 한없이 기다릴 수 있기 때문에 최대 대기 시간을 설정하여 최대 대기 시간이 지나면 메시지 처리를 수행하도록 수행할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;istio에서는&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;PILOT_ENABLE_EDS_DEBOUNCE&lt;/span&gt;&amp;nbsp;&lt;/b&gt;환경 변수가 true로 되어있을 때 debounce가 작동하도록 설계되었습니다. 해당 값은 기본적으로 true이기 때문에 별도의 설정이 없다면 항상 debounce는 작동하고 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;PILOT_DEBOUNCE_AFTER&lt;/span&gt;&lt;/b&gt;와&amp;nbsp;&lt;span style=&quot;color: #EE2323;&quot;&gt;&lt;b&gt;PILOT_DEBOUNCE_MAX&lt;/b&gt;&amp;nbsp;&lt;/span&gt;값은 이전에 설명한 일정 대기 시간과 최대 대기 시간을 의미합니다. 기본적으로 일정 대기 시간은 100ms 로써 100ms 동안 Event가 발생하면 해당 Event는 하나의 메시지로 병합됩니다. 또한 최대 대기 시간은 10초로 설정되어있으며, 메시지가 아무리 지속 유입이 된다고 할지라도 10초를 넘어가면 메시지 병합을 멈추고 메시지를 발행하도록 되어있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서 만약 istio를 운영하다가 너무 잦은 sync로 인한 overhead가 발생한다면 해당 두 값을 적절하게 튜닝하여 메시지 동기화 속도를 조절할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGtk8Z/btrMp25dhWT/aKdoQGUTVFHf7WN3reK8wK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGtk8Z/btrMp25dhWT/aKdoQGUTVFHf7WN3reK8wK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGtk8Z/btrMp25dhWT/aKdoQGUTVFHf7WN3reK8wK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGtk8Z%2FbtrMp25dhWT%2FaKdoQGUTVFHf7WN3reK8wK%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;1600&quot; height=&quot;662&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;debouce 작업이 끝나면, 하나의 메시지로 통합되며, 이를 통해 하나의 요청만 전달할 수 있습니다. 그 다음 작업은 해당 메시지를 개별 client로 전달하는 것입니다. 이때 전체 Client의 정보는 AdsClients에 저장되어 있습니다. 따라서 Client 별로 해당 메시지를 매핑하여 Queue에 입력합니다. 여기까지가 debounce 작업과 후속 메시지 처리 작업입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2 PushQueue&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전 내용에서 debounce 이후 PushQueue에 Client 정보와 요청 정보 메시지를 삽입하는 것을 확인했습니다. 이때 Enqueue 메소드 안에서는 여러가지 작업이 이루어지는데, 이와 관련하여 PushQueue의 구조와 수행 동작에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&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;223&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPbpP1/btrMxFg8dTN/kpLUNTgN3rhjezoki8lihk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPbpP1/btrMxFg8dTN/kpLUNTgN3rhjezoki8lihk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPbpP1/btrMxFg8dTN/kpLUNTgN3rhjezoki8lihk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPbpP1%2FbtrMxFg8dTN%2FkpLUNTgN3rhjezoki8lihk%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;223&quot; data-origin-width=&quot;611&quot; data-origin-height=&quot;223&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Push Queue의 주요 속성은 위 3가지입니다. queue에는 Client의 연결을 slice 형태로 저장하고 있고 Client 별로 pending과 processing의 Map이 존재하는 것을 확인할 수 있습니다. 위 세가지 자료 구조는 Push Queue가 동작하는데 있어 주요하게 사용됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;먼저 Queue에 삽입할 때 과정을 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1409&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckPeTc/btrMoSPZRCK/IhvnQNDG2a5LwXcLWye3C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckPeTc/btrMoSPZRCK/IhvnQNDG2a5LwXcLWye3C0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckPeTc/btrMoSPZRCK/IhvnQNDG2a5LwXcLWye3C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckPeTc%2FbtrMoSPZRCK%2FIhvnQNDG2a5LwXcLWye3C0%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;1409&quot; height=&quot;400&quot; data-origin-width=&quot;1409&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;debounce를 통해 입력되는 쓰레드와 Push Queue 처리 쓰레드는 별개의 go routine으로 동작합니다. 따라서 Push Queue에 빠르게 적재되었을 지라도 Push Queue의 처리 능력에 따라서 아직 처리되지 않고 Queue에 남아있을 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서 가장 먼저 수행하는 일은 queue 속성에 Client를 저장하는 작업 이외에 Client를 Key, 요청 Requset를 Value로 사용하는 pending Map에 메시지를 집어 넣습니다. 또한 Client는 queue Slice에 추가합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이때 모습은 위 그림과 같은 형태일 것입니다. 만약 이러한 상황에서 debounce 작업이 한차례 더 진행되고 이때 Service A에 대해서 새로운 PushRequest가 발생했는데, 아직 Push Queue에서 기존 데이터가 처리되지 않은 상황이라면 어떻게될까요?&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTCtPM/btrMxFayzpR/4mMhOeSKqo6G7KZHWAA2Rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTCtPM/btrMxFayzpR/4mMhOeSKqo6G7KZHWAA2Rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTCtPM/btrMxFayzpR/4mMhOeSKqo6G7KZHWAA2Rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTCtPM%2FbtrMxFayzpR%2F4mMhOeSKqo6G7KZHWAA2Rk%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;1388&quot; height=&quot;480&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;아직 메시지가 전송된 상태가 아니기 때문에 debounce에서 메시지들을 Merge 했던것 처럼 해당 Client에 전송되는 Message에 대해서 Merge 작업을 수행하도록 진행합니다. 따라서 Pending의 목적은 처리되지 않은 메시지를 저장함과 동시에 추가로 발행되는 메시지 중 Connection 정보가 일치하는 요청에 대해서는 Merge 작업을 수행하여 메시지 처리량을 높이는 효과가 있습니다.&lt;br&gt;&amp;nbsp;&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;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGNx9b/btrMxTGn3ja/0shx8cdGo5p5YOUm694v0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGNx9b/btrMxTGn3ja/0shx8cdGo5p5YOUm694v0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGNx9b/btrMxTGn3ja/0shx8cdGo5p5YOUm694v0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGNx9b%2FbtrMxTGn3ja%2F0shx8cdGo5p5YOUm694v0k%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;660&quot; data-origin-width=&quot;1485&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Push Queue에서 주기적으로 Queue에 데이터가 존재하는지 탐색하고 처리하는 작업은 doSendPushes 메소드에서 수행하는데, 해당 메소드는 별도의 go Routine으로 동작합니다. doSendPushes에서 처리하는 과정은 총 3단계로 이루어져있으며, 이는 다음과 같습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;1. Dequeue 명령을 통해 Push Queue에 존재하는 queue에서 Client 정보를 추출하고 pending Map에 존재하는 Client 정보를 삭제합니다. 추가로 작업 진행을 관리하기 위해 processing Map에 Client 정보를 추가합니다. 이때 중요한 점은 Map에 데이터를 넣을 때 Key 값 즉 Client 정보만 Map에 입력하고 Value는 nil로 입력한다는 것입니다. 그 이유에 대해서는 추후 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;2. Client 구조체에 위치한 PushChannel에 데이터를 삽입합니다. 이후 처리 과정은 Client와 연계되어있는 ADS에서 처리합니다. 이에 대해서는 추후 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;3. Client에 데이터 전송이 완료되면, 완료 응답을 수신받습니다. 응답 수신이 완료되면, processing Map에서 해당 정보를 삭제합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;위와 같은 세 가지 단계를 거치면 데이터가 정상적으로 처리되는 것을 확인할 수 있습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lu4gG/btrMvMBl43Y/yuIyTraRFzVnnA3btfj0d1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lu4gG/btrMvMBl43Y/yuIyTraRFzVnnA3btfj0d1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lu4gG/btrMvMBl43Y/yuIyTraRFzVnnA3btfj0d1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flu4gG%2FbtrMvMBl43Y%2FyuIyTraRFzVnnA3btfj0d1%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;1388&quot; height=&quot;480&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그렇다면 현재 데이터를 Service A에 전송하고 있는 과정에서 동일한 Client에게 Push Request가 요청되면 어떻게 될까요? 이때는 processing Map을 통해 메시지를 처리합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;메시지를 Client에게 전송하면 processing Map에 Connection 정보를 입력하는 것을 이전 내용을 통해 확인했습니다. 따라서 debounce 이후 Queue에 입력하는 과정에서 해당 Map을 확인하면 현재 메시지 전송 상태를 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이 경우에는 processing Map에 존재하는 Client 데이터에 Push Request를 추가합니다. 만약 그 이후에도 Client의 전송 완료 메시지를 받기 이전에 Push Request를 전달받으면 지속적으로 Push Request를 Merge 하여 하나의 메시지로 만듭니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이후 Client가 작업을 종료하고 전송 완료 응답을 전달하였을 때, processing Map을 살펴봅니다. 이때 Client Key에 해당하는 Value가 nil일 경우에는 메시지 전송 이후로 해당 Client에 요청되는 추가적인 메시지가 없으므로 작업을 종료합니다. 하지만 Value가 nil이 아닐 경우에는 중간에 입력된 메시지가 존재함을 의미합니다. 따라서 그 다음 doSendPushes 싸이클에서 메시지가 다시 처리될 수 있도록 pending 메시지에 해당 Client 정보와 Push Request를 다시 입력하고 queue에도 Client 정보를 삽입합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;지금까지 Push Queue에 대해 살펴봤습니다. 이를 통해 메시지를 전달하는 과정에서도 Network overhead를 줄이기 위해 여러가지 최적화 장치가 마련된 모습을 볼 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Connection 관리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;지금까지 Event가 발생했을 때, debounce 과정과 이후 Push Queue에서 데이터를 Client에게 전달하고 후속 작업을 처리하는 과정에 대해서 살펴봤습니다. 이번에는 XDS Server Client Connection 관리 주체와 어떻게 통신을 수행하는지 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1135&quot; data-origin-height=&quot;171&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qist2/btrMxEQ4Uuw/ZQUSmVIg747Da5Pg1ytyW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qist2/btrMxEQ4Uuw/ZQUSmVIg747Da5Pg1ytyW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qist2/btrMxEQ4Uuw/ZQUSmVIg747Da5Pg1ytyW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqist2%2FbtrMxEQ4Uuw%2FZQUSmVIg747Da5Pg1ytyW1%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;1135&quot; height=&quot;171&quot; data-origin-width=&quot;1135&quot; data-origin-height=&quot;171&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;envoy에서는 위와 같이 ADS Server에 대한 gRPC interface를 제공합니다. 따라서 istio에서는 해당 ADS Spec에 부합하도록 gRPC 코드가 구현되어있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1315&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q3sFB/btrMGAGDlvT/oN2n6SFujKTeciiLb40gJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q3sFB/btrMGAGDlvT/oN2n6SFujKTeciiLb40gJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q3sFB/btrMGAGDlvT/oN2n6SFujKTeciiLb40gJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq3sFB%2FbtrMGAGDlvT%2FoN2n6SFujKTeciiLb40gJ0%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;1315&quot; height=&quot;662&quot; data-origin-width=&quot;1315&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;istio에서는 사용자 요청을 처리하기 위해서 내부적으로 gRPC Server가 있습니다. pilot-agent는 gRPC에 정의된 interface 호출을 통해 pilot-discovery에 접속을 접속과 더불어 필요한 Resource 정보를 요청합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;사용자의 요청을 전달받으면 해당 Server는 별개의 go routine을 통해 receive와 processRequest 두 개의 작업을 수행합니다. 여기서 주목할 점은 receive와 processRequest는 별개로 동작하지만 두 routine간의 Request Channel로 연결되어있다는 점입니다. 지금부터는 두 과정에 대해서 살펴보면서 동작 원리를 관찰해보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;먼저 살펴볼 것은 receive입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1053&quot; data-origin-height=&quot;1025&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcJBh3/btrMC9jIuPU/t1YilZPows9NATlD516h81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcJBh3/btrMC9jIuPU/t1YilZPows9NATlD516h81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcJBh3/btrMC9jIuPU/t1YilZPows9NATlD516h81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcJBh3%2FbtrMC9jIuPU%2Ft1YilZPows9NATlD516h81%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;1053&quot; height=&quot;1025&quot; data-origin-width=&quot;1053&quot; data-origin-height=&quot;1025&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;receive의 역할은 연결된 Connection에서 데이터 처리가 필요할 때 해당 Request를 Requet Channel로 연결하기 위함입니다. 위 코드 내용을 통해 이를 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;최초에 Client가 gRPC 서버에 접속을 요청하면, firstRequest는 true일 것입니다. 이후 for-loop 구문을 수행하는데, 위 코드를 살펴보면 for-loop 자체에는 별도의 조건이 없기 때문에 계속 반복 수행되는 것을 확인할 수 있습니다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;1143&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/thMmJ/btrMFizwLn6/DtQ9wfxEV1oJukjx15bDsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/thMmJ/btrMFizwLn6/DtQ9wfxEV1oJukjx15bDsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/thMmJ/btrMFizwLn6/DtQ9wfxEV1oJukjx15bDsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FthMmJ%2FbtrMFizwLn6%2FDtQ9wfxEV1oJukjx15bDsK%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;1560&quot; height=&quot;1143&quot; data-origin-width=&quot;1560&quot; data-origin-height=&quot;1143&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이후 for-loop 구문으로 진입했을 때 눈여겨볼 점은 최초 접속시 initConnection 메소드를 호출한다는 점입니다. 해당 메소드가 수행하는 기능은 사용자의 요청이 적합한지 권한 검사 이후에 XDS Server에 위치하는 AdsClients Slice에 해당 Connection 정보를 추가하는 작업을 진행합니다. 마찬가지로&amp;nbsp;defer 메소드로 Client가 접속을 종료할 때는 closeConnection 메소드를 호출하여 AdsClients로부터 해당 정보를 삭제하는 과정 또한 진행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서 이전에 Service Discovery 로직에서 Event 정보를 모든 Clients에게 전파할 때 해당 자료 구조를 참조했음을 확인할 수 있는데 receive 로직에서 AdsClients를 관리함을 알 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;사용자 최초 접속 하여 Connection 할당 정보가 완료된 이후, 수행하는 일은 사용자로부터 전달 받은 요청이 있으면 이를 Request Channel로 전달하는 중간 매개체 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;정리하자면 별도의 go routine으로 동작하는 receive 메소드의 역할은 Connection 관리와 더불어 Connection에서 발생한 Request 정보를 Request Channel에 전파하는 것입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이번에는 사용자의 요청을 처리하거나 Server의 전달 요청을 처리하는 로직 부분을 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btuq3B/btrMDouceKI/figp6ElQWnBEd527GhJ6B0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btuq3B/btrMDouceKI/figp6ElQWnBEd527GhJ6B0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btuq3B/btrMDouceKI/figp6ElQWnBEd527GhJ6B0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbtuq3B%2FbtrMDouceKI%2Ffigp6ElQWnBEd527GhJ6B0%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;497&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;해당 로직 또한 마찬가지로 for-loop을 지속적으로 수행하면서 요청 구분에 따라 처리 방법을 달리 수행합니다. 그 중 위 코드가 핵심 로직을 설명합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;먼저 살펴볼 것은 사용자의 요청에 대한 처리입니다. Receive go routine에서 사용자 요청을 최초 접수받고 이를 Request Channel에 전파한다고 이전에 설명했습니다. 이때 Channel로 전파된 데이터를 수신받고 processRequset 메소드에 전달하는 역할을 수행합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1653&quot; data-origin-height=&quot;1128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nBHhS/btrMFGVnxFW/WEgwsCznhwSJDtf991cUE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nBHhS/btrMFGVnxFW/WEgwsCznhwSJDtf991cUE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nBHhS/btrMFGVnxFW/WEgwsCznhwSJDtf991cUE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnBHhS%2FbtrMFGVnxFW%2FWEgwsCznhwSJDtf991cUE0%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;1653&quot; height=&quot;1128&quot; data-origin-width=&quot;1653&quot; data-origin-height=&quot;1128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이때 processRequest 로직에서는 내부적으로 위와 같은 작업이 수행됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;1. Client가 요청한 DiscoveryRequest는 ADS interface에 의해 정의된 Envoy 구조체로써 Discovery를 희망하는 요청 타입(ex Cluster)을 알 수 있습니다. 따라서 먼저 해당 정보를 파싱합니다. 그리고 요청하는 Resource를 지속적으로 추적하기 위해서 Connection 내부에 위치한 WatchedResource map에 해당 Type을 저장합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;2. istio 내부에서는 Envoy Spec과 달리 Abstract한 Model 구조를 사용합니다. 따라서 istio에서 통용되는 데이터 구조인 PushRequest로변환합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;3. istio는 내부에서 관리하는 데이터 타입 구조를 envoy가 이해할 수 있는 xDS로 변환하기 위해 내부적으로 Generator를 내장하고 있습니다. 위 그림과 같이 Generator는 Map 형태로 되어있으며, 각기 다른 Generator 중 Watched Resource Slice에 저장된 사용자가 원하는 타입에 부합하는 Generator를 찾아 xDS API 스펙에 맞게 변환하는 작업을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;4. 변환 작업이 완료되면, 다시 ADS interface에 의해서 정의된 DiscoveryResponse로 다시 변환합니다. 그리고 해당 정보를 Client에게 반환합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;정리하자면, 사용자가 원하는 요청 정보를 분석하여 istio가 관리하고 있는 서비스 정보를 Generator를 통해 envoy가 이해할 수 있는 형태로 변환한 다음 ADS interface가 요구하는 형태로 Wrapping 하여 전달하는 것이 사용자 요청 처리의 핵심 흐름이라고 볼 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;797&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciZ6Ay/btrMIcTxVKU/vWMkYxtYV7QRly9Aiuzv8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciZ6Ay/btrMIcTxVKU/vWMkYxtYV7QRly9Aiuzv8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciZ6Ay/btrMIcTxVKU/vWMkYxtYV7QRly9Aiuzv8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciZ6Ay%2FbtrMIcTxVKU%2FvWMkYxtYV7QRly9Aiuzv8k%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;480&quot; height=&quot;509&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;797&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이번에는 Service 생성/수정/삭제로 인해 Server에서 Client에게 데이터를 전달해야되는 상황에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2n4MR/btrMDShUk25/m1iqpa5roHluW7djMdQ570/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2n4MR/btrMDShUk25/m1iqpa5roHluW7djMdQ570/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2n4MR/btrMDShUk25/m1iqpa5roHluW7djMdQ570/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2n4MR%2FbtrMDShUk25%2Fm1iqpa5roHluW7djMdQ570%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;1405&quot; height=&quot;497&quot; data-origin-width=&quot;1405&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전 Push Queue 설명을 통해서 최종 통보는 Client Connection에 위치한 Push Channel로 데이터가 전달된다고 설명했습니다. 따라서 위 코드를 살펴보면 PushChannel에 데이터가 들어왔을 때는 빨간색 음영 부분이 감지가 될 것이며, 이를 토대로 pushConnection 메소드를 호출하여 후속 작업을 처리합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이때 pushConnection 내부 처리 과정은 다음과 같습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;1233&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uioww/btrME25uMdf/0FEa4XjJL06oaYwLqCVFfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uioww/btrME25uMdf/0FEa4XjJL06oaYwLqCVFfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uioww/btrME25uMdf/0FEa4XjJL06oaYwLqCVFfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuioww%2FbtrME25uMdf%2F0FEa4XjJL06oaYwLqCVFfk%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;1380&quot; height=&quot;1233&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;1233&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;PushRequest는 이미 PushChannel로 부터 받았으므로 envoy의 요청과는 다르게 메시지를 변환할 필요가 없습니다. 이후 Connection에 등록된 Watched Resource 정보를 모두 가져와서 Generator에 매칭되는 정보로 변환하고 ADS interface에 의해서 정의된 DiscoveryResponse로 다시 변환합니다. 그리고 해당 정보를 Client에게 반환합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;해당 과정을 통해 Server의 변경 내용을 Client에게 전파할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 정리&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;지금까지 XDS Server에 존재하는 주요 컴포넌트에 대해서 살펴봤습니다. 이번에는 지금까지 배운 내용을 요약해서 전체적인 관점에서 흐름을 간략하게 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2359&quot; data-origin-height=&quot;2802&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JgSCx/btrMFG89Ydn/RBaFVvVduEYNAIESumNqPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JgSCx/btrMFG89Ydn/RBaFVvVduEYNAIESumNqPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JgSCx/btrMFG89Ydn/RBaFVvVduEYNAIESumNqPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJgSCx%2FbtrMFG89Ydn%2FRBaFVvVduEYNAIESumNqPK%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;2359&quot; height=&quot;2802&quot; data-origin-width=&quot;2359&quot; data-origin-height=&quot;2802&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;1. 사용자로부터 접속 요청을 전달받으면 Connection을 생성하고 XDS Server 내에 위치한 AdsClients에 저장하여 사용자를 관리합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;2. informer에 등록한 Handler를 통해서 관심 대상 Resource를 등록하며, Callback으로 ServiceDiscovery내에 있는 queue로 전달됩니다. 해당 queue에서는 kinds에 등록된 GVK(Group Version Kind)에 매칭된 handler를 실행합니다. 대부분의 handler는 XDSServer.ConfigUpdate 메소드를 수행하고 해당 정보는 PushChannel로 전달됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;3. 내부에는 독립적으로 수행하는 debounce go routine이 존재하여 일정 시간동안 유입되는 메시지를 병합하는 작업을 수행하고 이후 메시지 전달을 위해 Push Queue에 데이터를 저장합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;4. Push Queue를 처리하는 로직은 또 다른 별도 go routine을 동작하는 doSubPushes에서 수행되며, 여기서는 Client Connection 정보를 참조하여 해당 Connection의 Push Channel로 데이터를 다시 전달합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;5. Push Channel로 전달된 데이터는 내부 Generator를 통해 envoy가 이해할 수 있는 형태로 데이터를 가공한다음 Client로 전달합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 마치며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번 포스팅에서는 istio pilot-discovery에서 가장 중요한 XDS Server에 대해서 살펴봤습니다. 다음 포스팅에서는 인증서 관리와 SDS Server 구조에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <category>Envoy</category>
      <category>Istio</category>
      <category>istio architecture</category>
      <category>istio controlplane</category>
      <category>istio internal</category>
      <category>istio 구조</category>
      <category>istio 아키텍처</category>
      <category>istiod</category>
      <category>pilot-discovery</category>
      <category>XDS</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/201</guid>
      <comments>https://cla9.tistory.com/201#entry201comment</comments>
      <pubDate>Tue, 3 Mar 2026 11:50:29 +0900</pubDate>
    </item>
    <item>
      <title>9. [istio-internals] istio - Service Discovery</title>
      <link>https://cla9.tistory.com/194</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;envoy-internals 시리즈를 통해 envoy 아키텍처에 대해서 자세하게 살펴봤습니다. 이번부터 진행되는 시리즈는 envoy 위에서 동작하는 istio가 어떻게 상호작용하는지 그리고 이를 위해 어떤 내부 구조를 지니고 동작 원리가 어떻게 되는지에 대해서 자세하게 살펴보고자 합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이번 포스팅에서는 envoy proxy를 활용하기 위한 istio의 역할과 아키텍처에 구조에 대해서 일부 다루어보겠습니다.&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. envoy proxy와 istio&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;517&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U1vha/btrKxPFOhbU/Mk4hn9x2knYw2mP6I6urb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U1vha/btrKxPFOhbU/Mk4hn9x2knYw2mP6I6urb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U1vha/btrKxPFOhbU/Mk4hn9x2knYw2mP6I6urb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU1vha%2FbtrKxPFOhbU%2FMk4hn9x2knYw2mP6I6urb1%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;240&quot; height=&quot;363&quot; data-origin-width=&quot;342&quot; data-origin-height=&quot;517&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이전 envoy 시리즈를 통해 envoy proxy는 service 앞단에 위치하고 요청에 대한 라우팅 정책, Circuit Breaker, mTLS 통신과 같은 인프라단에서 제공해주는 네트워크 기능을 대신 처리함을 확인했습니다. 그에 따라 Client가 Service를 호출할 때 다이렉트로 Service를 호출하는 것이 아니라 envoy proxy를 경유해서 upstream으로 트래픽이 전달됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;위와 같은 구조를 가졌을 때, 애플리케이션 배포할 때는 대개 Service만 배포하지 않고 envoy proxy와 Service간 연결 설정 정보가 포함된 하나의 형태로 패키징하여 배포합니다. 이러한 배포 형태를 사이드카 패턴이라고 부르며, Kubernetes를 통해 배포할 경우 1개의 Pod내에 Service와 Proxy Container를 엮어서 같이 배포하는 것이 일반적입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이때 Service가 단일로 구성되어있다면, envoy 설정 정보가 포함된 1개의 Pod를 잘 만들어서 배포할 경우 서비스 제공에 문제가 없을 것입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1361&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mgy3u/btrKuTpcKde/k8Vj71s2wUDcP9bFfktoG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mgy3u/btrKuTpcKde/k8Vj71s2wUDcP9bFfktoG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mgy3u/btrKuTpcKde/k8Vj71s2wUDcP9bFfktoG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fmgy3u%2FbtrKuTpcKde%2Fk8Vj71s2wUDcP9bFfktoG1%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;1361&quot; height=&quot;599&quot; data-origin-width=&quot;1361&quot; data-origin-height=&quot;599&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그렇다면 MSA 환경에서는 어떨까요?&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;MSA 환경에서는 위와 같이 시스템을 구축하기 위해 협력해야하는 Service가 다수가 될 것입니다. 그리고 이때 문제가 되는 것은 협력 해야할 Service가 지속적으로 추가되거나 변경되는 일이 자주 이루어진다는 것입니다. 즉 envoy proxy간 통신을 위해서는 서로간의 정보를 추적하고 이를 반영해야 서비스를 안전하게 제공할 수 있음을 의미합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;envoy 구조 포스팅에서 살펴봤듯이 envoy 내부 컴포넌트 변경을 위해서는 static과 dynamic 방법이 있음을 설명했습니다. 만약 Service가 지속적으로 추가되거나 변경될 때 static 방식으로 처리해야한다면, 위 예시와 같이 Service A 입장에서 Service B, Service C가 추가될 때마다 static resources 파일을 변경해서 재기동을 수행해야하는 문제점을 야기합니다. 따라서 이 경우에는 dynamic 방식을 활용하여 xDS를 통해 config 파일 변경 없이 기동 시점에 동적으로 Cluster 정보, Endpoint 정보 등을 변경하는 것이 바람직합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1361&quot; data-origin-height=&quot;823&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qovJv/btrKrrtwWVz/PzuQSFKXIiouyiYDhCcZU0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qovJv/btrKrrtwWVz/PzuQSFKXIiouyiYDhCcZU0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qovJv/btrKrrtwWVz/PzuQSFKXIiouyiYDhCcZU0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqovJv%2FbtrKrrtwWVz%2FPzuQSFKXIiouyiYDhCcZU0%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;1361&quot; height=&quot;823&quot; data-origin-width=&quot;1361&quot; data-origin-height=&quot;823&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇다면 envoy proxy에 xDS를 통해 올바르게 정보를 제공하기 위해서는 누군가는 Service의 등록/수정/삭제 등을 지속적으로 추적하고 관리하는 Control plane이 필요할 것입니다. 아마 눈치채셨겠지만, istio가 바로 해당 Control Plane의 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇다면 istio는 어떠한 원리로 Service의 등록/수정/삭제를 추적하고 관리하고, envoy proxy들과 Connection을 맺고 있으며 어떻게 데이터를 전달할까요?&lt;br&gt;&amp;nbsp;&lt;br&gt;이러한 목표를 달성하기 위해서 istio에서는 pilot 아키텍처를 제공하였습니다.&lt;br&gt;&amp;nbsp;&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;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OO19N/btrLk3ypQ9f/bH7lLtKDTCutkBjsI8TLak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OO19N/btrLk3ypQ9f/bH7lLtKDTCutkBjsI8TLak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OO19N/btrLk3ypQ9f/bH7lLtKDTCutkBjsI8TLak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOO19N%2FbtrLk3ypQ9f%2FbH7lLtKDTCutkBjsI8TLak%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;111&quot; data-origin-width=&quot;441&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Pilot은 말 그대로 조종사를 의미합니다. 마치 istio를 통해서 서비스와 endpoint 정보를 제공하여 올바르게 애플리케이션이 동작할 수 있도록 조종하는 역할을 수행합니다. pilot 아키텍처에는 이를 구현하기 위해 두 개의 컨테이너 이미지가 제공됩니다. 하나는 pilot-discovery 나머지는 pilot-agent입니다. 각각 이미지가 무슨 역할을 수행하는지 알아보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMRnQ7/btrLu8kabCB/ZwbTJbnMbdxJBEvZmTy5gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMRnQ7/btrLu8kabCB/ZwbTJbnMbdxJBEvZmTy5gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMRnQ7/btrLu8kabCB/ZwbTJbnMbdxJBEvZmTy5gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMRnQ7%2FbtrLu8kabCB%2FZwbTJbnMbdxJBEvZmTy5gk%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;540&quot; height=&quot;446&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;840&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;pilot-agent는 envoy proxy를 wrapping하고 있는 프로그램으로써 pilot-discovery로부터 Service 변경 사항을 통지받으면, 내부의 envoy proxy에게 이를 전파하여 envoy proxy 내부 설정을 변경하는 중계자 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;반면 pilot-discovery는 Control Plane의 역할인 등록/수정/삭제되는 Service를 감지하고 이를 연결된 모든 pilot-agent에게 전달하는 역할을 수행합니다. 다만 여기서 알아야 할 중요 포인트는 pilot-discovery 자체가 Service 라이프 사이클을 직접 추적하지 않는다는 점입니다. 즉 외부에 존재하는 Service Registry로부터 관련 이벤트를 전달받아 이를 전파하는 중계의 역할만을 담당합니다. 따라서 istio에서는 3가지 방법을 통해 이벤트를 전달받을 수 있는 창구를 제공합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1 MCP&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pm1v2/btrLLgqlMNw/cXk2kuHKa4PRIIU5Z3blPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pm1v2/btrLLgqlMNw/cXk2kuHKa4PRIIU5Z3blPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pm1v2/btrLLgqlMNw/cXk2kuHKa4PRIIU5Z3blPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpm1v2%2FbtrLLgqlMNw%2FcXk2kuHKa4PRIIU5Z3blPK%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;329&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;첫번째 방식은 MCP(Mesh Configuration Protocol)를 활용하는 방법입니다. Service Discovery 기능을 제공하는 Platform은 많습니다. 가령 위와같이 Consul, Eureka 등이 예입니다. 이때 개별 Platform 마다 Service Discovery 제공 format은 상이할 것이므로 만약 istio에서 각각의 Platform format을 지원해야한다면, 시스템 의존성이 강해질 것입니다. 이는 유지보수 측면에서 굉장히 어려움을 겪을 수 있음을 의미합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서 외부 의존성을 줄이기 위해서 istio 내부에서 사용할 수 있는 MCP를 만들고 Service Discovery에서는 MCP 포맷으로 데이터를 전달받아 처리하도록 설계되었습니다. 또한 개별 Service Registry에서 바로 MCP 포맷으로 보내줄 수 없는 경우에는 위와 같이 중간에 Adapter 역할을 수행하는 MCP Server를 통해 pilot-discovery에 전달하도록 구성할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2 File&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;두번째 방식은 File 방식입니다. 이는 local에 저장된 Configuration file을 주기적으로 읽어들여서 메모리에 캐싱하고 해당 정보를 토대로 Configuration을 수행하는 방식입니다. 해당 방식은 주로 istio를 테스트할 때 사용합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3 Kubernetes&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1361&quot; data-origin-height=&quot;991&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blrtfT/btrLKaYk5zT/ns1YBNQJTsZIDkzO9xxJCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blrtfT/btrLKaYk5zT/ns1YBNQJTsZIDkzO9xxJCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blrtfT/btrLKaYk5zT/ns1YBNQJTsZIDkzO9xxJCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblrtfT%2FbtrLKaYk5zT%2Fns1YBNQJTsZIDkzO9xxJCk%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;1361&quot; height=&quot;991&quot; data-origin-width=&quot;1361&quot; data-origin-height=&quot;991&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;세번째 방식은 Kubernetes 기반의 Service Discovery 방식입니다. Kubernetes 플랫폼을 사용한다면, 자체적으로 Service의 등록/삭제/수정과 관련된 이벤트를 제공받을 수 있습니다. 저를 포함하여 istio를 사용하는 대부분의 사용자는 kubernetes위에서 istio를 사용하는 경우가 많을 것이기 때문에 대개 Service Discovery Provider로써 Kubernetes를 사용할 것입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;본 포스팅 또한 위 세가지 방법 중 Kubernetes를 통한 Service 전달 방식에 대해서 자세하게 알아보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Informer&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이전 내용에서 확인했듯이 pilot-discovery는 외부에 존재하는 Service Registry로부터 Service의 변경 사항을 전달받아 이를 pilot-agent에게 전달하는 역할을 수행합니다. 그리고 이를 위한 인터페이스 방법으로 3가지가 있음을 확인했습니다. 그 중 kubernetes가 가장 보편적으로 사용되는데, istio는 어떻게 kubernetes로부터 이벤트를 전달받을 수 있을까요?&lt;br&gt;&amp;nbsp;&lt;br&gt;kuberetes 내부에는 여러가지 Controller 즉 &lt;span style=&quot;color: #000000;&quot;&gt;Scheduler, &lt;span style=&quot;background-color: #FFFFFF;&quot;&gt;Service controller 등이 있습니다. 그리고 해당 컴포넌트의 역할은 kube-apiserver로부터 관심 대상 Resource에 대해서 변경사항이 발생했을 때, Watch 메커니즘을 통해 Resource를 전달받고 후속 작업을 처리합니다. istio 또한 kubernetes로부터 Resource 변경에 대해 통지를 받아 후속처리하는 역할로써 kubernetes의 Custom Controller라고 볼 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #000000;&quot;&gt;kubernetes의 Controller는 내부적으로 client-go sdk를 통해서 Kube API Server와 통신하고 Watch 메커니즘을 제공합니다. 그리고 이를 위해서 내부에는 &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;informer&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;구조를 사용하고 있습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;403&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1jDen/btrLLh9W7si/KrRC5kAuCIkXoKRHJQ6hC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1jDen/btrLLh9W7si/KrRC5kAuCIkXoKRHJQ6hC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1jDen/btrLLh9W7si/KrRC5kAuCIkXoKRHJQ6hC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1jDen%2FbtrLLh9W7si%2FKrRC5kAuCIkXoKRHJQ6hC0%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;348&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;403&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;그렇다면 informer는 왜 사용할까요? 만약 Controller 내부에 있는 컴포넌트들이 Resource 정보를 얻기 위해서 개별적으로 kube-apiserver와 통신을 수행한다면, kube-apiserver 입장에서는 부하가 걸릴 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서 kubernetes 개발자들은 이러한 kube-apiserver의 API 호출 부담을 줄여주고자 Controller 내부에 캐싱 기능과 kube-apiserver와의 효율적인 통신을 위한 informer 메커니즘을 설계하였습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;641&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMuOf3/btrLNp63vCn/7li5sVjMohiVMg7bgaloX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMuOf3/btrLNp63vCn/7li5sVjMohiVMg7bgaloX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMuOf3/btrLNp63vCn/7li5sVjMohiVMg7bgaloX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMuOf3%2FbtrLNp63vCn%2F7li5sVjMohiVMg7bgaloX0%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;480&quot; height=&quot;509&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;641&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;해당 구조를 개략적으로 살펴보면 위와 같습니다. 가령 하나의 Program안에 여러개의 Controller가 존재하고 그 안에서 Resource 변화를 감지를 통지받아야한다면, informer를 통해 통지를 전달받고 싶은 대상을 등록합니다. 그리고 informer가 kube-apiserver에 요청을 전달하여 list 목록을 얻어와서 자신의 Local Thread Safe Cache에 저장합니다.&lt;br&gt;(※ 참고로 위 그림의 informer는 SharedInformer 구조를 의미하며, istio에서는 이를 위해 SharedInformerFactory를 사용합니다. 자세한 내용은 &lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;https://rudecamel.tistory.com/35&lt;/span&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt; &lt;/span&gt;&lt;/u&gt;&amp;nbsp;내용을 참고하시기 바랍니다.)&lt;br&gt;&amp;nbsp;&lt;br&gt;이후 Watch 메커니즘을 통해서 kube-apiserver로 부터 이벤트를 전달받으면, 해당 이벤트 내역을 개별 Controller들에게 Callback 하는 방식으로 이벤트를 처리합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1821&quot; data-origin-height=&quot;677&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5bNYR/btrLPnnArqz/mKbqkq1WqBI0ivNzdr5zpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5bNYR/btrLPnnArqz/mKbqkq1WqBI0ivNzdr5zpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5bNYR/btrLPnnArqz/mKbqkq1WqBI0ivNzdr5zpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5bNYR%2FbtrLPnnArqz%2FmKbqkq1WqBI0ivNzdr5zpK%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;1821&quot; height=&quot;677&quot; data-origin-width=&quot;1821&quot; data-origin-height=&quot;677&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;조금 더 자세히 살펴보면, Controller Application을 살펴보면 위와 같이 Client-go에서 제공하는 informer 영역과 Controller의 비즈니스 로직 두 부분으로 나눌 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이때 Controller가 변경 통지를 희망하는 GroupVersionResource를 informer에게 등록하고 향후 해당 이벤트가 전달되었을 때 처리를 희망하는 내부의 여러 Process들이 Controller에게 Add/Update/Delete Callback Function을 등록합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1821&quot; data-origin-height=&quot;677&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/crtGye/btrLKx0jFnt/KvSIjtkAPm5bUk4YeLVey0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/crtGye/btrLKx0jFnt/KvSIjtkAPm5bUk4YeLVey0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/crtGye/btrLKx0jFnt/KvSIjtkAPm5bUk4YeLVey0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcrtGye%2FbtrLKx0jFnt%2FKvSIjtkAPm5bUk4YeLVey0%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;1821&quot; height=&quot;677&quot; data-origin-width=&quot;1821&quot; data-origin-height=&quot;677&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이후 informer가 기동되면, 다음과 같은 과정을 거칩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;1. Reflector가 kube-apiserver에게 llist API를 통해서 가장 최신의 resourceVersion이 포함된 Resource 정보를 가져옵니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;2. 가져온 Resource 정보는 DeltaFIFO에 적재합니다. DeltaFIFO는 Queue 형태로 구성되어있으며, add, update, delete, list, pop 과 같은 연산을 제공합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;3. Informer는 DeltaFIFO에 적재된 데이터를 가져옵니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;4. Informer는 Indexer에게 전달된 데이터 내용을 저장하도록 명령합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;5. Informer에 등록된 Controller의 ResourceEventHandler Callback function을 호출합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;6. ResourceEventHandler Callback function에서 filtering 룰을 등록한게 있으면, 해당 이벤트는 제외하고 난 다음 관심 대상의 Resource 정보를 WorkQueue에 삽입합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;7. Controller는 WorkQueue로 부터 Resource 정보를 획득하여 자신이 보유한 Processor들에게 전파합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;8. Processor들은 전달받은 정보를 토대로 비즈니스 로직을 수행합니다. 만약 이 과정에서 Resource에 대한 정보가 필요하다면, Lister interface를 통해서 Indexer에게 Resource 정보를 요청할 수 있습니다. 요청받은 Indexer는 Local Cache로 부터 정보를 제공하여 Processor에서 불필요하게 kube-apiserver에 질의하는 것을 차단할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;9. 초기화 과정이 끝나면 Reflector는 Watch API를 요청하여 kube-apiserver로부터 Resource 정보의 변경이 있을 때마다 내용을 전달받습니다. 그리고 이전의 흐름 그대로 호출이 이어지면서, 후속 작업을 수행합니다. 이때 Watch API는 HTTP의 Chunked Transfer-Encoding 기법을 사용하여 Connection을 유지하도록 합니다. 이에 대한 설명은 아래 블로그를 참고하시기 바랍니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;https://mutpp.tistory.com/10&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;HTTP Chunked Message를 알아보자&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;# if 0 두달간의 노예 일상을 이제야 끝나고,, 너무 오랜만에 들여다본 블로그,, 개발자로 지내고 있지만,, 일하고 싶지는 않은 그런 삶,, 갑작스럽게 HTTP 프로토콜을 구현해야할 일이 생겨서, 규격&quot; data-og-host=&quot;mutpp.tistory.com&quot; data-og-source-url=&quot;https://mutpp.tistory.com/10&quot; data-og-image=&quot;https://blog.kakaocdn.net/dna/bcaQq3/hyPIMSvi4j/AAAAAAAAAAAAAAAAAAAAAGsNIgvPXQSKj2YraKoz42MM9MnRR6RHMhoCmHx5aOSR/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1774969199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=srxzR9b8SvCChJPzjatb47pECKI%3D&quot; data-og-url=&quot;https://mutpp.tistory.com/entry/HTTP-Chunked-Message%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90&quot;&gt;&lt;a href=&quot;https://mutpp.tistory.com/entry/HTTP-Chunked-Message%EB%A5%BC-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90&quot; target=&quot;_blank&quot; data-source-url=&quot;https://mutpp.tistory.com/10&quot;&gt;&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://blog.kakaocdn.net/dna/bcaQq3/hyPIMSvi4j/AAAAAAAAAAAAAAAAAAAAAGsNIgvPXQSKj2YraKoz42MM9MnRR6RHMhoCmHx5aOSR/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&amp;amp;expires=1774969199&amp;amp;allow_ip=&amp;amp;allow_referer=&amp;amp;signature=srxzR9b8SvCChJPzjatb47pECKI%3D')&quot;&gt; &lt;/div&gt;&lt;div class=&quot;og-text&quot;&gt;&lt;p class=&quot;og-title&quot;&gt;HTTP Chunked Message를 알아보자&lt;/p&gt;&lt;p class=&quot;og-desc&quot;&gt;# if 0 두달간의 노예 일상을 이제야 끝나고,, 너무 오랜만에 들여다본 블로그,, 개발자로 지내고 있지만,, 일하고 싶지는 않은 그런 삶,, 갑작스럽게 HTTP 프로토콜을 구현해야할 일이 생겨서, 규격&lt;/p&gt;&lt;p class=&quot;og-host&quot;&gt;mutpp.tistory.com&lt;/p&gt;&lt;/div&gt;&lt;/a&gt;&lt;/figure&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;지금까지 client-go에서 제공하는 informer 로직에 대해서 살펴봤습니다. istio를 학습하는데 있어 굳이 kubernetes의 watch 메커니즘을 알아야하나 싶을 수도 있습니다. 하지만 istio의 Service Discovery 동작에 있어서 client-go에서 제공해주는 컴포넌트와 강하게 결합되어 있습니다. 따라서 해당 구조를 이해하면 istio 컴포넌트의 동작 흐름을 쉽게 이해할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Istio Service Discovery 과정&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;지금까지 kubernetes가 제공하는 client-go의 informer 구조에 대해서 살펴봤습니다. 지금부터는 istio가 어떻게 informer와 연결되어 Service Discovery를 수행하는지에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;641&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjoV66/btrLQmWIxCk/kljudtc8OzsK8uEuj84PC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjoV66/btrLQmWIxCk/kljudtc8OzsK8uEuj84PC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjoV66/btrLQmWIxCk/kljudtc8OzsK8uEuj84PC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjoV66%2FbtrLQmWIxCk%2Fkljudtc8OzsK8uEuj84PC0%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;403&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;641&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;informer 구조에서 살펴본 것과 같이 informer에는 여러개의 Controller가 존재하고 해당 Controller가 요구하는 Resource 정보를 가지고 있다가 변화가 발생하면, Controller에게 전달함을 확인했습니다. 그에따라 istio에서는 위와 같이 여러개의 Controller를 정의하고 해당 event 정보를 수신하여 비즈니스 로직을 처리하도록 구성되어있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;1573&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biHGeP/btrMgm2TlzN/iWV81NuePf4BQolK28CTgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biHGeP/btrMgm2TlzN/iWV81NuePf4BQolK28CTgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biHGeP/btrMgm2TlzN/iWV81NuePf4BQolK28CTgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbiHGeP%2FbtrMgm2TlzN%2FiWV81NuePf4BQolK28CTgk%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;692&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;1573&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Control Plane에서는&amp;nbsp;pilot-agent와 통신을 위한 gRPC Server가 존재합니다. 해당 Server의 역할은 pilot-agent의 Connection을 저장하고 데이터 통신 시 해당 Server를 통해 pilot-agent에게 정보를 전달하는 역할을 수행합니다. 그리고 해당 gRPC Server를 담당하는 것은 pilot-discovery에 속한 XDS Server입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서 Controller의 Event가 감지하면 1차적으로 XDS Server에게 해당 내용이 전달되고 XDS Server에서는 전달받은 Resource를 envoy가 이해할 수 있는 XDS 형태로 변환하여 이를 ADS에 전달하여 연결된 모든 pilot-agent에게 정보를 전달하는 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마치며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번 포스팅에서는 envoy proxy에게 xDS 정보 제공을 위한 istio control-plane의 기능과 Service Discovery 과정에 대해서 개략적으로 살펴봤습니다. pilot-discovery의 주요 컴포넌트에 대해서는 추후 다루어보도록 하고 이번 포스팅에서는&lt;br&gt;kubernetes의 informer 구조에 대한 이해와 XDS Server를 통해 envoy proxy들에게 Service Discovery 기능을 제공한다는 점을 기억하면 좋을 것 같습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <category>Envoy</category>
      <category>envoy proxy</category>
      <category>envoy proxy 주입</category>
      <category>galley</category>
      <category>Istio</category>
      <category>istio proxy</category>
      <category>istio 구조</category>
      <category>istio 사이드카</category>
      <category>istio 아키텍처</category>
      <category>Istio란</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/194</guid>
      <comments>https://cla9.tistory.com/194#entry194comment</comments>
      <pubDate>Tue, 3 Mar 2026 11:50:07 +0900</pubDate>
    </item>
    <item>
      <title>8. [envoy-internals] Client 요청 전달 과정 이해하기 - 2</title>
      <link>https://cla9.tistory.com/214</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이번 포스팅은 지난 포스팅에 이어서 사용자가 Connection 연결을 등록한 이후에 실제 HTTP 요청을 전달했을 때 Upstream까지 어떻게 트래픽이 전달되는지에 대해서 살펴보고자 합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;그 과정에서 너무 지엽적인 부분은 다이어그램으로 표현하고 필요한 부분은 envoy 코드를 같이 보면서, 내부 동작 과정에 대한 이해를 집중적으로 해보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이번 포스팅은 특히 HttpConnectionManager와 깊은 연관이 있으므로 먼저 해당 내용을 소개한 &lt;u&gt;&lt;span style=&quot;color: #006DD7;&quot;&gt;이전 블로그 내용&lt;/span&gt;&lt;/u&gt;을 학습 후 보시는 것을 추천드립니다.&lt;br&gt;&lt;br&gt;(아직 포스팅 미완성상태인데 공개합니다ㅜ 하단에 코드만 첨부된 내용은 추후 보강하겠습니다)&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Client HTTP 요청 과정&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Client Connection 연결은 설정되었기 때문에 libevent로 Client가 HTTP 접속을 요청하면, 내부적으로 어디로 이벤트를 전달해야하는지 이미 알 수 있습니다. 이전 포스팅 내용에서 ServerConnectionImpl이 생성되면 libevent에 onFileEvent를 Callback 메소드로 등록한다고 설명했습니다. 따라서 이번 포스팅에서는 해당 지점부터 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;connection_impl.cc&lt;/p&gt;&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;void ConnectionImpl::onFileEvent(uint32_t events) {
  ScopeTrackerScopeState scope(this, this-&amp;gt;dispatcher_);
  ENVOY_CONN_LOG(trace, &quot;socket event: {}&quot;, *this, events);

  if (immediate_error_event_ == ConnectionEvent::LocalClose ||
      immediate_error_event_ == ConnectionEvent::RemoteClose) {
    if (bind_error_) {
      ENVOY_CONN_LOG(debug, &quot;raising bind error&quot;, *this);
      // Update stats here, rather than on bind failure, to give the caller a chance to
      // setConnectionStats.
      if (connection_stats_ &amp;amp;&amp;amp; connection_stats_-&amp;gt;bind_errors_) {
        connection_stats_-&amp;gt;bind_errors_-&amp;gt;inc();
      }
    } else {
      ENVOY_CONN_LOG(debug, &quot;raising immediate error&quot;, *this);
    }
    closeSocket(immediate_error_event_);
    return;
  }

  if (events &amp;amp; Event::FileReadyType::Closed) {
    // We never ask for both early close and read at the same time. If we are reading, we want to
    // consume all available data.
    ASSERT(!(events &amp;amp; Event::FileReadyType::Read));
    ENVOY_CONN_LOG(debug, &quot;remote early close&quot;, *this);
    closeSocket(ConnectionEvent::RemoteClose);
    return;
  }

  if (events &amp;amp; Event::FileReadyType::Write) {
    onWriteReady();
  }

  // It's possible for a write event callback to close the socket (which will cause fd_ to be -1).
  // In this case ignore read event processing.
  if (ioHandle().isOpen() &amp;amp;&amp;amp; (events &amp;amp; Event::FileReadyType::Read)) {
    onReadReady();
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;소켓에 Write/Read 이벤트가 발생하면, libevent에 의해서 onFileEvent를 호출됩니다. 이때 코드 내용을 살펴보면, Socket이 종료되었을 경우에는 연결을 해제하도록 작업을 수행하며, 그렇지 않은 경우에는 Socket 이벤트 타입이 Write인지 혹은 Read인지에 따라서 각기 다른 메소드를 호출하는 것을 확인할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;1043&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/64AQ6/btrRebJyrUI/6UWvwM35dH64HpCbKjPFUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/64AQ6/btrRebJyrUI/6UWvwM35dH64HpCbKjPFUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/64AQ6/btrRebJyrUI/6UWvwM35dH64HpCbKjPFUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F64AQ6%2FbtrRebJyrUI%2F6UWvwM35dH64HpCbKjPFUk%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;640&quot; height=&quot;624&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;1043&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 경우에는 HTTP 요청으로 인해 Socket에 데이터가 작성된 것이기 때문에 데이터를 읽기 위해서 onReadReady() 메소드가 호출됨을 가정하여 진행하겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;connection_impl.cc&lt;/p&gt;&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionImpl::onReadReady() {
  ENVOY_CONN_LOG(trace, &quot;read ready. dispatch_buffered_data={}&quot;, *this,
                 static_cast&amp;lt;int&amp;gt;(dispatch_buffered_data_));
  const bool latched_dispatch_buffered_data = dispatch_buffered_data_;
  dispatch_buffered_data_ = false;

  ASSERT(!connecting_);

  // We get here while read disabled in two ways.
  // 1) There was a call to setTransportSocketIsReadable(), for example if a raw buffer socket ceded
  //    due to shouldDrainReadBuffer(). In this case we defer the event until the socket is read
  //    enabled.
  // 2) The consumer of connection data called readDisable(true), and instead of reading from the
  //    socket we simply need to dispatch already read data.
  if (read_disable_count_ != 0) {
    // Do not clear transport_wants_read_ when returning early; the early return skips the transport
    // socket doRead call.
    if (latched_dispatch_buffered_data &amp;amp;&amp;amp; filterChainWantsData()) {
      onRead(read_buffer_-&amp;gt;length());
    }
    return;
  }

  // Clear transport_wants_read_ just before the call to doRead. This is the only way to ensure that
  // the transport socket read resumption happens as requested; onReadReady() returns early without
  // reading from the transport if the read buffer is above high watermark at the start of the
  // method.
  transport_wants_read_ = false;
  IoResult result = transport_socket_-&amp;gt;doRead(*read_buffer_);
  uint64_t new_buffer_size = read_buffer_-&amp;gt;length();
  updateReadBufferStats(result.bytes_processed_, new_buffer_size);

  // If this connection doesn't have half-close semantics, translate end_stream into
  // a connection close.
  if ((!enable_half_close_ &amp;amp;&amp;amp; result.end_stream_read_)) {
    result.end_stream_read_ = false;
    result.action_ = PostIoAction::Close;
  }

  read_end_stream_ |= result.end_stream_read_;
  if (result.bytes_processed_ != 0 || result.end_stream_read_ ||
      (latched_dispatch_buffered_data &amp;amp;&amp;amp; read_buffer_-&amp;gt;length() &amp;gt; 0)) {
    // Skip onRead if no bytes were processed unless we explicitly want to force onRead for
    // buffered data. For instance, skip onRead if the connection was closed without producing
    // more data.
    onRead(new_buffer_size);
  }

  // The read callback may have already closed the connection.
  if (result.action_ == PostIoAction::Close || bothSidesHalfClosed()) {
    ENVOY_CONN_LOG(debug, &quot;remote close&quot;, *this);
    closeSocket(ConnectionEvent::RemoteClose);
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;onReadReady() 메소드 내용은 위와 같습니다. 내용을 살펴보면, buffer에 읽을 데이터가 존재하면 해당 길이만큼 read_buffer에서 읽도록 onRead 메소드를 호출하는 것을 볼 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;connection_impl.cc&lt;/p&gt;&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void ConnectionImpl::onRead(uint64_t read_buffer_size) {
  ASSERT(dispatcher_.isThreadSafe());
  if (inDelayedClose() || !filterChainWantsData()) {
    return;
  }
  ASSERT(ioHandle().isOpen());

  if (read_buffer_size == 0 &amp;amp;&amp;amp; !read_end_stream_) {
    return;
  }

  if (read_end_stream_) {
    // read() on a raw socket will repeatedly return 0 (EOF) once EOF has
    // occurred, so filter out the repeats so that filters don't have
    // to handle repeats.
    //
    // I don't know of any cases where this actually happens (we should stop
    // reading the socket after EOF), but this check guards against any bugs
    // in ConnectionImpl or strangeness in the OS events (epoll, kqueue, etc)
    // and maintains the guarantee for filters.
    if (read_end_stream_raised_) {
      // No further data can be delivered after end_stream
      ASSERT(read_buffer_size == 0);
      return;
    }
    read_end_stream_raised_ = true;
  }

  filter_manager_.onRead();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNbr1Y/btrRcPUNcHw/SRJANuY9DONJQBaF3BV7vK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNbr1Y/btrRcPUNcHw/SRJANuY9DONJQBaF3BV7vK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNbr1Y/btrRcPUNcHw/SRJANuY9DONJQBaF3BV7vK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNbr1Y%2FbtrRcPUNcHw%2FSRJANuY9DONJQBaF3BV7vK%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;480&quot; height=&quot;319&quot; data-origin-width=&quot;1070&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 onRead에서는 filter_manager에 등록된 filterChains에서 데이터를 읽도록 위임합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;filter_manager_impl.cc&lt;/p&gt;&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void FilterManagerImpl::onRead() {
  ASSERT(!upstream_filters_.empty());
  onContinueReading(nullptr, connection_);
}
&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;filter_manager에서는 내부에 존재하는 upstream_filters_를 순차적으로 실행 시켜서 Filter Chains를 수행시킵니다. 이를 위해 onContinueReading 메소드를 호출합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;filter_manager_impl.cc&lt;/p&gt;&lt;pre class=&quot;livescript&quot;&gt;&lt;code&gt;void FilterManagerImpl::onContinueReading(ActiveReadFilter* filter,
                                          ReadBufferSource&amp;amp; buffer_source) {
  // Filter could return status == FilterStatus::StopIteration immediately, close the connection and
  // use callback to call this function.
  if (connection_.state() != Connection::State::Open) {
    return;
  }

  std::list&amp;lt;ActiveReadFilterPtr&amp;gt;::iterator entry;
  if (!filter) {
    connection_.streamInfo().addBytesReceived(buffer_source.getReadBuffer().buffer.length());
    entry = upstream_filters_.begin();
  } else {
    entry = std::next(filter-&amp;gt;entry());
  }

  for (; entry != upstream_filters_.end(); entry++) {
    if (!(*entry)-&amp;gt;filter_) {
      continue;
    }
    if (!(*entry)-&amp;gt;initialized_) {
      (*entry)-&amp;gt;initialized_ = true;
      FilterStatus status = (*entry)-&amp;gt;filter_-&amp;gt;onNewConnection();
      if (status == FilterStatus::StopIteration || connection_.state() != Connection::State::Open) {
        return;
      }
    }

    StreamBuffer read_buffer = buffer_source.getReadBuffer();
    if (read_buffer.buffer.length() &amp;gt; 0 || read_buffer.end_stream) {
      FilterStatus status = (*entry)-&amp;gt;filter_-&amp;gt;onData(read_buffer.buffer, read_buffer.end_stream);
      if (status == FilterStatus::StopIteration || connection_.state() != Connection::State::Open) {
        return;
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;onContinueReading 메소드는 먼저 read buffer에 입력된 값 만큼을 fetch하는 과정을 수행합니다. 이후 자신이 보유하고 있는 upstream_filters를 순회하면서 사용자 요청 처리를 각각의 필터에 위임합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;참고로 해당 메소드는 이전 포스팅에서 Read Filter 등록이 모두 완료된 이후 &lt;b&gt;&lt;span style=&quot;color: #EE2323;&quot;&gt;initializeReadFilters()&lt;/span&gt; &lt;/b&gt;실행 과정에서도 호출된 적이 있습니다. 그 당시에는 초기화를 목적으로 onNewConnection()이 수행되었는데, 지금은 초기화가 모두완료된 이후이기 때문에 Filter를 순회하면서 onData를 호출하는 것이 이전과는 다른 차이점입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1647&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wYKo1/btrReE5OTMf/ZLTJSBQd2M4kCYDawc3n1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wYKo1/btrReE5OTMf/ZLTJSBQd2M4kCYDawc3n1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wYKo1/btrReE5OTMf/ZLTJSBQd2M4kCYDawc3n1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwYKo1%2FbtrReE5OTMf%2FZLTJSBQd2M4kCYDawc3n1K%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;640&quot; height=&quot;262&quot; data-origin-width=&quot;1647&quot; data-origin-height=&quot;673&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;본 포스팅에서는 HTTP 요청과 관련하여 처리됨을 가정하였으므로, filterChains에 등록된 filter 중HttpConnectionManager 필터의 onData가 호출될 것입니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. HttpConnectionManager&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2722&quot; data-origin-height=&quot;1687&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lDpKM/btr8kJKKM6K/Qpmk5SI04OeWs3Vq0w8y7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lDpKM/btr8kJKKM6K/Qpmk5SI04OeWs3Vq0w8y7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lDpKM/btr8kJKKM6K/Qpmk5SI04OeWs3Vq0w8y7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlDpKM%2Fbtr8kJKKM6K%2FQpmk5SI04OeWs3Vq0w8y7K%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;2722&quot; height=&quot;1687&quot; data-origin-width=&quot;2722&quot; data-origin-height=&quot;1687&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;HttpConnectionManager의 onData 메소드가 호출되면, 가장 먼저 수행하는 일은 codec을 생성하는 작업입니다. 해당 내용은 HttpConnectionManager 관련 포스팅에서도 다루었던 내용이며, 본 포스팅에서는 Http 1.1 요청임을 가정하므로 Http1::ServerConnectionImpl 인스턴스가 반환될 것입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;해당 과정을 코드와 함께 살펴보면 다음과 같습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;1. ServerConnectionImpl에서 filter_manager에게 upstream_filters를 호출하라고 지정하면, 내부에 등록된 HttpConnectionManager의 onData가 호출됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;connection_manager_impl.cc&lt;/p&gt;&lt;pre class=&quot;lasso&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance&amp;amp; data, bool) {
  if (!codec_) {
    // Http3 codec should have been instantiated by now.
    createCodec(data);
  }

  ...(후략)...
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;2. HttpConnectionManager에서는 먼저 codec_ 생성여부를 판별한 다음 codec_이 만들어진 적이 없으면 이를 생성합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::createCodec(Buffer::Instance&amp;amp; data) {
&amp;nbsp;&amp;nbsp;ASSERT(!codec_);
&amp;nbsp;&amp;nbsp;codec_ = config_.createCodec(read_callbacks_-&amp;gt;connection(), data, *this, overload_manager_);

&amp;nbsp;&amp;nbsp;...(중략)...
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이때 내부적으로는 config에 지정된 createCodec 메소드를 다시 재 호출함으로써 codec 할당이 이루어집니다,&lt;br&gt;&amp;nbsp;&lt;br&gt;config.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Http::ServerConnectionPtr HttpConnectionManagerConfig::createCodec(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Network::Connection&amp;amp; connection, const Buffer::Instance&amp;amp; data,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Http::ServerConnectionCallbacks&amp;amp; callbacks, Server::OverloadManager&amp;amp; overload_manager) {
&amp;nbsp;&amp;nbsp;switch (codec_type_) {
&amp;nbsp;&amp;nbsp;case CodecType::HTTP1:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return std::make_unique&amp;lt;Http::Http1::ServerConnectionImpl&amp;gt;(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection, Http::Http1::CodecStats::atomicGet(http1_codec_stats_, context_.scope()),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks, http1_settings_, maxRequestHeadersKb(), maxRequestHeadersCount(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headersWithUnderscoresAction(), overload_manager);
&amp;nbsp;&amp;nbsp;case CodecType::HTTP2:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return std::make_unique&amp;lt;Http::Http2::ServerConnectionImpl&amp;gt;(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection, callbacks,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Http::Http2::CodecStats::atomicGet(http2_codec_stats_, context_.scope()),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;context_.api().randomGenerator(), http2_options_, maxRequestHeadersKb(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;maxRequestHeadersCount(), headersWithUnderscoresAction(), overload_manager);
&amp;nbsp;&amp;nbsp;case CodecType::HTTP3:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Config::Utility::getAndCheckFactoryByName&amp;lt;QuicHttpServerConnectionFactory&amp;gt;(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;quic.http_server_connection.default&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;.createQuicHttpServerConnectionImpl(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection, callbacks,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Http::Http3::CodecStats::atomicGet(http3_codec_stats_, context_.scope()),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;http3_options_, maxRequestHeadersKb(), maxRequestHeadersCount(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headersWithUnderscoresAction());
&amp;nbsp;&amp;nbsp;case CodecType::AUTO:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Http::ConnectionManagerUtility::autoCreateCodec(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection, data, callbacks, context_.scope(), context_.api().randomGenerator(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;http1_codec_stats_, http2_codec_stats_, http1_settings_, http2_options_,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;maxRequestHeadersKb(), maxRequestHeadersCount(), headersWithUnderscoresAction(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;overload_manager);
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;PANIC_DUE_TO_CORRUPT_ENUM;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;생성 과정을 살펴보면, 연결된 Protocol 종류에 따라서 Codec이 결정되는 것을 볼 수 있습니다. 본 포스팅에서는 Http 1.1 통신을 가정하여 분석함으로 Http1::ServerConnectionImpl이 생성됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;ServerConnectionImpl::ServerConnectionImpl(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Network::Connection&amp;amp; connection, CodecStats&amp;amp; stats, ServerConnectionCallbacks&amp;amp; callbacks,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const Http1Settings&amp;amp; settings, uint32_t max_request_headers_kb,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const uint32_t max_request_headers_count,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers_with_underscores_action,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Server::OverloadManager&amp;amp; overload_manager)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;: ConnectionImpl(connection, stats, settings, MessageType::Request, max_request_headers_kb,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; max_request_headers_count),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_(callbacks),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response_buffer_releasor_([this](const Buffer::OwnedBufferFragmentImpl* fragment) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;releaseOutboundResponse(fragment);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;owned_output_buffer_(connection.dispatcher().getWatermarkFactory().createBuffer(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[&amp;amp;]() -&amp;gt; void { this-&amp;gt;onBelowLowWatermark(); },
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[&amp;amp;]() -&amp;gt; void { this-&amp;gt;onAboveHighWatermark(); },
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[]() -&amp;gt; void { /* TODO(adisuissa): handle overflow watermark */ })),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers_with_underscores_action_(headers_with_underscores_action),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;abort_dispatch_(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;overload_manager.getLoadShedPoint(&quot;envoy.load_shed_points.http1_server_abort_dispatch&quot;)) {
&amp;nbsp;&amp;nbsp;owned_output_buffer_-&amp;gt;setWatermarks(connection.bufferLimit());
&amp;nbsp;&amp;nbsp;// Inform parent
&amp;nbsp;&amp;nbsp;output_buffer_ = owned_output_buffer_.get();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;ServerConnectionImpl 인스턴스 생성 과정을 살펴보면, 내부적으로 데이터를 읽고 쓰기 위한 Buffer를 생성받으며, 그밖에 ConnectionImpl 생성자를 호출하는 것을 볼 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;3. &lt;span style=&quot;color: #333333;&quot;&gt;ConnectionImpl&amp;nbsp;&lt;/span&gt;생성자 호출과정에서 사용자의 요청을 분석할 Parser를 등록합니다. 이때 기본적으로는 LegacyHttpParserImpl이 parser_ 로써 등록됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;ConnectionImpl::ConnectionImpl(Network::Connection&amp;amp; connection, CodecStats&amp;amp; stats,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; const Http1Settings&amp;amp; settings, MessageType type,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; uint32_t max_headers_kb, const uint32_t max_headers_count)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;: connection_(connection), stats_(stats), codec_settings_(settings),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;encode_only_header_key_formatter_(encodeOnlyFormatterFromSettings(settings)),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;processing_trailers_(false), handling_upgrade_(false), reset_stream_called_(false),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;deferred_end_stream_headers_(false), dispatching_(false), max_headers_kb_(max_headers_kb),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;max_headers_count_(max_headers_count) {
&amp;nbsp;&amp;nbsp;if (codec_settings_.use_balsa_parser_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parser_ = std::make_unique&amp;lt;BalsaParser&amp;gt;(type, this, max_headers_kb_ * 1024, enableTrailers(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;codec_settings_.allow_custom_methods_);
&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parser_ = std::make_unique&amp;lt;LegacyHttpParserImpl&amp;gt;(type, this);
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;참고로 config 설정에 use_balsa_parser가 지정되어있다면, BalsaParser가 지정됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;connection_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance&amp;amp; data, bool) {
&amp;nbsp;&amp;nbsp;if (!codec_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Http3 codec should have been instantiated by now.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;createCodec(data);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;bool redispatch;
&amp;nbsp;&amp;nbsp;do {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;redispatch = false;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const Status status = codec_-&amp;gt;dispatch(data);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (isBufferFloodError(status) || isInboundFramesWithEmptyPayloadError(status)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;handleCodecError(status.message());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Network::FilterStatus::StopIteration;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else if (isCodecProtocolError(status)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stats_.named_.downstream_cx_protocol_error_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;handleCodecError(status.message());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Network::FilterStatus::StopIteration;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(status.ok());

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Processing incoming data may release outbound data so check for closure here as well.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checkForDeferredClose(false);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// The HTTP/1 codec will pause dispatch after a single message is complete. We want to
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// either redispatch if there are no streams and we have more data. If we have a single
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// complete non-WebSocket stream but have not responded yet we will pause socket reads
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// to apply back pressure.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (codec_-&amp;gt;protocol() &amp;lt; Protocol::Http2) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (read_callbacks_-&amp;gt;connection().state() == Network::Connection::State::Open &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;data.length() &amp;gt; 0 &amp;amp;&amp;amp; streams_.empty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;redispatch = true;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;} while (redispatch);

&amp;nbsp;&amp;nbsp;if (!read_callbacks_-&amp;gt;connection().streamInfo().protocol()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;read_callbacks_-&amp;gt;connection().streamInfo().protocol(codec_-&amp;gt;protocol());
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;return Network::FilterStatus::StopIteration;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;지금까지 3가지 단계에 걸쳐서 ServerConnectionImpl 생성과 더불어 실제 요청을 처리하는 codec 또한 생성됨을 확인했습니다. 여기까지 완료되면, 그 다음 작업은 위 코드와 같이 codec_ 에게 사용자가 요청한 데이터를 넘겨줌으로써 dispatch 하도록 처리를 위임하고 Codec 내부에서는 다시 parser_를 통해 사용자의 데이터를 분석하고 정제하는 과정을 거칩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이 과정에 대해서 보다 자세하게 이해하기 위해 다이어그램과 코드를 통해 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2766&quot; data-origin-height=&quot;2000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vggsr/btsdOKR8QFT/eASJWnrJejgO1a6ga2Hvs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vggsr/btsdOKR8QFT/eASJWnrJejgO1a6ga2Hvs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vggsr/btsdOKR8QFT/eASJWnrJejgO1a6ga2Hvs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvggsr%2FbtsdOKR8QFT%2FeASJWnrJejgO1a6ga2Hvs1%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;2766&quot; height=&quot;2000&quot; data-origin-width=&quot;2766&quot; data-origin-height=&quot;2000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;1. HttpConnectionManager의 onData 메소드 내에서 codec_인 Http1::ServerConnectionImpl에게 dispatch를 요청합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;connection_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance&amp;amp; data, bool) {
&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const Status status = codec_-&amp;gt;dispatch(data);
&amp;nbsp;&amp;nbsp;...(중략)...
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;2. ServerConnectionImpl은 내부에 존재하는 parser_ 프로퍼티를 통해서 데이터 분석을 요청합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Envoy::StatusOr&amp;lt;size_t&amp;gt; ConnectionImpl::dispatchSlice(const char* slice, size_t len) {
&amp;nbsp;&amp;nbsp;ASSERT(codec_status_.ok() &amp;amp;&amp;amp; dispatching_);
&amp;nbsp;&amp;nbsp;const size_t nread = parser_-&amp;gt;execute(slice, len);
&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;return nread;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;legacy_parser_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;size_t execute(const char* slice, int len) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return http_parser_execute(&amp;amp;parser_, &amp;amp;settings_, slice, len);
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;parser에게 execute 요청을 보내고나면, parser는 내부적으로 사용자 데이터를 전달받아 이를 분석하는 과정을 거칠 것입니다. 그리고 이전 포스팅을 통해 설명했듯이 사전에 정의된 ParserCallback 함수를 통해서 분석 중간중간의 결과물을 Callback을 통해 전달합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;3. Parser가 사용자의 데이터를 분석하면서 가장 먼저 onMessageBegin 콜백 함수가 실행됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;legacy_parser_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[](http_parser* parser) -&amp;gt; int {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onMessageBegin());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;4. 해당 콜백 함수를 전달받은 ServerConnectionImpl은 ActiveRequest 인스턴스를 생성합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Status ServerConnectionImpl::onMessageBeginBase() {
&amp;nbsp;&amp;nbsp;if (!resetStreamCalled()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(active_request_ == nullptr);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;active_request_ = std::make_unique&amp;lt;ActiveRequest&amp;gt;(*this, std::move(bytes_meter_before_stream_));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...(중략)...
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;return okStatus();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;5. ActiveRequest를 만든 이후에 ActiveStream을 새로 생성하기 위해 HttpConnectionManager에게 생성을 요청합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Status ServerConnectionImpl::onMessageBeginBase() {
&amp;nbsp;&amp;nbsp;if (!resetStreamCalled()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...(중략)...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;active_request_-&amp;gt;request_decoder_ = &amp;amp;callbacks_.newStream(active_request_-&amp;gt;response_encoder_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...(중략)...
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;return okStatus();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;6. HttpConnectionManager에서는 새로운 ActiveStream을 생성합니다. 이때 HttpConnectionManager가 보유한 filter_factories 정보 또한 같이 참조하여 ActiveStream 인스턴스 내에 filter_manager_를 구성합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;RequestDecoder&amp;amp; ConnectionManagerImpl::newStream(ResponseEncoder&amp;amp; response_encoder,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; bool is_internally_created) {
&amp;nbsp;&amp;nbsp;TRACE_EVENT(&quot;core&quot;, &quot;ConnectionManagerImpl::newStream&quot;);
&amp;nbsp;&amp;nbsp;if (connection_idle_timer_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_idle_timer_-&amp;gt;disableTimer();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;ENVOY_CONN_LOG(debug, &quot;new stream&quot;, read_callbacks_-&amp;gt;connection());

&amp;nbsp;&amp;nbsp;Buffer::BufferMemoryAccountSharedPtr downstream_stream_account =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response_encoder.getStream().account();

&amp;nbsp;&amp;nbsp;if (downstream_stream_account == nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Create account, wiring the stream to use it for tracking bytes.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// If tracking is disabled, the wiring becomes a NOP.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;auto&amp;amp; buffer_factory = read_callbacks_-&amp;gt;connection().dispatcher().getWatermarkFactory();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;downstream_stream_account = buffer_factory.createAccount(response_encoder.getStream());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response_encoder.getStream().setAccount(downstream_stream_account);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;ActiveStreamPtr new_stream(new ActiveStream(*this, response_encoder.getStream().bufferLimit(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::move(downstream_stream_account)));

&amp;nbsp;&amp;nbsp;accumulated_requests_++;
&amp;nbsp;&amp;nbsp;if (config_.maxRequestsPerConnection() &amp;gt; 0 &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;accumulated_requests_ &amp;gt;= config_.maxRequestsPerConnection()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (codec_-&amp;gt;protocol() &amp;lt; Protocol::Http2) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;new_stream-&amp;gt;state_.saw_connection_close_ = true;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Prevent erroneous debug log of closing due to incoming connection close header.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;drain_state_ = DrainState::Closing;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else if (drain_state_ == DrainState::NotDraining) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;startDrainSequence();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_CONN_LOG(debug, &quot;max requests per connection reached&quot;, read_callbacks_-&amp;gt;connection());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stats_.named_.downstream_cx_max_requests_reached_.inc();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;...(중략)...
&amp;nbsp;&amp;nbsp;return **streams_.begin();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이때 먼저, config에 지정된 max_requests_per_connection을 넘겼는지 확인합니다. 해당 작업은 하나의 Connection 내부에서 여러 Stream이 생성될 때 이를 제한하기 위한 용도로 사용됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;7. Stream 생성이 완료되면, HttpConnectionManager 내부에 존재하는 streams_ List에 추가하고 이를 반환합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;RequestDecoder&amp;amp; ConnectionManagerImpl::newStream(ResponseEncoder&amp;amp; response_encoder,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; bool is_internally_created) {
&amp;nbsp;&amp;nbsp;...(중략)...
&amp;nbsp;&amp;nbsp;LinkedList::moveIntoList(std::move(new_stream), streams_);
&amp;nbsp;&amp;nbsp;return **streams_.begin();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;8. Stream 인스턴스가 반환되면, 이를 ActiveRequest의 request_decoder_에 저장합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;위와 같은 8단계 과정을 거치게되면, 사용자 요청 처리를 위해 초기화 과정이 수행되는 것을 알 수 있습니다. 또한 그 과정에서 ActiveRequest와 Stream 요청 처리를 위한 ActiveStream이 내부적으로 생성되어 저장됨을 확인할 수 있습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;초기화 작업이 모두 완료되면, 이후에는 본격적으로 데이터 parsing 작업을 시작합니다. 해당 과정을 살펴보면 다음과 같습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLHfyY/btsdTPDPhtO/pZudF2Km9AAJwmBaiVG0gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLHfyY/btsdTPDPhtO/pZudF2Km9AAJwmBaiVG0gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLHfyY/btsdTPDPhtO/pZudF2Km9AAJwmBaiVG0gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLHfyY%2FbtsdTPDPhtO%2FpZudF2Km9AAJwmBaiVG0gk%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;2578&quot; height=&quot;1808&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;9. 분석 과정에서 먼저 원격지 호출 URL이 무엇인지를 parser가 분석하고 이에 대한 Event를 전파하기 위하여 onUrl 콜백 함수를 호출합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;legacy_parser_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[](http_parser* parser, const char* at, size_t length) -&amp;gt; int {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onUrl(at, length));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;10. ServerConnectionImpl은 해당 콜백 함수를 호출받으면, 내부에 존재하는 ActiveRequest의 request_url에 parsing된 값을 매핑합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;div&gt; 
 &lt;div&gt;
   codec_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1669777182863&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CallbackResult ConnectionImpl::onUrl(const char* data, size_t length) {
  return setAndCheckCallbackStatus(onUrlBase(data, length));
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   codec_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1669777216743&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Status ServerConnectionImpl::onUrlBase(const char* data, size_t length) {
  if (active_request_) {
    active_request_-&amp;gt;request_url_.append(data, length);

    RETURN_IF_ERROR(checkMaxHeadersSize());
  }

  return okStatus();
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   codec_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1669777316811&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Status ConnectionImpl::checkMaxHeadersSize() {
  const uint32_t total = getHeadersSize();
  if (total &amp;gt; (max_headers_kb_ * 1024)) {
    const absl::string_view header_type =
        processing_trailers_ ? Http1HeaderTypes::get().Trailers : Http1HeaderTypes::get().Headers;
    error_code_ = Http::Code::RequestHeaderFieldsTooLarge;
    RETURN_IF_ERROR(sendProtocolError(Http1ResponseCodeDetails::get().HeadersTooLarge));
    return codecProtocolError(
        absl::StrCat(&quot;http/1.1 protocol error: &quot;, header_type, &quot; size exceeds limit&quot;));
  }
  return okStatus();
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   또한 이 과정에서 현재까지 분석된 Header의 길이가 max_headers_kb에 지정된 값(기본 60)을 넘는지를 검증하는 작업 또한 진행합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1808&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NukZL/btsdPu2lAdz/L4XZN8pVmXFjLOWQLtUkSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NukZL/btsdPu2lAdz/L4XZN8pVmXFjLOWQLtUkSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NukZL/btsdPu2lAdz/L4XZN8pVmXFjLOWQLtUkSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNukZL%2FbtsdPu2lAdz%2FL4XZN8pVmXFjLOWQLtUkSk%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;2578&quot; height=&quot;1808&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1808&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   11. Url 분석 작업이 완료된 이후에는 본격적으로 입력 값 Header를 분석하여 채우기 시작합니다. 이때 Header의 명을 Parsing 했을 경우에는 onHeaderField 콜백을 호출하고, Header의 값을 분석했을 때는 onHeaderValue 콜백을 호출합니다.&amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   12. Header 명과 값을 모두 추출한 경우에는 해당 값을 headers_or_trailers_ 라는 Map에 입력합니다. 해당 자료구조는 Header의 필드명을 Key로 값을 Value로 하는 Map 구조로써 향후 Http 정보를 요청할 때 활용됩니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   codec_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1680773412482&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Status ConnectionImpl::completeCurrentHeader() {
  ASSERT(dispatching_);
  ENVOY_CONN_LOG(trace, &quot;completed header: key={} value={}&quot;, connection_,
                 current_header_field_.getStringView(), current_header_value_.getStringView());
  auto&amp;amp; headers_or_trailers = headersOrTrailers();

  // Account for &quot;:&quot; and &quot;\r\n&quot; bytes between the header key value pair.
  getBytesMeter().addHeaderBytesReceived(CRLF_SIZE + 1);

  // TODO(10646): Switch to use HeaderUtility::checkHeaderNameForUnderscores().
  RETURN_IF_ERROR(checkHeaderNameForUnderscores());
  if (!current_header_field_.empty()) {
    // Strip trailing whitespace of the current header value if any. Leading whitespace was trimmed
    // in ConnectionImpl::onHeaderValue. http_parser does not strip leading or trailing whitespace
    // as the spec requires: https://tools.ietf.org/html/rfc7230#section-3.2.4
    current_header_value_.rtrim();

    // If there is a stateful formatter installed, remember the original header key before
    // converting to lower case.
    auto formatter = headers_or_trailers.formatter();
    if (formatter.has_value()) {
      formatter-&amp;gt;processKey(current_header_field_.getStringView());
    }
    current_header_field_.inlineTransform([](char c) { return absl::ascii_tolower(c); });

    headers_or_trailers.addViaMove(std::move(current_header_field_),
                                   std::move(current_header_value_));
  }

  // Check if the number of headers exceeds the limit.
  if (headers_or_trailers.size() &amp;gt; max_headers_count_) {
    error_code_ = Http::Code::RequestHeaderFieldsTooLarge;
    RETURN_IF_ERROR(sendProtocolError(Http1ResponseCodeDetails::get().TooManyHeaders));
    const absl::string_view header_type =
        processing_trailers_ ? Http1HeaderTypes::get().Trailers : Http1HeaderTypes::get().Headers;
    return codecProtocolError(
        absl::StrCat(&quot;http/1.1 protocol error: &quot;, header_type, &quot; count exceeds limit&quot;));
  }

  header_parsing_state_ = HeaderParsingState::Field;
  ASSERT(current_header_field_.empty());
  ASSERT(current_header_value_.empty());
  return okStatus();
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2789&quot; data-origin-height=&quot;1993&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG96ne/btsdQMnwSZC/zD4sresAJLy7AETGfF4M70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG96ne/btsdQMnwSZC/zD4sresAJLy7AETGfF4M70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG96ne/btsdQMnwSZC/zD4sresAJLy7AETGfF4M70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG96ne%2FbtsdQMnwSZC%2FzD4sresAJLy7AETGfF4M70%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;2789&quot; height=&quot;1993&quot; data-origin-width=&quot;2789&quot; data-origin-height=&quot;1993&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   이번에는 Header 분석이 모두 완료되었을 때 흐름에 대해서 살펴보겠습니다. 해당 과정은 이전에 살펴봤던 Cluster Manager에서의 역할과 Http filter 중 하나인 Router Filter 등이 사용되므로 해당 과정을 이해하기 위해서는 이전 시리즈의 내용에 대한 개념 숙지가 반드시 필요합니다.&amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   13. Parser로 부터 Header 분석이 완료되면, onHeaderComplete 콜백 함수를 호출합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   legacy_parser_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1685250675011&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        [](http_parser* parser) -&amp;gt; int {
          auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
          return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onHeadersComplete());
        },&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   14. 해당 콜백 함수는 ServerConnectionImpl에 지정된 onHeaderCompleteImpl 메서드를 호출합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   codec_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1685250707575&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CallbackResult ConnectionImpl::onHeadersComplete() {
  return setAndCheckCallbackStatusOr(onHeadersCompleteImpl());
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   15. Header를 분석하는 과정에서 저장되었던 headers_or_trailers_ Map에서 RequestMap을 추출합니다. 해당 자료구조는 Http 요청에 필요한 헤더 및 요청에 필요한 속성이 포함된 자료구조입니다.&amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   codec_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1685250760010&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;StatusOr&amp;lt;CallbackResult&amp;gt; ConnectionImpl::onHeadersCompleteImpl() {
  ...(중략)...
  RequestOrResponseHeaderMap&amp;amp; request_or_response_headers = requestOrResponseHeaders();
  ...(중략)...
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   codec_impl.h 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1685251750511&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  RequestOrResponseHeaderMap&amp;amp; requestOrResponseHeaders() override {
    return *absl::get&amp;lt;RequestHeaderMapPtr&amp;gt;(headers_or_trailers_);
  }&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   16. RequestMap에 기존 ActiveRequest에서 보관중이던 request_url을 추출하여 Upstream 대상 Url을 설정합니다. 이 과정에서 추가로 Method 타입 또한 지정합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;p data-ke-size=&quot;size16&quot;&gt;codec_impl.cc&lt;/p&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1685252055102&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;StatusOr&amp;lt;CallbackResult&amp;gt; ConnectionImpl::onHeadersCompleteImpl() {
  ...(중략)...

  auto statusor = onHeadersCompleteBase();
  ...(중략)...
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;p data-ke-size=&quot;size16&quot;&gt;codec_impl.cc&lt;/p&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1685252168383&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Envoy::StatusOr&amp;lt;CallbackResult&amp;gt; ServerConnectionImpl::onHeadersCompleteBase() {
  // Handle the case where response happens prior to request complete. It's up to upper layer code
  // to disconnect the connection but we shouldn't fire any more events since it doesn't make
  // sense.
  if (active_request_) {
    auto&amp;amp; headers = absl::get&amp;lt;RequestHeaderMapPtr&amp;gt;(headers_or_trailers_);
    ...(중략)...

    // Inform the response encoder about any HEAD method, so it can set content
    // length and transfer encoding headers correctly.
    const Http::HeaderValues&amp;amp; header_values = Http::Headers::get();
    active_request_-&amp;gt;response_encoder_.setIsResponseToHeadRequest(parser_-&amp;gt;methodName() ==
                                                                  header_values.MethodValues.Head);
    active_request_-&amp;gt;response_encoder_.setIsResponseToConnectRequest(
        parser_-&amp;gt;methodName() == header_values.MethodValues.Connect);

    RETURN_IF_ERROR(handlePath(*headers, parser_-&amp;gt;methodName()));
    
    ASSERT(active_request_-&amp;gt;request_url_.empty());
    headers-&amp;gt;setMethod(parser_-&amp;gt;methodName());
    ...(중략)...
  }

  return CallbackResult::Success;
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   codec_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1685252295602&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Status ServerConnectionImpl::handlePath(RequestHeaderMap&amp;amp; headers, absl::string_view method) {
  const Http::HeaderValues&amp;amp; header_values = Http::Headers::get();
  HeaderString path(header_values.Path);

  bool is_connect = (method == header_values.MethodValues.Connect);

  // The url is relative or a wildcard when the method is OPTIONS. Nothing to do here.
  if (!is_connect &amp;amp;&amp;amp; !active_request_-&amp;gt;request_url_.getStringView().empty() &amp;amp;&amp;amp;
      (active_request_-&amp;gt;request_url_.getStringView()[0] == '/' ||
       (method == header_values.MethodValues.Options &amp;amp;&amp;amp;
        active_request_-&amp;gt;request_url_.getStringView()[0] == '*'))) {
    headers.addViaMove(std::move(path), std::move(active_request_-&amp;gt;request_url_));
    return okStatus();
  }

  // If absolute_urls and/or connect are not going be handled, copy the url and return.
  // This forces the behavior to be backwards compatible with the old codec behavior.
  // CONNECT &quot;urls&quot; are actually host:port so look like absolute URLs to the above checks.
  // Absolute URLS in CONNECT requests will be rejected below by the URL class validation.
  if (!codec_settings_.allow_absolute_url_ &amp;amp;&amp;amp; !is_connect) {
    headers.addViaMove(std::move(path), std::move(active_request_-&amp;gt;request_url_));
    return okStatus();
  }

  Utility::Url absolute_url;
  if (!absolute_url.initialize(active_request_-&amp;gt;request_url_.getStringView(), is_connect)) {
    RETURN_IF_ERROR(sendProtocolError(Http1ResponseCodeDetails::get().InvalidUrl));
    return codecProtocolError(&quot;http/1.1 protocol error: invalid url in request line&quot;);
  }
  // RFC7230#5.7
  // When a proxy receives a request with an absolute-form of
  // request-target, the proxy MUST ignore the received Host header field
  // (if any) and instead replace it with the host information of the
  // request-target. A proxy that forwards such a request MUST generate a
  // new Host field-value based on the received request-target rather than
  // forward the received Host field-value.
  headers.setHost(absolute_url.hostAndPort());
  // Add the scheme and validate to ensure no https://
  // requests are accepted over unencrypted connections by front-line Envoys.
  if (!is_connect) {
    headers.setScheme(absolute_url.scheme());
    if (!HeaderUtility::schemeIsValid(absolute_url.scheme())) {
      RETURN_IF_ERROR(sendProtocolError(Http1ResponseCodeDetails::get().InvalidScheme));
      return codecProtocolError(&quot;http/1.1 protocol error: invalid scheme&quot;);
    }
    if (codec_settings_.validate_scheme_ &amp;amp;&amp;amp;
        absolute_url.scheme() == header_values.SchemeValues.Https &amp;amp;&amp;amp; !connection().ssl()) {
      error_code_ = Http::Code::Forbidden;
      RETURN_IF_ERROR(sendProtocolError(Http1ResponseCodeDetails::get().HttpsInPlaintext));
      return codecProtocolError(&quot;http/1.1 protocol error: https in the clear&quot;);
    }
  }

  if (!absolute_url.pathAndQueryParams().empty()) {
    headers.setPath(absolute_url.pathAndQueryParams());
  }
  active_request_-&amp;gt;request_url_.clear();
  return okStatus();
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   17. ActiveRequest에 지정된 request_decoder_ 즉 ActiveStream에게 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Header 처리를 위임합니다. 이 과정에서 RequestMap을 인자로 전달합니다.&lt;/span&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   codec_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1685252431482&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Envoy::StatusOr&amp;lt;CallbackResult&amp;gt; ServerConnectionImpl::onHeadersCompleteBase() {
    ...(중략)...
    if (parser_-&amp;gt;isChunked() ||
        (parser_-&amp;gt;contentLength().has_value() &amp;amp;&amp;amp; parser_-&amp;gt;contentLength().value() &amp;gt; 0) ||
        handling_upgrade_) {
      active_request_-&amp;gt;request_decoder_-&amp;gt;decodeHeaders(std::move(headers), false);

      // If the connection has been closed (or is closing) after decoding headers, pause the parser
      // so we return control to the caller.
      if (connection_.state() != Network::Connection::State::Open) {
        return parser_-&amp;gt;pause();
      }
    } else {
      deferred_end_stream_headers_ = true;
    }
  }

  return CallbackResult::Success;
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;18. ActiveStream에서는 내부에 존재하는 filter_manager_ 에게 먼저 Downstream의 주소와 전달받은 RequestMap을 토대로 만든 header 정보를 매핑합니다. 매핑이 완료되면, 그 이후에는 filter_manager_가 보유한 filter chain을 생성합니다.&lt;/span&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   참고로 filter_manager_는 ActiveStream이 생성되는 당시에 HttpConnectionManager로 부터 filter_factories_ 의 정보를 전달받았으며, filter chain 생성을 요청받으면, filter_factories_가 보유한 factory 콜백 메소드를 실행시켜 filter를 내부에 생성하는 작업을 수행합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   filter 생성이 모두 완료되면, 해당 filter chain을 순회하면서 header 정보 처리를 위임합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   19. Http filter 종류 중 하나인 Router filter가 filter chain 가장 마지막에 호출됩니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   20. Router filter에서는 Upstream 대상 Host 정보와 Connection Pool을 획득하기 위해 우선 ClusterEntry 정보를 ClusterManager에게 요청합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   21. Cluster Manager는 자신이 보유하고 있는 thread_local_clusters_ 에서 Router가 요청한 Cluster 정보를 찾습니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   22. Cluster Manager로 부터 ThreadLocalCluster를 찾아서 Router로 반환합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   23. Router에서는 Cluster내 존재하는 host를 통해 Connection 연결을 수행해야합니다. 따라서 전달받은 ThreadLocalCluster를 통해서 Connection Pool 할당을 요청합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   24. 이를 전달받은 ThreadLocalCluster(Cluster Entry)는 Cluster Manager에게 요청하여 host_http_conn_pool_map에 할당 받은 Connection Pool이 존재하는지를 확인합니다. 만약 존재한다면, 해당 Connection Pool Map에 있는 Container에 접근합니다. 반면, 존재하지 않는다면 새로운 Container를 생성하여 해당 Map에 추가합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   25. Container 내부에는 Cluster 별로 Connection을 관리하는 Pool이 존재합니다. 여기에서 Pool 존재여부를 최종적으로 확인합니다. 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;26. 최초 접근시에는 Pool이 생성되지 않았을 것이기 때문에 먼저 Cluster가 Connection Pool을 생성이 가능한지 설정된 Resource Limit 설정 값을 살펴봅니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;27. Resource Limit 설정 값에 이상이 없어 신규 Pool 생성이 가능하면, ClusterManager에게 Pool 할당을 요청합니다. 이때 ClusterManager는 Client가 요구하는 프로토콜이 무엇인지 확인한 다음 해당 요청을 처리할 수 있는 Connection을 생성하여 반환합니다. 그리고 생성된 Pool을 Container가 보유하고 있는 active_pools_에 삽입합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;28. 생성된 Pool을 ThreadLocalCluster(Cluster Entry)에 반환합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;29. Pool을 Router에게 반환합니다. Router는 전달받은 Pool 정보를 기반으로 Upstream Connection을 생성하기 위해 UpstreamRequest를 생성합니다. 그리고 생성된 UpstreamRequest를 자신이 보유하고 있는 upstream_requests_ 리스트에 추가합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;여기서 UpstreamRequest는 전달받은 Connection Pool을 기반으로 Upstream에 연결을 요청하기 위해 설정된 자료구조로써 해당 자료구조를 통해 상위 Host와 통신을 수행할 수 있습니다. 해당 자료구조에 대해서 조금 더 살펴보면 다음과 같습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctFFQ2/btr91Rz98Gq/tesuQoA3pmvrcKMLAImH71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctFFQ2/btr91Rz98Gq/tesuQoA3pmvrcKMLAImH71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctFFQ2/btr91Rz98Gq/tesuQoA3pmvrcKMLAImH71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctFFQ2%2Fbtr91Rz98Gq%2FtesuQoA3pmvrcKMLAImH71%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;440&quot; height=&quot;350&quot; data-origin-width=&quot;662&quot; data-origin-height=&quot;527&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;UpstreamRequest 구조에서 핵심 속성은 위와 같습니다. 위 속성에 대해서 하나씩 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;먼저 conn_pool_은 UpstreamRequest를 관리하는 Connection Pool을 가르키는 포인터입니다. 해당 속성은 이전 ThreadLocalCluster를 통해서 전달받았던 Connection Pool을 가르키고 있습니다. 따라서 해당 Pool을 통해 사용자 요청 Stream을 매핑시킴으로써 데이터 송/수신 역할 처리를 담당할 수 있도록 가교 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2504&quot; data-origin-height=&quot;857&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B8VVP/btsdbKSH8wt/lSIZGWdIU4csfpaJkkjDB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B8VVP/btsdbKSH8wt/lSIZGWdIU4csfpaJkkjDB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B8VVP/btsdbKSH8wt/lSIZGWdIU4csfpaJkkjDB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB8VVP%2FbtsdbKSH8wt%2FlSIZGWdIU4csfpaJkkjDB1%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;2504&quot; height=&quot;857&quot; data-origin-width=&quot;2504&quot; data-origin-height=&quot;857&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_pool_ 내부를 조금 더 살펴보겠습니다. Connection Pool 내부에는 Client를 관리하는 list가 존재합니다. 다만 Connection Pool 내부에 존재하는 Client가 상태가 모두 다를 수 있기 때문에 이를 관리하기 위한 여러 자료구조가 존재합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;가령 Connection Pool에서 생성된 Client는 위 그림과 같이 상태를 내부 속성으로 지니고 있는데, 상태가 Ready, Busy, Draining 등에 따라 재사용 가능 여부가 결정됩니다. 그 이유는 현재 Client 사용중인데, 해당 Client를 재사용하게된다면 이는 올바른 서비스를 제공할 수 없기 때문입니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서 현재 재사용이 가능한 Client 목록을 지니고 있는 ready_clients_, 현재 Stream 처리가 진행중이거나 Drain을 수행중인 Client 목록을 보유하는 busy_clients 그리고 연결을 시도중인 Client 목록을 보유하고 있는 connecting_clients_ 등 여러 자료구조가 존재합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;해당 자료구조는 Client의 상태 변이에 따라 저장되는 리스트가 다르며, 만약 재사용이 가능한 ready_clients_ 에서 Client를 사용하여 해당 Client가 Connecting을 수행한다면 connectiong_clients_로 Client 목록이 이동하게 됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;이번에는 Upstream의 filter_manager에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;upstream_request.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;// Set up the upstream filter manager.
&amp;nbsp;&amp;nbsp;filter_manager_callbacks_ = std::make_unique&amp;lt;UpstreamRequestFilterManagerCallbacks&amp;gt;(*this);
&amp;nbsp;&amp;nbsp;filter_manager_ = std::make_unique&amp;lt;UpstreamFilterManager&amp;gt;(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*filter_manager_callbacks_, parent_.callbacks()-&amp;gt;dispatcher(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parent_.callbacks()-&amp;gt;connection(), parent_.callbacks()-&amp;gt;streamId(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parent_.callbacks()-&amp;gt;account(), true, parent_.callbacks()-&amp;gt;decoderBufferLimit(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*parent_.cluster(), *this);
&amp;nbsp;&amp;nbsp;parent_.cluster()-&amp;gt;createFilterChain(*filter_manager_);
&amp;nbsp;&amp;nbsp;// The cluster will always create a codec filter, which sets the upstream&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;UpstreamRequest 내부 filter_manager는 Upstream 요청을 처리하기 위한 filter를 생성하고 관리하는 역할을 수행합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1403&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckrm54/btsdc0AGMg6/A2hsHK7bVnAtEtmBE3ABuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckrm54/btsdc0AGMg6/A2hsHK7bVnAtEtmBE3ABuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckrm54/btsdc0AGMg6/A2hsHK7bVnAtEtmBE3ABuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fckrm54%2Fbtsdc0AGMg6%2FA2hsHK7bVnAtEtmBE3ABuK%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;640&quot; height=&quot;240&quot; data-origin-width=&quot;1403&quot; data-origin-height=&quot;527&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;upstream_codec_filter.h&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;class UpstreamCodecFilterFactory
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;: public Extensions::HttpFilters::Common::CommonFactoryBase&amp;lt;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;envoy::extensions::filters::http::upstream_codec::v3::UpstreamCodec&amp;gt;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;public Server::Configuration::UpstreamHttpFilterConfigFactory {
public:
&amp;nbsp;&amp;nbsp;UpstreamCodecFilterFactory() : CommonFactoryBase(&quot;envoy.filters.http.upstream_codec&quot;) {}

&amp;nbsp;&amp;nbsp;std::string category() const override { return &quot;envoy.filters.http.upstream&quot;; }
&amp;nbsp;&amp;nbsp;Http::FilterFactoryCb
&amp;nbsp;&amp;nbsp;createFilterFactoryFromProto(const Protobuf::Message&amp;amp;, const std::string&amp;amp;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Server::Configuration::UpstreamHttpFactoryContext&amp;amp;) override {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return [](Http::FilterChainFactoryCallbacks&amp;amp; callbacks) -&amp;gt; void {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks.addStreamDecoderFilter(std::make_shared&amp;lt;UpstreamCodecFilter&amp;gt;());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;bool isTerminalFilterByProtoTyped(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const envoy::extensions::filters::http::upstream_codec::v3::UpstreamCodec&amp;amp;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Server::Configuration::ServerFactoryContext&amp;amp;) override {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return true;
&amp;nbsp;&amp;nbsp;}
};&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이때 내부적으로는 위 코드와 같이 UpstreamCodecFilterFactory에 의해서 UpstreamCodecFilter가 UpstreamRequest의 filter chain으로 등록되며, 향후 Upstream 요청을처리하는 역할을 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;마지막으로 살펴볼 것은 stream_info_와 upstream_ 속성입니다. stream_info의 경우 현재 Upstream의 메타정보를 관리하는 속성이며, upstream_의 경우 이 다음에 바로 설명하겠지만, UpstreamRequest가 생성된 이후 사용자 요청을 처리하기 위한 헤더 정보 매핑, 해당 요청을 처리해야할 encoder 등의 속성을 지니고 있는 자료구조로써 Upstream 처리를 수행합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;지금까지 UpstreamRequest 내부에 포함된 주요 속성에 대해서 살펴봤는데요. 이번에는 UpstreamRequest를 생성한 다음 후속 과정에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2303&quot; data-origin-height=&quot;929&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vpLPY/btsdaHteUEw/hhK7ergFsD1iyY4OQIZDCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vpLPY/btsdaHteUEw/hhK7ergFsD1iyY4OQIZDCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vpLPY/btsdaHteUEw/hhK7ergFsD1iyY4OQIZDCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvpLPY%2FbtsdaHteUEw%2FhhK7ergFsD1iyY4OQIZDCk%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;2303&quot; height=&quot;929&quot; data-origin-width=&quot;2303&quot; data-origin-height=&quot;929&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;30. Router 에서는 upstream_requests_ 에 추가된 UpstreamRequest에게 Header 정보를 전달합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;31. UpstreamRequest 내부에서는 내부에 매핑된 pool을 통해 Client를 해당 Pool에 매핑하도록 요청합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;32. Pool 내부에는 ready_clients_ 를 먼저 살펴보고 연결 가능한 목록이 존재하면 해당 목록에 존재하는 Client 목록을 얻어와서 현재 Stream을 연결하도록 합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_pool_base.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;if (!ready_clients_.empty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ActiveClient&amp;amp; client = *ready_clients_.front();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_CONN_LOG(debug, &quot;using existing fully connected connection&quot;, client);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;attachStreamToClient(client, context);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Even if there's a ready client, we may want to preconnect to handle the next incoming stream.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;tryCreateNewConnections();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return nullptr;
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이때 위 코드와 같이 먼저 ready_clients_ 로부터 재사용이 가능한 Client 목록이 존재하는지 살펴보고 만약 존재한다면 해당 Client를 재사용하여 사용자의 Context를 해당 Client에게 연결시킵니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_pool_base.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;if (!host_-&amp;gt;cluster().resourceManager(priority_).pendingRequests().canCreate()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_LOG(debug, &quot;max pending streams overflow&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;onPoolFailure(nullptr, absl::string_view(), ConnectionPool::PoolFailureReason::Overflow,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;context);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;host_-&amp;gt;cluster().stats().upstream_rq_pending_overflow_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return nullptr;
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;하지만 ready_clients_ 목록에 연결 가능한 Client가 존재하지 않는다면, 해당 Host에 연결된 설정을 토대로 pending request 생성 여부를 판별한 다음 해당 설정을 넘어선 Stream 요청이 들어왔을 경우는 연결을 해제하도록 합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;반면 Stream 생성이 가능하다면, 해당 요청은 HttpPendingStream으로 생성하여 Pool에 보유한 pending_streams_ 리스트에 이를 추가합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CEDxr/btsddGaGpsF/8wK8nSKj2OiMmXCaTKQKuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CEDxr/btsddGaGpsF/8wK8nSKj2OiMmXCaTKQKuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CEDxr/btsddGaGpsF/8wK8nSKj2OiMmXCaTKQKuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCEDxr%2FbtsddGaGpsF%2F8wK8nSKj2OiMmXCaTKQKuk%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;640&quot; height=&quot;330&quot; data-origin-width=&quot;1009&quot; data-origin-height=&quot;520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;그리고 이렇게 등록된 pending_streams_는 주기적으로 Dispatcher에 존재하는 baseScheduler에 의해서 관리됩니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_pool_base.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnPoolImplBase::onUpstreamReady() {
&amp;nbsp;&amp;nbsp;while (!pending_streams_.empty() &amp;amp;&amp;amp; !ready_clients_.empty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ActiveClientPtr&amp;amp; client = ready_clients_.front();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_CONN_LOG(debug, &quot;attaching to next stream&quot;, *client);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Pending streams are pushed onto the front, so pull from the back.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;attachStreamToClient(*client, pending_streams_.back()-&amp;gt;context());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;state_.decrPendingStreams(1);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;pending_streams_.pop_back();
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;if (!pending_streams_.empty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;tryCreateNewConnections();
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이때 주기적으로 호출되는 메소드는 onUpstreamReady이며, 해당 메소드의 역할을 살펴보면, pending_streams에 존재하는 stream을 살펴보고 호출 당시 ready_clients_에 재사용 가능한 client가 존재한다면, 해당 client에 pending_streams에 존재하는 stream을 연결시켜주는 역할을 수행하는 것을 볼 수 있습니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;33. HttpUpstream이 생성되면 UpstreamRequest에 할당하여 이후 해당 Stream을 통해 Upstream과 연결을 수행할 수 있습니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;지금까지 살펴볼 것은 Header가 분석이 완료된 이후 후속 작업에 대해서 살펴봤습니다. 해당 과정을 요약하자면, Header 정보를 파싱하여 이를 별도 Map에 저장하는 것은 물론 Cluster Manager와 협업을 통해서 Connection Pool을 할당받고 해당 Pool에 존재하는 Client에 연결을 수행하여 Upstream을 만드는 작업을 진행합니다. 이번에는 Http Request의 본문을 파싱하고 이를 해석하는 과정에 대해서 살펴보겠습니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1797&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/elhS2m/btsdQdeCKAj/LLIa8fJyIKfUKcRWN4rib0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/elhS2m/btsdQdeCKAj/LLIa8fJyIKfUKcRWN4rib0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/elhS2m/btsdQdeCKAj/LLIa8fJyIKfUKcRWN4rib0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FelhS2m%2FbtsdQdeCKAj%2FLLIa8fJyIKfUKcRWN4rib0%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;2578&quot; height=&quot;1797&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1797&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;34. Parser에서 본문을 분석한 다음 bufferBody callback 함수를 호출합니다.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;35. bufferBody를 호출받으면 이를 다시 bufferBody 함수를 호추하여 후속 작업을 이어갑니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;legacy_parser_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data)-&amp;gt;bufferBody(at, length);
return 0;&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionImpl::bufferBody(const char* data, size_t length) {
&amp;nbsp;&amp;nbsp;auto slice = current_dispatching_buffer_-&amp;gt;frontSlice();
&amp;nbsp;&amp;nbsp;if (data == slice.mem_ &amp;amp;&amp;amp; length == slice.len_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffered_body_.move(*current_dispatching_buffer_, length);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dispatching_slice_already_drained_ = true;
&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffered_body_.add(data, length);
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;ServerConnectionImpl은 bufferBody 내부에서 전달받은 데이터를 내부의&amp;nbsp; buffer_body_에 추가합니다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1797&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AMMCR/btsdRgIMmp5/OovHqemFtixrHMObYYUaTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AMMCR/btsdRgIMmp5/OovHqemFtixrHMObYYUaTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AMMCR/btsdRgIMmp5/OovHqemFtixrHMObYYUaTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAMMCR%2FbtsdRgIMmp5%2FOovHqemFtixrHMObYYUaTk%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;2578&quot; height=&quot;1797&quot; data-origin-width=&quot;2578&quot; data-origin-height=&quot;1797&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;36. Body 파싱이 모두 완료되면 Parser는 onMessageComplete callback 함수를 호출합니다.&lt;br&gt;&amp;nbsp;&lt;br&gt;legacy_parser_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onMessageComplete());&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;37.&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;1850:&lt;/p&gt;&lt;div&gt; 
 &lt;div&gt; &lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; [](&lt;/span&gt;&lt;span style=&quot;color: #4ec9b0;&quot;&gt;http_parser&lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;parser&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;) -&amp;gt; &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt; {&lt;/span&gt; 
 &lt;/div&gt; 
 &lt;div&gt; &lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;auto&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;* &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;conn_impl&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;static_cast&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #4ec9b0;&quot;&gt;ParserCallbacks&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;*&amp;gt;(&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;parser&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;);&lt;/span&gt; 
 &lt;/div&gt; 
 &lt;div&gt; &lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #c586c0;&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;static_cast&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;conn_impl&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #dcdcaa;&quot;&gt;onHeadersComplete&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;());&lt;/span&gt; 
 &lt;/div&gt; 
 &lt;div&gt; &lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; },&lt;/span&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;p data-ke-size=&quot;size16&quot;&gt;codec_impl.cc&lt;/p&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1669777999851&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CallbackResult ConnectionImpl::onHeadersComplete() {
  return setAndCheckCallbackStatusOr(onHeadersCompleteImpl());
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   codec_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1669778017860&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CallbackResult ConnectionImpl::onHeadersComplete() {
  return setAndCheckCallbackStatusOr(onHeadersCompleteImpl());
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   codec_impl.cc 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1669778039623&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;StatusOr&amp;lt;CallbackResult&amp;gt; ConnectionImpl::onHeadersCompleteImpl() {
  ASSERT(!processing_trailers_);
  ASSERT(dispatching_);
  ENVOY_CONN_LOG(trace, &quot;onHeadersCompleteBase&quot;, connection_);
  RETURN_IF_ERROR(completeCurrentHeader());

  if (!parser_-&amp;gt;isHttp11()) {
    // This is not necessarily true, but it's good enough since higher layers only care if this is
    // HTTP/1.1 or not.
    protocol_ = Protocol::Http10;
  }
  RequestOrResponseHeaderMap&amp;amp; request_or_response_headers = requestOrResponseHeaders();
  const Http::HeaderValues&amp;amp; header_values = Http::Headers::get();
  if (Utility::isUpgrade(request_or_response_headers) &amp;amp;&amp;amp; upgradeAllowed()) {
    // Ignore h2c upgrade requests until we support them.
    // See https://github.com/envoyproxy/envoy/issues/7161 for details.
    if (absl::EqualsIgnoreCase(request_or_response_headers.getUpgradeValue(),
                               header_values.UpgradeValues.H2c)) {
      ENVOY_CONN_LOG(trace, &quot;removing unsupported h2c upgrade headers.&quot;, connection_);
      request_or_response_headers.removeUpgrade();
      if (request_or_response_headers.Connection()) {
        const auto&amp;amp; tokens_to_remove = caseUnorderdSetContainingUpgradeAndHttp2Settings();
        std::string new_value = StringUtil::removeTokens(
            request_or_response_headers.getConnectionValue(), &quot;,&quot;, tokens_to_remove, &quot;,&quot;);
        if (new_value.empty()) {
          request_or_response_headers.removeConnection();
        } else {
          request_or_response_headers.setConnection(new_value);
        }
      }
      request_or_response_headers.remove(header_values.Http2Settings);
    } else {
      ENVOY_CONN_LOG(trace, &quot;codec entering upgrade mode.&quot;, connection_);
      handling_upgrade_ = true;
    }
  }
  if (parser_-&amp;gt;methodName() == header_values.MethodValues.Connect) {
    if (request_or_response_headers.ContentLength()) {
      if (request_or_response_headers.getContentLengthValue() == &quot;0&quot;) {
        request_or_response_headers.removeContentLength();
      } else {
        // Per https://tools.ietf.org/html/rfc7231#section-4.3.6 a payload with a
        // CONNECT request has no defined semantics, and may be rejected.
        error_code_ = Http::Code::BadRequest;
        RETURN_IF_ERROR(sendProtocolError(Http1ResponseCodeDetails::get().BodyDisallowed));
        return codecProtocolError(&quot;http/1.1 protocol error: unsupported content length&quot;);
      }
    }
    ENVOY_CONN_LOG(trace, &quot;codec entering upgrade mode for CONNECT request.&quot;, connection_);
    handling_upgrade_ = true;
  }

  // https://tools.ietf.org/html/rfc7230#section-3.3.3
  // If a message is received with both a Transfer-Encoding and a
  // Content-Length header field, the Transfer-Encoding overrides the
  // Content-Length. Such a message might indicate an attempt to
  // perform request smuggling (Section 9.5) or response splitting
  // (Section 9.4) and ought to be handled as an error. A sender MUST
  // remove the received Content-Length field prior to forwarding such
  // a message.

#ifndef ENVOY_ENABLE_UHV
  // This check is moved into default header validator.
  // TODO(yanavlasov): use runtime override here when UHV is moved into the main build

  // Reject message with Http::Code::BadRequest if both Transfer-Encoding and Content-Length
  // headers are present or if allowed by http1 codec settings and 'Transfer-Encoding'
  // is chunked - remove Content-Length and serve request.
  if (parser_-&amp;gt;hasTransferEncoding() != 0 &amp;amp;&amp;amp; request_or_response_headers.ContentLength()) {
    if (parser_-&amp;gt;isChunked() &amp;amp;&amp;amp; codec_settings_.allow_chunked_length_) {
      request_or_response_headers.removeContentLength();
    } else {
      error_code_ = Http::Code::BadRequest;
      RETURN_IF_ERROR(sendProtocolError(Http1ResponseCodeDetails::get().ChunkedContentLength));
      return codecProtocolError(
          &quot;http/1.1 protocol error: both 'Content-Length' and 'Transfer-Encoding' are set.&quot;);
    }
  }
#endif

  // Per https://tools.ietf.org/html/rfc7230#section-3.3.1 Envoy should reject
  // transfer-codings it does not understand.
  // Per https://tools.ietf.org/html/rfc7231#section-4.3.6 a payload with a
  // CONNECT request has no defined semantics, and may be rejected.
  if (request_or_response_headers.TransferEncoding()) {
    const absl::string_view encoding = request_or_response_headers.getTransferEncodingValue();
    if (!absl::EqualsIgnoreCase(encoding, header_values.TransferEncodingValues.Chunked) ||
        parser_-&amp;gt;methodName() == header_values.MethodValues.Connect) {
      error_code_ = Http::Code::NotImplemented;
      RETURN_IF_ERROR(sendProtocolError(Http1ResponseCodeDetails::get().InvalidTransferEncoding));
      return codecProtocolError(&quot;http/1.1 protocol error: unsupported transfer encoding&quot;);
    }
  }

  auto statusor = onHeadersCompleteBase();
  if (!statusor.ok()) {
    RETURN_IF_ERROR(statusor.status());
  }

  header_parsing_state_ = HeaderParsingState::Done;

  // Returning CallbackResult::NoBodyData informs http_parser to not expect a body or further data
  // on this connection.
  return handling_upgrade_ ? CallbackResult::NoBodyData : statusor.value();
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;p data-ke-size=&quot;size16&quot;&gt;codec_impl.cc&lt;/p&gt; 
 &lt;div&gt; 
  &lt;pre id=&quot;code_1669778369397&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Envoy::StatusOr&amp;lt;CallbackResult&amp;gt; ServerConnectionImpl::onHeadersCompleteBase() {
  // Handle the case where response happens prior to request complete. It's up to upper layer code
  // to disconnect the connection but we shouldn't fire any more events since it doesn't make
  // sense.
  if (active_request_) {
    auto&amp;amp; headers = absl::get&amp;lt;RequestHeaderMapPtr&amp;gt;(headers_or_trailers_);
    ENVOY_CONN_LOG(trace, &quot;Server: onHeadersComplete size={}&quot;, connection_, headers-&amp;gt;size());

    if (!handling_upgrade_ &amp;amp;&amp;amp; headers-&amp;gt;Connection()) {
      // If we fail to sanitize the request, return a 400 to the client
      if (!Utility::sanitizeConnectionHeader(*headers)) {
        absl::string_view header_value = headers-&amp;gt;getConnectionValue();
        ENVOY_CONN_LOG(debug, &quot;Invalid nominated headers in Connection: {}&quot;, connection_,
                       header_value);
        error_code_ = Http::Code::BadRequest;
        RETURN_IF_ERROR(
            sendProtocolError(Http1ResponseCodeDetails::get().ConnectionHeaderSanitization));
        return codecProtocolError(&quot;Invalid nominated headers in Connection.&quot;);
      }
    }

    // Inform the response encoder about any HEAD method, so it can set content
    // length and transfer encoding headers correctly.
    const Http::HeaderValues&amp;amp; header_values = Http::Headers::get();
    active_request_-&amp;gt;response_encoder_.setIsResponseToHeadRequest(parser_-&amp;gt;methodName() ==
                                                                  header_values.MethodValues.Head);
    active_request_-&amp;gt;response_encoder_.setIsResponseToConnectRequest(
        parser_-&amp;gt;methodName() == header_values.MethodValues.Connect);

    RETURN_IF_ERROR(handlePath(*headers, parser_-&amp;gt;methodName()));
    ASSERT(active_request_-&amp;gt;request_url_.empty());

    headers-&amp;gt;setMethod(parser_-&amp;gt;methodName());

    // Make sure the host is valid.
    auto details = HeaderUtility::requestHeadersValid(*headers);
    if (details.has_value()) {
      RETURN_IF_ERROR(sendProtocolError(details.value().get()));
      return codecProtocolError(
          &quot;http/1.1 protocol error: request headers failed spec compliance checks&quot;);
    }

    // Determine here whether we have a body or not. This uses the new RFC semantics where the
    // presence of content-length or chunked transfer-encoding indicates a body vs. a particular
    // method. If there is no body, we defer raising decodeHeaders() until the parser is flushed
    // with message complete. This allows upper layers to behave like HTTP/2 and prevents a proxy
    // scenario where the higher layers stream through and implicitly switch to chunked transfer
    // encoding because end stream with zero body length has not yet been indicated.
    if (parser_-&amp;gt;isChunked() ||
        (parser_-&amp;gt;contentLength().has_value() &amp;amp;&amp;amp; parser_-&amp;gt;contentLength().value() &amp;gt; 0) ||
        handling_upgrade_) {
      active_request_-&amp;gt;request_decoder_-&amp;gt;decodeHeaders(std::move(headers), false);

      // If the connection has been closed (or is closing) after decoding headers, pause the parser
      // so we return control to the caller.
      if (connection_.state() != Network::Connection::State::Open) {
        return parser_-&amp;gt;pause();
      }
    } else {
      deferred_end_stream_headers_ = true;
    }
  }

  return CallbackResult::Success;
}&lt;/code&gt;&lt;/pre&gt; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt;
   &amp;nbsp; 
 &lt;/div&gt; 
 &lt;div&gt; &lt;span style=&quot;color: #d4d4d4;&quot;&gt;1934:&lt;/span&gt; 
 &lt;/div&gt; 
 &lt;div&gt; 
  &lt;div&gt; &lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; [](&lt;/span&gt;&lt;span style=&quot;color: #4ec9b0;&quot;&gt;http_parser&lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;parser&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;) -&amp;gt; &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt; {&lt;/span&gt; 
  &lt;/div&gt; 
  &lt;div&gt; &lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;auto&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;* &lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;conn_impl&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt; = &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;static_cast&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #4ec9b0;&quot;&gt;ParserCallbacks&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;*&amp;gt;(&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;parser&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;);&lt;/span&gt; 
  &lt;/div&gt; 
  &lt;div&gt; &lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &lt;/span&gt;&lt;span style=&quot;color: #c586c0;&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt; &lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;static_cast&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&quot;color: #569cd6;&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;gt;(&lt;/span&gt;&lt;span style=&quot;color: #9cdcfe;&quot;&gt;conn_impl&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;-&amp;gt;&lt;/span&gt;&lt;span style=&quot;color: #dcdcaa;&quot;&gt;onMessageComplete&lt;/span&gt;&lt;span style=&quot;color: #d4d4d4;&quot;&gt;());&lt;/span&gt; 
  &lt;/div&gt; 
  &lt;div&gt; &lt;span style=&quot;color: #d4d4d4;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; },&lt;/span&gt; 
  &lt;/div&gt; 
  &lt;div&gt;
    &amp;nbsp; 
  &lt;/div&gt; 
  &lt;div&gt;
    &amp;nbsp; 
  &lt;/div&gt; 
  &lt;div&gt;
    codec_impl.cc 
  &lt;/div&gt; 
  &lt;div&gt;
    &amp;nbsp; 
  &lt;/div&gt; 
  &lt;div&gt; 
   &lt;pre id=&quot;code_1669778405412&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CallbackResult ConnectionImpl::onMessageComplete() {
  return setAndCheckCallbackStatusOr(onMessageCompleteImpl());
}&lt;/code&gt;&lt;/pre&gt; 
  &lt;/div&gt; 
 &lt;/div&gt; 
&lt;/div&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;StatusOr&amp;lt;CallbackResult&amp;gt; ConnectionImpl::onMessageCompleteImpl() {
&amp;nbsp;&amp;nbsp;ENVOY_CONN_LOG(trace, &quot;message complete&quot;, connection_);

&amp;nbsp;&amp;nbsp;dispatchBufferedBody();

&amp;nbsp;&amp;nbsp;if (handling_upgrade_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// If this is an upgrade request, swallow the onMessageComplete. The
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// upgrade payload will be treated as stream body.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(!deferred_end_stream_headers_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_CONN_LOG(trace, &quot;Pausing parser due to upgrade.&quot;, connection_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return parser_-&amp;gt;pause();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// If true, this indicates we were processing trailers and must
&amp;nbsp;&amp;nbsp;// move the last header into current_header_map_
&amp;nbsp;&amp;nbsp;if (header_parsing_state_ == HeaderParsingState::Value) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;RETURN_IF_ERROR(completeCurrentHeader());
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;return onMessageCompleteBase();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionImpl::dispatchBufferedBody() {
&amp;nbsp;&amp;nbsp;ASSERT(parser_-&amp;gt;getStatus() == ParserStatus::Ok || parser_-&amp;gt;getStatus() == ParserStatus::Paused);
&amp;nbsp;&amp;nbsp;ASSERT(codec_status_.ok());
&amp;nbsp;&amp;nbsp;if (buffered_body_.length() &amp;gt; 0) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;onBody(buffered_body_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffered_body_.drain(buffered_body_.length());
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;body에서 추출할게 있으면 추출한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;CallbackResult ServerConnectionImpl::onMessageCompleteBase() {
&amp;nbsp;&amp;nbsp;ASSERT(!handling_upgrade_);
&amp;nbsp;&amp;nbsp;if (active_request_) {

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// The request_decoder should be non-null after we've called the newStream on callbacks.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(active_request_-&amp;gt;request_decoder_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;active_request_-&amp;gt;remote_complete_ = true;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (deferred_end_stream_headers_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;active_request_-&amp;gt;request_decoder_-&amp;gt;decodeHeaders(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::move(absl::get&amp;lt;RequestHeaderMapPtr&amp;gt;(headers_or_trailers_)), true);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;deferred_end_stream_headers_ = false;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else if (processing_trailers_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;active_request_-&amp;gt;request_decoder_-&amp;gt;decodeTrailers(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::move(absl::get&amp;lt;RequestTrailerMapPtr&amp;gt;(headers_or_trailers_)));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Buffer::OwnedImpl buffer;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;active_request_-&amp;gt;request_decoder_-&amp;gt;decodeData(buffer, true);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Reset to ensure no information from one requests persists to the next.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers_or_trailers_.emplace&amp;lt;RequestHeaderMapPtr&amp;gt;(nullptr);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Always pause the parser so that the calling code can process 1 request at a time and apply
&amp;nbsp;&amp;nbsp;// back pressure. However this means that the calling code needs to detect if there is more data
&amp;nbsp;&amp;nbsp;// in the buffer and dispatch it again.
&amp;nbsp;&amp;nbsp;return parser_-&amp;gt;pause();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;// Ordering in this function is complicated, but important.
//
// We want to do minimal work before selecting route and creating a filter
// chain to maximize the number of requests which get custom filter behavior,
// e.g. registering access logging.
//
// This must be balanced by doing sanity checking for invalid requests (one
// can't route select properly without full headers), checking state required to
// serve error responses (connection close, head requests, etc), and
// modifications which may themselves affect route selection.
void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&amp;amp;&amp;amp; headers,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bool end_stream) {
&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(debug, &quot;request headers complete (end_stream={}):\n{}&quot;, *this, end_stream,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; *headers);
&amp;nbsp;&amp;nbsp;ScopeTrackerScopeState scope(this,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; connection_manager_.read_callbacks_-&amp;gt;connection().dispatcher());
&amp;nbsp;&amp;nbsp;request_headers_ = std::move(headers);
&amp;nbsp;&amp;nbsp;filter_manager_.requestHeadersInitialized();
&amp;nbsp;&amp;nbsp;if (request_header_timer_ != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;request_header_timer_-&amp;gt;disableTimer();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;request_header_timer_.reset();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Both saw_connection_close_ and is_head_request_ affect local replies: set
&amp;nbsp;&amp;nbsp;// them as early as possible.
&amp;nbsp;&amp;nbsp;const Protocol protocol = connection_manager_.codec_-&amp;gt;protocol();
&amp;nbsp;&amp;nbsp;state_.saw_connection_close_ = HeaderUtility::shouldCloseConnection(protocol, *request_headers_);

&amp;nbsp;&amp;nbsp;// We end the decode here to mark that the downstream stream is complete.
&amp;nbsp;&amp;nbsp;maybeEndDecode(end_stream);

&amp;nbsp;&amp;nbsp;if (!validateHeaders()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(debug, &quot;request headers validation failed\n{}&quot;, *this, *request_headers_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// We need to snap snapped_route_config_ here as it's used in mutateRequestHeaders later.
&amp;nbsp;&amp;nbsp;if (connection_manager_.config_.isRoutable()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (connection_manager_.config_.routeConfigProvider() != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapped_route_config_ = connection_manager_.config_.routeConfigProvider()-&amp;gt;configCast();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else if (connection_manager_.config_.scopedRouteConfigProvider() != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapped_scoped_routes_config_ =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.config_.scopedRouteConfigProvider()-&amp;gt;config&amp;lt;Router::ScopedConfig&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapScopedRouteConfig();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapped_route_config_ = connection_manager_.config_.routeConfigProvider()-&amp;gt;configCast();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Drop new requests when overloaded as soon as we have decoded the headers.
&amp;nbsp;&amp;nbsp;if (connection_manager_.random_generator_.bernoulli(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.overload_stop_accepting_requests_ref_.value())) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// In this one special case, do not create the filter chain. If there is a risk of memory
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// overload it is more important to avoid unnecessary allocation than to create the filters.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;filter_manager_.skipFilterChainCreation();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.stats_.named_.downstream_rq_overload_close_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(Http::Code::ServiceUnavailable, &quot;envoy overloaded&quot;, nullptr, absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().Overload);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;if (!connection_manager_.config_.proxy100Continue() &amp;amp;&amp;amp; request_headers_-&amp;gt;Expect() &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// The Expect field-value is case-insensitive.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// https://tools.ietf.org/html/rfc7231#section-5.1.1
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;absl::EqualsIgnoreCase((request_headers_-&amp;gt;Expect()-&amp;gt;value().getStringView()),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Headers::get().ExpectValues._100Continue)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Note in the case Envoy is handling 100-Continue complexity, it skips the filter chain
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// and sends the 100-Continue directly to the encoder.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chargeStats(continueHeader());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response_encoder_-&amp;gt;encode1xxHeaders(continueHeader());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Remove the Expect header so it won't be handled again upstream.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;request_headers_-&amp;gt;removeExpect();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;connection_manager_.user_agent_.initializeFromHeaders(*request_headers_,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.stats_.prefixStatName(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.stats_.scope_);

&amp;nbsp;&amp;nbsp;// Make sure we are getting a codec version we support.
&amp;nbsp;&amp;nbsp;if (protocol == Protocol::Http10) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Assume this is HTTP/1.0. This is fine for HTTP/0.9 but this code will also affect any
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// requests with non-standard version numbers (0.9, 1.3), basically anything which is not
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// HTTP/1.1.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// The protocol may have shifted in the HTTP/1.0 case so reset it.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().protocol(protocol);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!connection_manager_.config_.http1Settings().accept_http_10_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Send &quot;Upgrade Required&quot; if HTTP/1.0 support is not explicitly configured on.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(Code::UpgradeRequired, &quot;&quot;, nullptr, absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().LowVersion);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!request_headers_-&amp;gt;Host() &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;!connection_manager_.config_.http1Settings().default_host_for_http_10_.empty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Add a default host if configured to do so.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;request_headers_-&amp;gt;setHost(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.config_.http1Settings().default_host_for_http_10_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;if (!request_headers_-&amp;gt;Host()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Require host header. For HTTP/1.1 Host has already been translated to :authority.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(Code::BadRequest, &quot;&quot;, nullptr, absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().MissingHost);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Verify header sanity checks which should have been performed by the codec.
&amp;nbsp;&amp;nbsp;ASSERT(HeaderUtility::requestHeadersValid(*request_headers_).has_value() == false);

&amp;nbsp;&amp;nbsp;// Check for the existence of the :path header for non-CONNECT requests, or present-but-empty
&amp;nbsp;&amp;nbsp;// :path header for CONNECT requests. We expect the codec to have broken the path into pieces if
&amp;nbsp;&amp;nbsp;// applicable. NOTE: Currently the HTTP/1.1 codec only does this when the allow_absolute_url flag
&amp;nbsp;&amp;nbsp;// is enabled on the HCM.
&amp;nbsp;&amp;nbsp;if ((!HeaderUtility::isConnect(*request_headers_) || request_headers_-&amp;gt;Path()) &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;request_headers_-&amp;gt;getPathValue().empty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(Code::NotFound, &quot;&quot;, nullptr, absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().MissingPath);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Currently we only support relative paths at the application layer.
&amp;nbsp;&amp;nbsp;if (!request_headers_-&amp;gt;getPathValue().empty() &amp;amp;&amp;amp; request_headers_-&amp;gt;getPathValue()[0] != '/') {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.stats_.named_.downstream_rq_non_relative_path_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(Code::NotFound, &quot;&quot;, nullptr, absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().AbsolutePath);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Path sanitization should happen before any path access other than the above sanity check.
&amp;nbsp;&amp;nbsp;const auto action =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ConnectionManagerUtility::maybeNormalizePath(*request_headers_, connection_manager_.config_);
&amp;nbsp;&amp;nbsp;// gRPC requests are rejected if Envoy is configured to redirect post-normalization. This is
&amp;nbsp;&amp;nbsp;// because gRPC clients do not support redirect.
&amp;nbsp;&amp;nbsp;if (action == ConnectionManagerUtility::NormalizePathAction::Reject ||
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(action == ConnectionManagerUtility::NormalizePathAction::Redirect &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Grpc::Common::hasGrpcContentType(*request_headers_))) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.stats_.named_.downstream_rq_failed_path_normalization_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(Code::BadRequest, &quot;&quot;, nullptr, absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().PathNormalizationFailed);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;} else if (action == ConnectionManagerUtility::NormalizePathAction::Redirect) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.stats_.named_.downstream_rq_redirected_with_normalized_path_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Code::TemporaryRedirect, &quot;&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[new_path = request_headers_-&amp;gt;Path()-&amp;gt;value().getStringView()](
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Http::ResponseHeaderMap&amp;amp; response_headers) -&amp;gt; void {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response_headers.addReferenceKey(Http::Headers::get().Location, new_path);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;absl::nullopt, StreamInfo::ResponseCodeDetails::get().PathNormalizationFailed);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;ASSERT(action == ConnectionManagerUtility::NormalizePathAction::Continue);
&amp;nbsp;&amp;nbsp;auto optional_port = ConnectionManagerUtility::maybeNormalizeHost(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*request_headers_, connection_manager_.config_, localPort());
&amp;nbsp;&amp;nbsp;if (optional_port.has_value() &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;requestWasConnect(request_headers_, connection_manager_.codec_-&amp;gt;protocol())) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().filterState()-&amp;gt;setData(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Router::OriginalConnectPort::key(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::make_unique&amp;lt;Router::OriginalConnectPort&amp;gt;(optional_port.value()),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;StreamInfo::FilterState::StateType::ReadOnly, StreamInfo::FilterState::LifeSpan::Request);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;if (!state_.is_internally_created_) { // Only sanitize headers on first pass.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Modify the downstream remote address depending on configuration and headers.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto mutate_result = ConnectionManagerUtility::mutateRequestHeaders(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*request_headers_, connection_manager_.read_callbacks_-&amp;gt;connection(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.config_, *snapped_route_config_, connection_manager_.local_info_);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// IP detection failed, reject the request.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (mutate_result.reject_request.has_value()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto&amp;amp; reject_request_params = mutate_result.reject_request.value();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.stats_.named_.downstream_rq_rejected_via_ip_detection_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(reject_request_params.response_code, reject_request_params.body, nullptr,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().OriginalIPDetectionFailed);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;filter_manager_.setDownstreamRemoteAddress(mutate_result.final_remote_address);
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;ASSERT(filter_manager_.streamInfo().downstreamAddressProvider().remoteAddress() != nullptr);

&amp;nbsp;&amp;nbsp;ASSERT(!cached_route_);
&amp;nbsp;&amp;nbsp;refreshCachedRoute();

&amp;nbsp;&amp;nbsp;if (!state_.is_internally_created_) { // Only mutate tracing headers on first pass.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().setTraceReason(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ConnectionManagerUtility::mutateTracingRequestHeader(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*request_headers_, connection_manager_.runtime_, connection_manager_.config_,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cached_route_.value().get()));
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().setRequestHeaders(*request_headers_);

&amp;nbsp;&amp;nbsp;const bool upgrade_rejected = filter_manager_.createFilterChain() == false;

&amp;nbsp;&amp;nbsp;// TODO if there are no filters when starting a filter iteration, the connection manager
&amp;nbsp;&amp;nbsp;// should return 404. The current returns no response if there is no router filter.
&amp;nbsp;&amp;nbsp;if (hasCachedRoute()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Do not allow upgrades if the route does not support it.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (upgrade_rejected) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// While downstream servers should not send upgrade payload without the upgrade being
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// accepted, err on the side of caution and refuse to process any further requests on this
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// connection, to avoid a class of HTTP/1.1 smuggling bugs where Upgrade or CONNECT payload
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// contains a smuggled HTTP request.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;state_.saw_connection_close_ = true;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.stats_.named_.downstream_rq_ws_on_non_ws_route_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(Code::Forbidden, &quot;&quot;, nullptr, absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().UpgradeFailed);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Allow non websocket requests to go through websocket enabled routes.
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Check if tracing is enabled.
&amp;nbsp;&amp;nbsp;if (connection_manager_tracing_config_.has_value()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;traceRequest();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;filter_manager_.decodeHeaders(*request_headers_, end_stream);

&amp;nbsp;&amp;nbsp;// Reset it here for both global and overridden cases.
&amp;nbsp;&amp;nbsp;resetIdleTimer();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;위 과정에서 이제 connectionManager와 결합하여 Connection을 만든다.&lt;br&gt;과정을 조금 자세히 살펴보면 다음과 같다.&lt;br&gt;&amp;nbsp;&lt;br&gt;header_utility.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool HeaderUtility::shouldCloseConnection(Http::Protocol protocol,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const RequestOrResponseHeaderMap&amp;amp; headers) {
&amp;nbsp;&amp;nbsp;// HTTP/1.0 defaults to single-use connections. Make sure the connection will be closed unless
&amp;nbsp;&amp;nbsp;// Keep-Alive is present.
&amp;nbsp;&amp;nbsp;if (protocol == Protocol::Http10 &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(!headers.Connection() ||
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; !Envoy::StringUtil::caseFindToken(headers.Connection()-&amp;gt;value().getStringView(), &quot;,&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Http::Headers::get().ConnectionValues.KeepAlive))) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return true;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;if (protocol == Protocol::Http11 &amp;amp;&amp;amp; headers.Connection() &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Envoy::StringUtil::caseFindToken(headers.Connection()-&amp;gt;value().getStringView(), &quot;,&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Http::Headers::get().ConnectionValues.Close)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return true;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Note: Proxy-Connection is not a standard header, but is supported here
&amp;nbsp;&amp;nbsp;// since it is supported by http-parser the underlying parser for http
&amp;nbsp;&amp;nbsp;// requests.
&amp;nbsp;&amp;nbsp;if (protocol &amp;lt; Protocol::Http2 &amp;amp;&amp;amp; headers.ProxyConnection() &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Envoy::StringUtil::caseFindToken(headers.ProxyConnection()-&amp;gt;value().getStringView(), &quot;,&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Http::Headers::get().ConnectionValues.Close)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return true;
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;return false;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;먼저 shouldCloseCOnnection을 통해서 해당 프로토콜이 http2이하인지를 살펴보고 종료 여부를 결정한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::ActiveStream::maybeEndDecode(bool end_stream) {
&amp;nbsp;&amp;nbsp;// If recreateStream is called, the HCM rewinds state and may send more encodeData calls.
&amp;nbsp;&amp;nbsp;if (end_stream &amp;amp;&amp;amp; !filter_manager_.remoteDecodeComplete()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().downstreamTiming().onLastDownstreamRxByteReceived(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.read_callbacks_-&amp;gt;connection().dispatcher().timeSource());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(debug, &quot;request end stream&quot;, *this);
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그 다음에 end_stream이면서 decode 완료 여부를 확인한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&amp;amp;&amp;amp; headers,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bool end_stream) {
&amp;nbsp;&amp;nbsp;....(중략)...

&amp;nbsp;&amp;nbsp;// We need to snap snapped_route_config_ here as it's used in mutateRequestHeaders later.
&amp;nbsp;&amp;nbsp;if (connection_manager_.config_.isRoutable()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (connection_manager_.config_.routeConfigProvider() != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapped_route_config_ = connection_manager_.config_.routeConfigProvider()-&amp;gt;configCast();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else if (connection_manager_.config_.scopedRouteConfigProvider() != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapped_scoped_routes_config_ =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.config_.scopedRouteConfigProvider()-&amp;gt;config&amp;lt;Router::ScopedConfig&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapScopedRouteConfig();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapped_route_config_ = connection_manager_.config_.routeConfigProvider()-&amp;gt;configCast();
&amp;nbsp;&amp;nbsp;}

 ...(중략)...
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Connection Manager로부터 Route Config가 존재하는지를 확인하고 존재하면 해당 Config 정보를 가져온다. 만약 ScopedRouteConfig가 설정되어있으면, 해당 정보를 가져오고 그렇지 않을 경우에는 Route 정보를 가져올 것이다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::ActiveStream::snapScopedRouteConfig() {
&amp;nbsp;&amp;nbsp;// NOTE: if a RDS subscription hasn't got a RouteConfiguration back, a Router::NullConfigImpl is
&amp;nbsp;&amp;nbsp;// returned, in that case we let it pass.
&amp;nbsp;&amp;nbsp;snapped_route_config_ = snapped_scoped_routes_config_-&amp;gt;getRouteConfig(*request_headers_);
&amp;nbsp;&amp;nbsp;if (snapped_route_config_ == nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(trace, &quot;can't find SRDS scope.&quot;, *this);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// TODO(stevenzzzz): Consider to pass an error message to router filter, so that it can
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// send back 404 with some more details.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapped_route_config_ = std::make_shared&amp;lt;Router::NullConfigImpl&amp;gt;();
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;scoped_config_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Router::ConfigConstSharedPtr
ScopedConfigImpl::getRouteConfig(const Http::HeaderMap&amp;amp; headers) const {
&amp;nbsp;&amp;nbsp;ScopeKeyPtr scope_key = scope_key_builder_.computeScopeKey(headers);
&amp;nbsp;&amp;nbsp;if (scope_key == nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return nullptr;
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;auto iter = scoped_route_info_by_key_.find(scope_key-&amp;gt;hash());
&amp;nbsp;&amp;nbsp;if (iter != scoped_route_info_by_key_.end()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return iter-&amp;gt;second-&amp;gt;routeConfig();
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;return nullptr;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;그외 route를 위한 사전 작업을 수행한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&amp;amp;&amp;amp; headers,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bool end_stream) {
&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;if (!state_.is_internally_created_) { // Only sanitize headers on first pass.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Modify the downstream remote address depending on configuration and headers.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto mutate_result = ConnectionManagerUtility::mutateRequestHeaders(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*request_headers_, connection_manager_.read_callbacks_-&amp;gt;connection(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.config_, *snapped_route_config_, connection_manager_.local_info_);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// IP detection failed, reject the request.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (mutate_result.reject_request.has_value()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto&amp;amp; reject_request_params = mutate_result.reject_request.value();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.stats_.named_.downstream_rq_rejected_via_ip_detection_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(reject_request_params.response_code, reject_request_params.body, nullptr,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().OriginalIPDetectionFailed);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;filter_manager_.setDownstreamRemoteAddress(mutate_result.final_remote_address);
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;...(중략)...
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음에 하는 일은 DownStreamRemoteAddress를 확인해서 설정하는 작업을 수행한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;filter_manager.h&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;void setDownstreamRemoteAddress(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const Network::Address::InstanceConstSharedPtr&amp;amp; downstream_remote_address) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stream_info_.setDownstreamRemoteAddress(downstream_remote_address);
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&amp;amp;&amp;amp; headers,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bool end_stream) {
&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;refreshCachedRoute();

&amp;nbsp;&amp;nbsp;if (!state_.is_internally_created_) { // Only mutate tracing headers on first pass.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().setTraceReason(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ConnectionManagerUtility::mutateTracingRequestHeader(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*request_headers_, connection_manager_.runtime_, connection_manager_.config_,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cached_route_.value().get()));
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().setRequestHeaders(*request_headers_);

&amp;nbsp;&amp;nbsp;const bool upgrade_rejected = filter_manager_.createFilterChain() == false;

&amp;nbsp;&amp;nbsp;// TODO if there are no filters when starting a filter iteration, the connection manager
&amp;nbsp;&amp;nbsp;// should return 404. The current returns no response if there is no router filter.
&amp;nbsp;&amp;nbsp;if (hasCachedRoute()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Do not allow upgrades if the route does not support it.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (upgrade_rejected) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// While downstream servers should not send upgrade payload without the upgrade being
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// accepted, err on the side of caution and refuse to process any further requests on this
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// connection, to avoid a class of HTTP/1.1 smuggling bugs where Upgrade or CONNECT payload
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// contains a smuggled HTTP request.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;state_.saw_connection_close_ = true;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.stats_.named_.downstream_rq_ws_on_non_ws_route_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendLocalReply(Code::Forbidden, &quot;&quot;, nullptr, absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().UpgradeFailed);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Allow non websocket requests to go through websocket enabled routes.
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Check if tracing is enabled.
&amp;nbsp;&amp;nbsp;if (connection_manager_tracing_config_.has_value()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;traceRequest();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;filter_manager_.decodeHeaders(*request_headers_, end_stream);

&amp;nbsp;&amp;nbsp;// Reset it here for both global and overridden cases.
&amp;nbsp;&amp;nbsp;resetIdleTimer();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그 다음에는 refreshCachedRoute()을 호출하여 Route를 결정한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::ActiveStream::refreshCachedRoute() { refreshCachedRoute(nullptr); }&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::ActiveStream::refreshCachedRoute(const Router::RouteCallback&amp;amp; cb) {
&amp;nbsp;&amp;nbsp;Router::RouteConstSharedPtr route;
&amp;nbsp;&amp;nbsp;if (request_headers_ != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (connection_manager_.config_.isRoutable() &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.config_.scopedRouteConfigProvider() != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// NOTE: re-select scope as well in case the scope key header has been changed by a filter.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapScopedRouteConfig();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (snapped_route_config_ != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;route = snapped_route_config_-&amp;gt;route(cb, *request_headers_, filter_manager_.streamInfo(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; stream_id_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;setRoute(route);
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::ActiveStream::snapScopedRouteConfig() {
&amp;nbsp;&amp;nbsp;// NOTE: if a RDS subscription hasn't got a RouteConfiguration back, a Router::NullConfigImpl is
&amp;nbsp;&amp;nbsp;// returned, in that case we let it pass.
&amp;nbsp;&amp;nbsp;snapped_route_config_ = snapped_scoped_routes_config_-&amp;gt;getRouteConfig(*request_headers_);
&amp;nbsp;&amp;nbsp;if (snapped_route_config_ == nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(trace, &quot;can't find SRDS scope.&quot;, *this);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// TODO(stevenzzzz): Consider to pass an error message to router filter, so that it can
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// send back 404 with some more details.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;snapped_route_config_ = std::make_shared&amp;lt;Router::NullConfigImpl&amp;gt;();
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Router::RouteConstSharedPtr
ConnectionManagerImpl::ActiveStream::route(const Router::RouteCallback&amp;amp; cb) {
&amp;nbsp;&amp;nbsp;if (cached_route_.has_value()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return cached_route_.value();
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;refreshCachedRoute(cb);
&amp;nbsp;&amp;nbsp;return cached_route_.value();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;/**
 * Sets the cached route to the RouteConstSharedPtr argument passed in. Handles setting the
 * cached_route_/cached_cluster_info_ ActiveStream attributes, the FilterManager streamInfo, tracing
 * tags, and timeouts.
 *
 * Declared as a StreamFilterCallbacks member function for filters to call directly, but also
 * functions as a helper to refreshCachedRoute(const Router::RouteCallback&amp;amp; cb).
 */
void ConnectionManagerImpl::ActiveStream::setRoute(Router::RouteConstSharedPtr route) {
&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().route_ = route;
&amp;nbsp;&amp;nbsp;cached_route_ = std::move(route);
&amp;nbsp;&amp;nbsp;if (nullptr == filter_manager_.streamInfo().route() ||
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;nullptr == filter_manager_.streamInfo().route()-&amp;gt;routeEntry()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cached_cluster_info_ = nullptr;
&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Upstream::ThreadLocalCluster* local_cluster =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;connection_manager_.cluster_manager_.getThreadLocalCluster(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().route()-&amp;gt;routeEntry()-&amp;gt;clusterName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cached_cluster_info_ = (nullptr == local_cluster) ? nullptr : local_cluster-&amp;gt;info();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().setUpstreamClusterInfo(cached_cluster_info_.value());
&amp;nbsp;&amp;nbsp;refreshCachedTracingCustomTags();
&amp;nbsp;&amp;nbsp;refreshDurationTimeout();
&amp;nbsp;&amp;nbsp;refreshIdleTimeout();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;cluster_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;ThreadLocalCluster* ClusterManagerImpl::getThreadLocalCluster(absl::string_view cluster) {
&amp;nbsp;&amp;nbsp;ThreadLocalClusterManagerImpl&amp;amp; cluster_manager = *tls_;

&amp;nbsp;&amp;nbsp;auto entry = cluster_manager.thread_local_clusters_.find(cluster);
&amp;nbsp;&amp;nbsp;if (entry != cluster_manager.thread_local_clusters_.end()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return entry-&amp;gt;second.get();
&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return nullptr;
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;stream_info_impl.h&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;void setUpstreamClusterInfo(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const Upstream::ClusterInfoConstSharedPtr&amp;amp; upstream_cluster_info) override {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;upstream_cluster_info_ = upstream_cluster_info;
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;upstream cluster 정보를 등록하고 만약 tracing 정보가 추가되어야한다면 해당 정보를 추가한다.&lt;br&gt;그리고 Timeout 관련해서 refresh를 수행한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;stream_info_impl.h&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;void setUpstreamClusterInfo(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const Upstream::ClusterInfoConstSharedPtr&amp;amp; upstream_cluster_info) override {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;upstream_cluster_info_ = upstream_cluster_info;
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&amp;amp;&amp;amp; headers,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bool end_stream) {
&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;filter_manager_.streamInfo().setRequestHeaders(*request_headers_);

&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;// Check if tracing is enabled.
&amp;nbsp;&amp;nbsp;if (connection_manager_tracing_config_.has_value()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;traceRequest();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;filter_manager_.decodeHeaders(*request_headers_, end_stream);

&amp;nbsp;&amp;nbsp;// Reset it here for both global and overridden cases.
&amp;nbsp;&amp;nbsp;resetIdleTimer();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그 다음에는 filter_manager에게 request header 정보를 저장하고 decodeheaders를 통해서 Route 과정을 수행할 수 있또록 진행한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;filter_manager.h&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;void decodeHeaders(RequestHeaderMap&amp;amp; headers, bool end_stream) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;state_.remote_decode_complete_ = end_stream;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;decodeHeaders(nullptr, headers, end_stream);
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;filter_manager.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void FilterManager::decodeHeaders(ActiveStreamDecoderFilter* filter, RequestHeaderMap&amp;amp; headers,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bool end_stream) {
&amp;nbsp;&amp;nbsp;// Headers filter iteration should always start with the next filter if available.
&amp;nbsp;&amp;nbsp;std::list&amp;lt;ActiveStreamDecoderFilterPtr&amp;gt;::iterator entry =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;commonDecodePrefix(filter, FilterIterationStartState::AlwaysStartFromNext);
&amp;nbsp;&amp;nbsp;std::list&amp;lt;ActiveStreamDecoderFilterPtr&amp;gt;::iterator continue_data_entry = decoder_filters_.end();

&amp;nbsp;&amp;nbsp;for (; entry != decoder_filters_.end(); entry++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(!(state_.filter_call_state_ &amp;amp; FilterCallState::DecodeHeaders));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;state_.filter_call_state_ |= FilterCallState::DecodeHeaders;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(*entry)-&amp;gt;end_stream_ = (end_stream &amp;amp;&amp;amp; continue_data_entry == decoder_filters_.end());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FilterHeadersStatus status = (*entry)-&amp;gt;decodeHeaders(headers, (*entry)-&amp;gt;end_stream_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (state_.decoder_filter_chain_aborted_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(trace,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;decodeHeaders filter iteration aborted due to local reply: filter={}&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; *this, (*entry)-&amp;gt;filter_context_.config_name);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;status = FilterHeadersStatus::StopIteration;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(!(status == FilterHeadersStatus::ContinueAndDontEndStream &amp;amp;&amp;amp; !(*entry)-&amp;gt;end_stream_),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;Filters should not return FilterHeadersStatus::ContinueAndDontEndStream from &quot;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &quot;decodeHeaders when end_stream is already false&quot;);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;state_.filter_call_state_ &amp;amp;= ~FilterCallState::DecodeHeaders;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(trace, &quot;decode headers called: filter={} status={}&quot;, *this,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; (*entry)-&amp;gt;filter_context_.config_name, static_cast&amp;lt;uint64_t&amp;gt;(status));

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(*entry)-&amp;gt;decode_headers_called_ = true;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto continue_iteration = (*entry)-&amp;gt;commonHandleAfterHeadersCallback(status, end_stream);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_BUG(!continue_iteration || !state_.local_complete_,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;Filter did not return StopAll or StopIteration after sending a local reply.&quot;);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// If this filter ended the stream, decodeComplete() should be called for it.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if ((*entry)-&amp;gt;end_stream_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(*entry)-&amp;gt;handle_-&amp;gt;decodeComplete();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Skip processing metadata after sending local reply
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (state_.local_complete_ &amp;amp;&amp;amp; std::next(entry) != decoder_filters_.end()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;maybeContinueDecoding(continue_data_entry);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const bool new_metadata_added = processNewlyAddedMetadata();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// If end_stream is set in headers, and a filter adds new metadata, we need to delay end_stream
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// in headers by inserting an empty data frame with end_stream set. The empty data frame is sent
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// after the new metadata.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if ((*entry)-&amp;gt;end_stream_ &amp;amp;&amp;amp; new_metadata_added &amp;amp;&amp;amp; !buffered_request_data_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Buffer::OwnedImpl empty_data(&quot;&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;trace, &quot;inserting an empty data frame for end_stream due metadata being added.&quot;, *this);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Metadata frame doesn't carry end of stream bit. We need an empty data frame to end the
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// stream.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;addDecodedData(*((*entry).get()), empty_data, true);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!continue_iteration &amp;amp;&amp;amp; std::next(entry) != decoder_filters_.end()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Stop iteration IFF this is not the last filter. If it is the last filter, continue with
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// processing since we need to handle the case where a terminal filter wants to buffer, but
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// a previous filter has added body.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;maybeContinueDecoding(continue_data_entry);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Here we handle the case where we have a header only request, but a filter adds a body
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// to it. We need to not raise end_stream = true to further filters during inline iteration.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (end_stream &amp;amp;&amp;amp; buffered_request_data_ &amp;amp;&amp;amp; continue_data_entry == decoder_filters_.end()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;continue_data_entry = entry;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;maybeContinueDecoding(continue_data_entry);

&amp;nbsp;&amp;nbsp;if (end_stream) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;disarmRequestTimeout();
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;그러면 HTTP Connection Manager에는 cors, fault, router 등과 같은 필터가 있을 수 있는데, 해당 필터를 수행한다. 여기서는 router가 수행됨을 가정해보자.&lt;br&gt;&amp;nbsp;&lt;br&gt;router\config.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Http::FilterFactoryCb RouterFilterConfig::createFilterFactoryFromProtoTyped(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const envoy::extensions::filters::http::router::v3::Router&amp;amp; proto_config,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const std::string&amp;amp; stat_prefix, Server::Configuration::FactoryContext&amp;amp; context) {
&amp;nbsp;&amp;nbsp;Stats::StatNameManagedStorage prefix(stat_prefix, context.scope().symbolTable());
&amp;nbsp;&amp;nbsp;Router::FilterConfigSharedPtr filter_config(new Router::FilterConfig(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;prefix.statName(), context,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::make_unique&amp;lt;Router::ShadowWriterImpl&amp;gt;(context.clusterManager()), proto_config));

&amp;nbsp;&amp;nbsp;return [filter_config](Http::FilterChainFactoryCallbacks&amp;amp; callbacks) -&amp;gt; void {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks.addStreamDecoderFilter(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::make_shared&amp;lt;Router::ProdFilter&amp;gt;(*filter_config, filter_config-&amp;gt;default_stats_));
&amp;nbsp;&amp;nbsp;};
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;filter_manager.h&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;void addStreamDecoderFilter(ActiveStreamDecoderFilterPtr filter) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Note: configured decoder filters are appended to decoder_filters_.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// This means that if filters are configured in the following order (assume all three filters
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// are both decoder/encoder filters):
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//&amp;nbsp;&amp;nbsp; http_filters:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - A
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - B
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; - C
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// The decoder filter chain will iterate through filters A, B, C.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;LinkedList::moveIntoListBack(std::move(filter), decoder_filters_);
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;참고로&amp;nbsp; Router 필터 등록 시에는 위와 같은 필터가 decoder_filters로 등록되어있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;router.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap&amp;amp; headers, bool end_stream) {
&amp;nbsp;&amp;nbsp;downstream_headers_ = &amp;amp;headers;

&amp;nbsp;&amp;nbsp;// Extract debug configuration from filter state. This is used further along to determine whether
&amp;nbsp;&amp;nbsp;// we should append cluster and host headers to the response, and whether to forward the request
&amp;nbsp;&amp;nbsp;// upstream.
&amp;nbsp;&amp;nbsp;const StreamInfo::FilterStateSharedPtr&amp;amp; filter_state = callbacks_-&amp;gt;streamInfo().filterState();
&amp;nbsp;&amp;nbsp;const DebugConfig* debug_config = filter_state-&amp;gt;getDataReadOnly&amp;lt;DebugConfig&amp;gt;(DebugConfig::key());

&amp;nbsp;&amp;nbsp;// TODO: Maybe add a filter API for this.
&amp;nbsp;&amp;nbsp;grpc_request_ = Grpc::Common::isGrpcRequestHeaders(headers);
&amp;nbsp;&amp;nbsp;exclude_http_code_stats_ = grpc_request_ &amp;amp;&amp;amp; config_.suppress_grpc_request_failure_code_stats_;

&amp;nbsp;&amp;nbsp;// Only increment rq total stat if we actually decode headers here. This does not count requests
&amp;nbsp;&amp;nbsp;// that get handled by earlier filters.
&amp;nbsp;&amp;nbsp;stats_.rq_total_.inc();

&amp;nbsp;&amp;nbsp;// Initialize the `modify_headers` function as a no-op (so we don't have to remember to check it
&amp;nbsp;&amp;nbsp;// against nullptr before calling it), and feed it behavior later if/when we have cluster info
&amp;nbsp;&amp;nbsp;// headers to append.
&amp;nbsp;&amp;nbsp;std::function&amp;lt;void(Http::ResponseHeaderMap&amp;amp;)&amp;gt; modify_headers = [](Http::ResponseHeaderMap&amp;amp;) {};

&amp;nbsp;&amp;nbsp;// Determine if there is a route entry or a direct response for the request.
&amp;nbsp;&amp;nbsp;route_ = callbacks_-&amp;gt;route();
&amp;nbsp;&amp;nbsp;if (!route_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stats_.no_route_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(debug, &quot;no route match for URL '{}'&quot;, *callbacks_, headers.getPathValue());

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoRouteFound);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;sendLocalReply(Http::Code::NotFound, &quot;&quot;, modify_headers, absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().RouteNotFound);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Http::FilterHeadersStatus::StopIteration;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Determine if there is a direct response for the request.
&amp;nbsp;&amp;nbsp;const auto* direct_response = route_-&amp;gt;directResponseEntry();
&amp;nbsp;&amp;nbsp;if (direct_response != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stats_.rq_direct_response_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;direct_response-&amp;gt;rewritePathHeader(headers, !config_.suppress_envoy_headers_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;streamInfo().setRouteName(direct_response-&amp;gt;routeName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;sendLocalReply(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;direct_response-&amp;gt;responseCode(), direct_response-&amp;gt;responseBody(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[this, direct_response,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &amp;amp;request_headers = headers](Http::ResponseHeaderMap&amp;amp; response_headers) -&amp;gt; void {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::string new_path;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (request_headers.Path()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;new_path = direct_response-&amp;gt;newPath(request_headers);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// See https://tools.ietf.org/html/rfc7231#section-7.1.2.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto add_location =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;direct_response-&amp;gt;responseCode() == Http::Code::Created ||
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Http::CodeUtility::is3xx(enumToInt(direct_response-&amp;gt;responseCode()));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!new_path.empty() &amp;amp;&amp;amp; add_location) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;response_headers.addReferenceKey(Http::Headers::get().Location, new_path);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;direct_response-&amp;gt;finalizeResponseHeaders(response_headers, callbacks_-&amp;gt;streamInfo());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;absl::nullopt, StreamInfo::ResponseCodeDetails::get().DirectResponse);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Http::FilterHeadersStatus::StopIteration;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// A route entry matches for the request.
&amp;nbsp;&amp;nbsp;route_entry_ = route_-&amp;gt;routeEntry();
&amp;nbsp;&amp;nbsp;// If there's a route specific limit and it's smaller than general downstream
&amp;nbsp;&amp;nbsp;// limits, apply the new cap.
&amp;nbsp;&amp;nbsp;retry_shadow_buffer_limit_ =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::min(retry_shadow_buffer_limit_, route_entry_-&amp;gt;retryShadowBufferLimit());
&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;streamInfo().setRouteName(route_entry_-&amp;gt;routeName());
&amp;nbsp;&amp;nbsp;if (debug_config &amp;amp;&amp;amp; debug_config-&amp;gt;append_cluster_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// The cluster name will be appended to any local or upstream responses from this point.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modify_headers = [this, debug_config](Http::ResponseHeaderMap&amp;amp; headers) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.addCopy(debug_config-&amp;gt;cluster_header_.value_or(Http::Headers::get().EnvoyCluster),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;route_entry_-&amp;gt;clusterName());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;Upstream::ThreadLocalCluster* cluster =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;config_.cm_.getThreadLocalCluster(route_entry_-&amp;gt;clusterName());
&amp;nbsp;&amp;nbsp;if (!cluster) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stats_.no_cluster_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(debug, &quot;unknown cluster '{}'&quot;, *callbacks_, route_entry_-&amp;gt;clusterName());

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::NoClusterFound);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;sendLocalReply(route_entry_-&amp;gt;clusterNotFoundResponseCode(), &quot;&quot;, modify_headers,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StreamInfo::ResponseCodeDetails::get().ClusterNotFound);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Http::FilterHeadersStatus::StopIteration;
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;cluster_ = cluster-&amp;gt;info();

&amp;nbsp;&amp;nbsp;// Set up stat prefixes, etc.
&amp;nbsp;&amp;nbsp;request_vcluster_ = route_entry_-&amp;gt;virtualCluster(headers);
&amp;nbsp;&amp;nbsp;if (request_vcluster_ != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;streamInfo().setVirtualClusterName(request_vcluster_-&amp;gt;name());
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;route_stats_context_ = route_entry_-&amp;gt;routeStatsContext();
&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(debug, &quot;cluster '{}' match for URL '{}'&quot;, *callbacks_,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; route_entry_-&amp;gt;clusterName(), headers.getPathValue());

&amp;nbsp;&amp;nbsp;if (config_.strict_check_headers_ != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (const auto&amp;amp; header : *config_.strict_check_headers_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto res = FilterUtility::StrictHeaderChecker::checkHeader(headers, header);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!res.valid_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;streamInfo().setResponseFlag(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;StreamInfo::ResponseFlag::InvalidEnvoyRequestHeaders);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const std::string body = fmt::format(&quot;invalid header '{}' with value '{}'&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; std::string(res.entry_-&amp;gt;key().getStringView()),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; std::string(res.entry_-&amp;gt;value().getStringView()));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const std::string details =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;absl::StrCat(StreamInfo::ResponseCodeDetails::get().InvalidEnvoyRequestHeaders, &quot;{&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; StringUtil::replaceAllEmptySpace(res.entry_-&amp;gt;key().getStringView()), &quot;}&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;sendLocalReply(Http::Code::BadRequest, body, nullptr, absl::nullopt, details);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Http::FilterHeadersStatus::StopIteration;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;const Http::HeaderEntry* request_alt_name = headers.EnvoyUpstreamAltStatName();
&amp;nbsp;&amp;nbsp;if (request_alt_name) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;alt_stat_prefix_ = std::make_unique&amp;lt;Stats::StatNameDynamicStorage&amp;gt;(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;request_alt_name-&amp;gt;value().getStringView(), config_.scope_.symbolTable());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.removeEnvoyUpstreamAltStatName();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// See if we are supposed to immediately kill some percentage of this cluster's traffic.
&amp;nbsp;&amp;nbsp;if (cluster_-&amp;gt;maintenanceMode()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UpstreamOverflow);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;chargeUpstreamCode(Http::Code::ServiceUnavailable, nullptr, true);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;sendLocalReply(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Http::Code::ServiceUnavailable, &quot;maintenance mode&quot;,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;[modify_headers, this](Http::ResponseHeaderMap&amp;amp; headers) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!config_.suppress_envoy_headers_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.addReference(Http::Headers::get().EnvoyOverloaded,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Http::Headers::get().EnvoyOverloadedValues.True);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Note: append_cluster_info does not respect suppress_envoy_headers.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modify_headers(headers);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;},
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;absl::nullopt, StreamInfo::ResponseCodeDetails::get().MaintenanceMode);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;cluster_-&amp;gt;stats().upstream_rq_maintenance_mode_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Http::FilterHeadersStatus::StopIteration;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// Fetch a connection pool for the upstream cluster.
&amp;nbsp;&amp;nbsp;const auto&amp;amp; upstream_http_protocol_options = cluster_-&amp;gt;upstreamHttpProtocolOptions();

&amp;nbsp;&amp;nbsp;if (upstream_http_protocol_options.has_value() &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(upstream_http_protocol_options.value().auto_sni() ||
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; upstream_http_protocol_options.value().auto_san_validation())) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Default the header to Host/Authority header.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;absl::string_view header_value = headers.getHostValue();

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Check whether `override_auto_sni_header` is specified.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto override_auto_sni_header =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;upstream_http_protocol_options.value().override_auto_sni_header();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!override_auto_sni_header.empty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Use the header value from `override_auto_sni_header` to set the SNI value.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto overridden_header_value = Http::HeaderUtility::getAllOfHeaderAsString(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers, Http::LowerCaseString(override_auto_sni_header));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (overridden_header_value.result().has_value() &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;!overridden_header_value.result().value().empty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;header_value = overridden_header_value.result().value();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto parsed_authority = Http::Utility::parseAuthority(header_value);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;bool should_set_sni = !parsed_authority.is_ip_address_;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// `host_` returns a string_view so doing this should be safe.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;absl::string_view sni_value = parsed_authority.host_;

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (should_set_sni &amp;amp;&amp;amp; upstream_http_protocol_options.value().auto_sni()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;streamInfo().filterState()-&amp;gt;setData(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Network::UpstreamServerName::key(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::make_unique&amp;lt;Network::UpstreamServerName&amp;gt;(sni_value),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;StreamInfo::FilterState::StateType::Mutable);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (upstream_http_protocol_options.value().auto_san_validation()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;streamInfo().filterState()-&amp;gt;setData(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Network::UpstreamSubjectAltNames::key(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::make_unique&amp;lt;Network::UpstreamSubjectAltNames&amp;gt;(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;std::vector&amp;lt;std::string&amp;gt;{std::string(sni_value)}),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;StreamInfo::FilterState::StateType::Mutable);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;transport_socket_options_ = Network::TransportSocketOptionsUtility::fromFilterState(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*callbacks_-&amp;gt;streamInfo().filterState());

&amp;nbsp;&amp;nbsp;if (auto downstream_connection = downstreamConnection(); downstream_connection != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (auto typed_state = downstream_connection-&amp;gt;streamInfo()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .filterState()
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; .getDataReadOnly&amp;lt;Network::UpstreamSocketOptionsFilterState&amp;gt;(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; Network::UpstreamSocketOptionsFilterState::key());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;typed_state != nullptr) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;auto downstream_options = typed_state-&amp;gt;value();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!upstream_options_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;upstream_options_ = std::make_shared&amp;lt;Network::Socket::Options&amp;gt;();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Network::Socket::appendOptions(upstream_options_, downstream_options);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;if (upstream_options_ &amp;amp;&amp;amp; callbacks_-&amp;gt;getUpstreamSocketOptions()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Network::Socket::appendOptions(upstream_options_, callbacks_-&amp;gt;getUpstreamSocketOptions());
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;std::unique_ptr&amp;lt;GenericConnPool&amp;gt; generic_conn_pool = createConnPool(*cluster);

&amp;nbsp;&amp;nbsp;if (!generic_conn_pool) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;sendNoHealthyUpstreamResponse();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Http::FilterHeadersStatus::StopIteration;
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;Upstream::HostDescriptionConstSharedPtr host = generic_conn_pool-&amp;gt;host();

&amp;nbsp;&amp;nbsp;if (debug_config &amp;amp;&amp;amp; debug_config-&amp;gt;append_upstream_host_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// The hostname and address will be appended to any local or upstream responses from this point,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// possibly in addition to the cluster name.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modify_headers = [modify_headers, debug_config, host](Http::ResponseHeaderMap&amp;amp; headers) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modify_headers(headers);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.addCopy(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;debug_config-&amp;gt;hostname_header_.value_or(Http::Headers::get().EnvoyUpstreamHostname),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;host-&amp;gt;hostname());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.addCopy(debug_config-&amp;gt;host_address_header_.value_or(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Http::Headers::get().EnvoyUpstreamHostAddress),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;host-&amp;gt;address()-&amp;gt;asString());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// If we've been instructed not to forward the request upstream, send an empty local response.
&amp;nbsp;&amp;nbsp;if (debug_config &amp;amp;&amp;amp; debug_config-&amp;gt;do_not_forward_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modify_headers = [modify_headers, debug_config](Http::ResponseHeaderMap&amp;amp; headers) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modify_headers(headers);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.addCopy(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;debug_config-&amp;gt;not_forwarded_header_.value_or(Http::Headers::get().EnvoyNotForwarded),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&quot;true&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;sendLocalReply(Http::Code::NoContent, &quot;&quot;, modify_headers, absl::nullopt, &quot;&quot;);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Http::FilterHeadersStatus::StopIteration;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;hedging_params_ = FilterUtility::finalHedgingParams(*route_entry_, headers);

&amp;nbsp;&amp;nbsp;timeout_ = FilterUtility::finalTimeout(*route_entry_, headers, !config_.suppress_envoy_headers_,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; grpc_request_, hedging_params_.hedge_on_per_try_timeout_,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; config_.respect_expected_rq_timeout_);

&amp;nbsp;&amp;nbsp;const Http::HeaderEntry* header_max_stream_duration_entry =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.EnvoyUpstreamStreamDurationMs();
&amp;nbsp;&amp;nbsp;if (header_max_stream_duration_entry) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dynamic_max_stream_duration_ =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FilterUtility::tryParseHeaderTimeout(*header_max_stream_duration_entry);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.removeEnvoyUpstreamStreamDurationMs();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// If this header is set with any value, use an alternate response code on timeout
&amp;nbsp;&amp;nbsp;if (headers.EnvoyUpstreamRequestTimeoutAltResponse()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;timeout_response_code_ = Http::Code::NoContent;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.removeEnvoyUpstreamRequestTimeoutAltResponse();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;include_attempt_count_in_request_ = route_entry_-&amp;gt;includeAttemptCountInRequest();
&amp;nbsp;&amp;nbsp;if (include_attempt_count_in_request_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.setEnvoyAttemptCount(attempt_count_);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// The router has reached a point where it is going to try to send a request upstream,
&amp;nbsp;&amp;nbsp;// so now modify_headers should attach x-envoy-attempt-count to the downstream response if the
&amp;nbsp;&amp;nbsp;// config flag is true.
&amp;nbsp;&amp;nbsp;if (route_entry_-&amp;gt;includeAttemptCountInResponse()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modify_headers = [modify_headers, this](Http::ResponseHeaderMap&amp;amp; headers) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;modify_headers(headers);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// This header is added without checking for config_.suppress_envoy_headers_ to mirror what is
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// done for upstream requests.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers.setEnvoyAttemptCount(attempt_count_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;};
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;callbacks_-&amp;gt;streamInfo().setAttemptCount(attempt_count_);

&amp;nbsp;&amp;nbsp;route_entry_-&amp;gt;finalizeRequestHeaders(headers, callbacks_-&amp;gt;streamInfo(),
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; !config_.suppress_envoy_headers_);
&amp;nbsp;&amp;nbsp;FilterUtility::setUpstreamScheme(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;headers, callbacks_-&amp;gt;streamInfo().downstreamAddressProvider().sslConnection() != nullptr);

&amp;nbsp;&amp;nbsp;// Ensure an http transport scheme is selected before continuing with decoding.
&amp;nbsp;&amp;nbsp;ASSERT(headers.Scheme());

&amp;nbsp;&amp;nbsp;retry_state_ =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;createRetryState(route_entry_-&amp;gt;retryPolicy(), headers, *cluster_, request_vcluster_,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; route_stats_context_, config_.runtime_, config_.random_,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; callbacks_-&amp;gt;dispatcher(), config_.timeSource(), route_entry_-&amp;gt;priority());

&amp;nbsp;&amp;nbsp;// Determine which shadow policies to use. It's possible that we don't do any shadowing due to
&amp;nbsp;&amp;nbsp;// runtime keys. Also the method CONNECT doesn't support shadowing.
&amp;nbsp;&amp;nbsp;auto method = headers.getMethodValue();
&amp;nbsp;&amp;nbsp;if (method != Http::Headers::get().MethodValues.Connect) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;for (const auto&amp;amp; shadow_policy : route_entry_-&amp;gt;shadowPolicies()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;const auto&amp;amp; policy_ref = *shadow_policy;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (FilterUtility::shouldShadow(policy_ref, config_.runtime_, callbacks_-&amp;gt;streamId())) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;active_shadow_policies_.push_back(std::cref(policy_ref));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;shadow_headers_ = Http::createHeaderMap&amp;lt;Http::RequestHeaderMapImpl&amp;gt;(*downstream_headers_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;ENVOY_STREAM_LOG(debug, &quot;router decoding headers:\n{}&quot;, *callbacks_, headers);

&amp;nbsp;&amp;nbsp;// Hang onto the modify_headers function for later use in handling upstream responses.
&amp;nbsp;&amp;nbsp;modify_headers_ = modify_headers;

&amp;nbsp;&amp;nbsp;conn_pool_new_stream_with_early_data_and_http3_ =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;Runtime::runtimeFeatureEnabled(Runtime::conn_pool_new_stream_with_early_data_and_http3);
&amp;nbsp;&amp;nbsp;const bool can_send_early_data =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;conn_pool_new_stream_with_early_data_and_http3_ &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;route_entry_-&amp;gt;earlyDataPolicy().allowsEarlyDataForRequest(*downstream_headers_);
&amp;nbsp;&amp;nbsp;// Set initial HTTP/3 use based on the presence of HTTP/1.1 proxy config.
&amp;nbsp;&amp;nbsp;// For retries etc, HTTP/3 usability may transition from true to false, but
&amp;nbsp;&amp;nbsp;// will never transition from false to true.
&amp;nbsp;&amp;nbsp;bool can_use_http3 =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;!transport_socket_options_ || !transport_socket_options_-&amp;gt;http11ProxyInfo().has_value();
&amp;nbsp;&amp;nbsp;UpstreamRequestPtr upstream_request = std::make_unique&amp;lt;UpstreamRequest&amp;gt;(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;*this, std::move(generic_conn_pool), can_send_early_data, can_use_http3);
&amp;nbsp;&amp;nbsp;LinkedList::moveIntoList(std::move(upstream_request), upstream_requests_);
&amp;nbsp;&amp;nbsp;upstream_requests_.front()-&amp;gt;acceptHeadersFromRouter(end_stream);
&amp;nbsp;&amp;nbsp;if (end_stream) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;onRequestComplete();
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;return Http::FilterHeadersStatus::StopIteration;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;legacy_parser_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;CallbackResult LegacyHttpParserImpl::pause() { return impl_-&amp;gt;pause(); }&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;legacy_parser_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;CallbackResult pause() {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;http_parser_pause(&amp;amp;parser_, 1);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return CallbackResult::Success;
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;http_parser.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void
http_parser_pause(http_parser *parser, int paused) {
&amp;nbsp;&amp;nbsp;/* Users should only be pausing/unpausing a parser that is not in an error
&amp;nbsp;&amp;nbsp; * state. In non-debug builds, there's not much that we can do about this
&amp;nbsp;&amp;nbsp; * other than ignore it.
&amp;nbsp;&amp;nbsp; */
&amp;nbsp;&amp;nbsp;if (HTTP_PARSER_ERRNO(parser) == HPE_OK ||
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;HTTP_PARSER_ERRNO(parser) == HPE_PAUSED) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;uint32_t nread = parser-&amp;gt;nread; /* used by the SET_ERRNO macro */
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;SET_ERRNO((paused) ? HPE_PAUSED : HPE_OK);
&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;assert(0 &amp;amp;&amp;amp; &quot;Attempting to pause parser in error state&quot;);
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;Parsing 과정이 끝나면 다시 dispatchSlice에서부터 시작함&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Envoy::StatusOr&amp;lt;size_t&amp;gt; ConnectionImpl::dispatchSlice(const char* slice, size_t len) {
&amp;nbsp;&amp;nbsp;ASSERT(codec_status_.ok() &amp;amp;&amp;amp; dispatching_);
&amp;nbsp;&amp;nbsp;const size_t nread = parser_-&amp;gt;execute(slice, len);
&amp;nbsp;&amp;nbsp;if (!codec_status_.ok()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return codec_status_;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;const ParserStatus status = parser_-&amp;gt;getStatus();
&amp;nbsp;&amp;nbsp;if (status != ParserStatus::Ok &amp;amp;&amp;amp; status != ParserStatus::Paused) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;absl::string_view error = Http1ResponseCodeDetails::get().HttpCodecError;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (Runtime::runtimeFeatureEnabled(&quot;envoy.reloadable_features.http1_use_balsa_parser&quot;)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (parser_-&amp;gt;errorMessage() == &quot;headers size exceeds limit&quot; ||
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parser_-&amp;gt;errorMessage() == &quot;trailers size exceeds limit&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;error = Http1ResponseCodeDetails::get().HeadersTooLarge;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else if (parser_-&amp;gt;errorMessage() == &quot;header value contains invalid chars&quot;) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;error = Http1ResponseCodeDetails::get().InvalidCharacters;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;RETURN_IF_ERROR(sendProtocolError(error));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Avoid overwriting the codec_status_ set in the callbacks.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(codec_status_.ok());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;codec_status_ =
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;codecProtocolError(absl::StrCat(&quot;http/1.1 protocol error: &quot;, parser_-&amp;gt;errorMessage()));
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return codec_status_;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;return nread;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Parsing 과정이 끝나면 Parser는 Paused 상태가 되니 Parser를 통해 읽은 size 개수를 반환한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Http::Status ConnectionImpl::dispatch(Buffer::Instance&amp;amp; data) {
&amp;nbsp;&amp;nbsp;...(중략),,,

&amp;nbsp;&amp;nbsp;ssize_t total_parsed = 0;
&amp;nbsp;&amp;nbsp;if (data.length() &amp;gt; 0) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;current_dispatching_buffer_ = &amp;amp;data;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while (data.length() &amp;gt; 0) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...(중략)...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!dispatching_slice_already_drained_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(statusor_parsed.value() &amp;lt;= slice.len_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;data.drain(statusor_parsed.value());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;total_parsed += statusor_parsed.value();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (parser_-&amp;gt;getStatus() != ParserStatus::Ok) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Parse errors trigger an exception in dispatchSlice so we are guaranteed to be paused at
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// this point.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(parser_-&amp;gt;getStatus() == ParserStatus::Paused);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;current_dispatching_buffer_ = nullptr;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dispatchBufferedBody();
&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;auto result = dispatchSlice(nullptr, 0);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!result.ok()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return result.status();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;ASSERT(buffered_body_.length() == 0);

&amp;nbsp;&amp;nbsp;ENVOY_CONN_LOG(trace, &quot;parsed {} bytes&quot;, connection_, total_parsed);

&amp;nbsp;&amp;nbsp;// If an upgrade has been handled and there is body data or early upgrade
&amp;nbsp;&amp;nbsp;// payload to send on, send it on.
&amp;nbsp;&amp;nbsp;maybeDirectDispatch(data);
&amp;nbsp;&amp;nbsp;return Http::okStatus();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;buffer_impl.h&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;&amp;nbsp;&amp;nbsp;void drain(uint64_t size) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(data_ + size &amp;lt;= reservable_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;data_ += size;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (data_ == reservable_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// All the data in the slice has been drained. Reset the offsets so all
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// the data can be reused.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;data_ = 0;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;reservable_ = 0;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Http::Status ConnectionImpl::dispatch(Buffer::Instance&amp;amp; data) {
&amp;nbsp;&amp;nbsp;...(중략),,,

&amp;nbsp;&amp;nbsp;ssize_t total_parsed = 0;
&amp;nbsp;&amp;nbsp;if (data.length() &amp;gt; 0) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;current_dispatching_buffer_ = &amp;amp;data;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;while (data.length() &amp;gt; 0) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...(중략)...
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (parser_-&amp;gt;getStatus() != ParserStatus::Ok) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Parse errors trigger an exception in dispatchSlice so we are guaranteed to be paused at
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// this point.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(parser_-&amp;gt;getStatus() == ParserStatus::Paused);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;break;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;current_dispatching_buffer_ = nullptr;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dispatchBufferedBody();
&amp;nbsp;&amp;nbsp;} else {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;auto result = dispatchSlice(nullptr, 0);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (!result.ok()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return result.status();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;ASSERT(buffered_body_.length() == 0);

&amp;nbsp;&amp;nbsp;ENVOY_CONN_LOG(trace, &quot;parsed {} bytes&quot;, connection_, total_parsed);

&amp;nbsp;&amp;nbsp;// If an upgrade has been handled and there is body data or early upgrade
&amp;nbsp;&amp;nbsp;// payload to send on, send it on.
&amp;nbsp;&amp;nbsp;maybeDirectDispatch(data);
&amp;nbsp;&amp;nbsp;return Http::okStatus();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;dispatch가 완료되면 parser 상태가 중지이므로 do-while 문을 벗어난다.&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionImpl::dispatchBufferedBody() {
&amp;nbsp;&amp;nbsp;ASSERT(parser_-&amp;gt;getStatus() == ParserStatus::Ok || parser_-&amp;gt;getStatus() == ParserStatus::Paused);
&amp;nbsp;&amp;nbsp;ASSERT(codec_status_.ok());
&amp;nbsp;&amp;nbsp;if (buffered_body_.length() &amp;gt; 0) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;onBody(buffered_body_);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;buffered_body_.drain(buffered_body_.length());
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;parser가 완료되면 body length가 없으므로 처리하는 내용은 없다.&lt;br&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool ConnectionImpl::maybeDirectDispatch(Buffer::Instance&amp;amp; data) {
&amp;nbsp;&amp;nbsp;if (!handling_upgrade_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Only direct dispatch for Upgrade requests.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return false;
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;ENVOY_CONN_LOG(trace, &quot;direct-dispatched {} bytes&quot;, connection_, data.length());
&amp;nbsp;&amp;nbsp;onBody(data);
&amp;nbsp;&amp;nbsp;data.drain(data.length());
&amp;nbsp;&amp;nbsp;return true;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;parser가 완료되면 return fals를 통해 더 이상 데이터 fetch를 수행하지 않는다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;549&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfAvLs/btrSqHuPxBb/Th84ptsAmNFmBHAjNEOrjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfAvLs/btrSqHuPxBb/Th84ptsAmNFmBHAjNEOrjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfAvLs/btrSqHuPxBb/Th84ptsAmNFmBHAjNEOrjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfAvLs%2FbtrSqHuPxBb%2FTh84ptsAmNFmBHAjNEOrjK%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;549&quot; height=&quot;393&quot; data-origin-width=&quot;549&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;codec_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Http::Status ServerConnectionImpl::dispatch(Buffer::Instance&amp;amp; data) {
&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;if (active_request_ != nullptr &amp;amp;&amp;amp; active_request_-&amp;gt;remote_complete_) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Read disable the connection if the downstream is sending additional data while we are working
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// on an existing request. Reading from the connection will be re-enabled after the active
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// request is completed.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (data.length() &amp;gt; 0) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;active_request_-&amp;gt;response_encoder_.readDisable(true);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;return status;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;상태를 반환한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Network::FilterStatus ConnectionManagerImpl::onData(Buffer::Instance&amp;amp; data, bool) {
...(중략)...

&amp;nbsp;&amp;nbsp;bool redispatch;
&amp;nbsp;&amp;nbsp;do {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (isBufferFloodError(status) || isInboundFramesWithEmptyPayloadError(status)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;handleCodecError(status.message());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Network::FilterStatus::StopIteration;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;} else if (isCodecProtocolError(status)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;stats_.named_.downstream_cx_protocol_error_.inc();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;handleCodecError(status.message());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return Network::FilterStatus::StopIteration;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ASSERT(status.ok());

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Processing incoming data may release outbound data so check for closure here as well.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;checkForDeferredClose(false);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// The HTTP/1 codec will pause dispatch after a single message is complete. We want to
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// either redispatch if there are no streams and we have more data. If we have a single
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// complete non-WebSocket stream but have not responded yet we will pause socket reads
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// to apply back pressure.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (codec_-&amp;gt;protocol() &amp;lt; Protocol::Http2) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (read_callbacks_-&amp;gt;connection().state() == Network::Connection::State::Open &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;data.length() &amp;gt; 0 &amp;amp;&amp;amp; streams_.empty()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;redispatch = true;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;} while (redispatch);

&amp;nbsp;&amp;nbsp;if (!read_callbacks_-&amp;gt;connection().streamInfo().protocol()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;read_callbacks_-&amp;gt;connection().streamInfo().protocol(codec_-&amp;gt;protocol());
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;return Network::FilterStatus::StopIteration;
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;conn_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::checkForDeferredClose(bool skip_delay_close) {
&amp;nbsp;&amp;nbsp;Network::ConnectionCloseType close = Network::ConnectionCloseType::FlushWriteAndDelay;
&amp;nbsp;&amp;nbsp;if (Runtime::runtimeFeatureEnabled(&quot;envoy.reloadable_features.skip_delay_close&quot;) &amp;amp;&amp;amp;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;skip_delay_close) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;close = Network::ConnectionCloseType::FlushWrite;
&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;if (drain_state_ == DrainState::Closing &amp;amp;&amp;amp; streams_.empty() &amp;amp;&amp;amp; !codec_-&amp;gt;wantsToWrite()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;doConnectionClose(close, absl::nullopt,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;StreamInfo::ResponseCodeDetails::get().DownstreamLocalDisconnect);
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;이후 StopIteration을 return한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;filter_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void FilterManagerImpl::onContinueReading(ActiveReadFilter* filter,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ReadBufferSource&amp;amp; buffer_source) {
&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;for (; entry != upstream_filters_.end(); entry++) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;... (중략)...

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;StreamBuffer read_buffer = buffer_source.getReadBuffer();
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (read_buffer.buffer.length() &amp;gt; 0 || read_buffer.end_stream) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;FilterStatus status = (*entry)-&amp;gt;filter_-&amp;gt;onData(read_buffer.buffer, read_buffer.end_stream);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (status == FilterStatus::StopIteration || connection_.state() != Connection::State::Open) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;Stopiteration이 반환되었으므로 upstream_filter 또한 종료한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;filter_manager_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void FilterManagerImpl::onRead() {
&amp;nbsp;&amp;nbsp;ASSERT(!upstream_filters_.empty());
&amp;nbsp;&amp;nbsp;onContinueReading(nullptr, connection_);
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;onContinueReading 메소드를 종료한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;connection_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionImpl::onRead(uint64_t read_buffer_size) {
&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;filter_manager_.onRead();
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;onRead 메소드를 종료한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;connection_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionImpl::onReadReady() {
&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;read_end_stream_ |= result.end_stream_read_;
&amp;nbsp;&amp;nbsp;if (result.bytes_processed_ != 0 || result.end_stream_read_ ||
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;(latched_dispatch_buffered_data &amp;amp;&amp;amp; read_buffer_-&amp;gt;length() &amp;gt; 0)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// Skip onRead if no bytes were processed unless we explicitly want to force onRead for
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// buffered data. For instance, skip onRead if the connection was closed without producing
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;// more data.
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;onRead(new_buffer_size);
&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;// The read callback may have already closed the connection.
&amp;nbsp;&amp;nbsp;if (result.action_ == PostIoAction::Close || bothSidesHalfClosed()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ENVOY_CONN_LOG(debug, &quot;remote close&quot;, *this);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;closeSocket(ConnectionEvent::RemoteClose);
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;onRead가 끝난 이후 IoAction이 Close된 경우 소켓을 종료한다. 여기서는 정상적인 연결이기 때문에 해당 로직을 수행하지 않는다.&lt;br&gt;&amp;nbsp;&lt;br&gt;connection_impl.cc&lt;/p&gt;&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionImpl::onFileEvent(uint32_t events) {
&amp;nbsp;&amp;nbsp;...(중략)...

&amp;nbsp;&amp;nbsp;// It's possible for a write event callback to close the socket (which will cause fd_ to be -1).
&amp;nbsp;&amp;nbsp;// In this case ignore read event processing.
&amp;nbsp;&amp;nbsp;if (ioHandle().isOpen() &amp;amp;&amp;amp; (events &amp;amp; Event::FileReadyType::Read)) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;onReadReady();
&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;최초에 호출했던 onFileEvent를 종료한다.&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/214</guid>
      <comments>https://cla9.tistory.com/214#entry214comment</comments>
      <pubDate>Tue, 3 Mar 2026 11:49:46 +0900</pubDate>
    </item>
    <item>
      <title>Kafka Broker 네트워크 구조</title>
      <link>https://cla9.tistory.com/169</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Broker에 대해서 학습한 이후 아래와 같이 PDF를 정리해서 공유했었다. 이후 Kafka Broker의 네트워크 연결은 어떻게 이루어질까에 대한 생각에 이에 대하여 학습하여 공유하고자 한다.&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;a href=&quot;https://www.scribd.com/document/560749821/Kafka-Broker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.scribd.com/document/560749821/Kafka-Broker&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1685064656182&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;scribd-com:document&quot; data-og-title=&quot;Kafka Broker | PDF&quot; data-og-description=&quot;Kafka Broker 내부 모듈 정리&quot; data-og-host=&quot;www.scribd.com&quot; data-og-source-url=&quot;https://www.scribd.com/document/560749821/Kafka-Broker&quot; data-og-url=&quot;https://www.scribd.com/document/560749821/Kafka-Broker&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/byyTZl/hySKEp4l9x/dCoxKjnqDxebByAVFkGjl1/img.jpg?width=768&amp;amp;height=1024&amp;amp;face=0_0_768_1024,https://scrap.kakaocdn.net/dn/bvnzaG/hySLJ4mvmC/zlSEUVl6v47tkLys9jnpvK/img.jpg?width=768&amp;amp;height=1024&amp;amp;face=0_0_768_1024&quot;&gt;&lt;a href=&quot;https://www.scribd.com/document/560749821/Kafka-Broker&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://www.scribd.com/document/560749821/Kafka-Broker&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/byyTZl/hySKEp4l9x/dCoxKjnqDxebByAVFkGjl1/img.jpg?width=768&amp;amp;height=1024&amp;amp;face=0_0_768_1024,https://scrap.kakaocdn.net/dn/bvnzaG/hySLJ4mvmC/zlSEUVl6v47tkLys9jnpvK/img.jpg?width=768&amp;amp;height=1024&amp;amp;face=0_0_768_1024');&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;Kafka Broker | PDF&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Kafka Broker 내부 모듈 정리&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;www.scribd.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1304&quot; data-origin-height=&quot;635&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSy7iG/btrsSutEGPu/iVrwkKF0YEh7GVMLlXsTlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSy7iG/btrsSutEGPu/iVrwkKF0YEh7GVMLlXsTlk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSy7iG/btrsSutEGPu/iVrwkKF0YEh7GVMLlXsTlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSy7iG%2FbtrsSutEGPu%2FiVrwkKF0YEh7GVMLlXsTlk%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;640&quot; height=&quot;312&quot; data-origin-width=&quot;1304&quot; data-origin-height=&quot;635&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 분산 시스템으로써, 수많은 Broker와 Producer, Consumer끼리 통신을 수행하며, Broker 간에도 데이터 복제 및 상태 중재를 위한 네트워크 통신이 사용된다.&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;그렇다면, Kafka에서는 어떠한 방식으로 통신을 수행할까?&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;일반적으로 생각해보면, HTTP Protocol을 사용하는 방법으로 이미 구현된 HTTP 프레임워크를 사용하는 것이 개발 입장에서는 쉬울 것이다. 하지만 Kafka 커미터들은 다음과 같은 이슈로 인하여 자체 통신 체계를 구축했다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka에서는 빠른 메시지 전달을 위해 Kafka에 최적화된 통신 체계가 필요하다. 또한 커다란 프레임워크 코드 영역에서 Kafka가 필요한 부분은 일부에 불과하다.&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;p data-ke-size=&quot;size16&quot;&gt;Kafka는 메시지 전달을 위해 빠른 네트워크 성능이 필요하기 때문에, 고성능 통신을 위해 간결하면서도 최적화된 방식이 필요하다. 그리고 라이브러리 및 의존성 문제에서 자유로워야한다. 만약 다른 프레임워크에 의존하게된다면, Broker 및 Client 모두 해당 라이브러리에 대한 강한 의존성이 생긴다. 이는 버전 관리의 어려움이 존재하게되며, Kafka 라이브러리를 포함하는 Client의 파일 크기 또한 커지게 된다. 따라서, 위 두 가지 이슈로 인해 자체 네트워크 모델을 구축하으며, 빠른 메시지 전달과 동시 신뢰성 있는 데이터 전달이 중요하기 때문에 UDP 기반이 아닌 TCP 기반위에서 동작하도록 자체 Protocol을 설계했다.&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;Kafka 네트워크 모델 기반에는 Java의 NIO API가 광범위하게 사용된다. 따라서, Kafka 네트워크 모델을 살펴보기 앞서 NIO에서 Network 연관 부분만 살펴보자.&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NIO&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NIO는 New IO의 약자로써 기존 IO 방식에서 발생하는 Blocking 이슈를 개선하기 위해 Java 1.4 부터 새롭게 도입된 기능(JSR-51)이며, 다음과 같은 특징을 지니고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Non-Blocking&lt;/li&gt;
&lt;li&gt;IO Multiplexing&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;위 두 가지 특징을 기반으로 NIO가 기존 IO와 무엇이 다른지에 대해 살펴보자.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bznWZX/btrsRQqmwd9/gXuWNHDWkF75P75TpqJVs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bznWZX/btrsRQqmwd9/gXuWNHDWkF75P75TpqJVs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bznWZX/btrsRQqmwd9/gXuWNHDWkF75P75TpqJVs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbznWZX%2FbtrsRQqmwd9%2FgXuWNHDWkF75P75TpqJVs1%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;640&quot; height=&quot;461&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 Socket을 활용한 네트워킹 과정을 살펴보면 위와 같다. 여기서 기존 I/O 방식은 Client의 연결을 받아들이는 Accept 부분과 Read, Write 연산 등은 모두 Blocking된다. 이는 즉 Accept의 경우 요청이 들어올 때까지 요청을 반환하지 않음을 의미하며, Read, 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;이러한 경우 일반적인 단일 소켓으로는 여러 요청을 효과적으로 처리할 수 없다. 그 이유는 여러 요청을 빠르게 처리하기 위해서는 Blocking되어 Idle한 시간을 효율적으로 분배하여 다른 작업을 처리 해야하는데, 기존 구조로는 이에 대해 효과적으로 대응할 수 없기 때문이다.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ACyvX/btrsVKidgo3/NXbPL4jUlyLaRCDMI6MUP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ACyvX/btrsVKidgo3/NXbPL4jUlyLaRCDMI6MUP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ACyvX/btrsVKidgo3/NXbPL4jUlyLaRCDMI6MUP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FACyvX%2FbtrsVKidgo3%2FNXbPL4jUlyLaRCDMI6MUP1%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;640&quot; height=&quot;275&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 기존에는 동기식 I/O 방식의 문제점을 해결하고자 각 Client의 연결 요청에 대해 이를 처리할 수 있는 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;하지만 위와 같이 Multi Thread 방식의 네트워크 통신 방법에는 한계가 존재한다. 그 이유는 아무리 Thread가 Process보다는 가볍다고 하나 개별 요청 별 Thread를 할당하는 방식은 메모리 사용 및 Context Switching에 따른 Overhead가 크기 때문이다. 가령 Thread별 스택을 1M씩만 할당한다고 가정하더라도 1024개 사용자 요청을 처리하기 위해서 생성되는 스택만 1GB가 사용될 것이다. 따라서, 사용자가 증가할 수록 처리량은 감소하게된다.&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;그렇다면, Non-Blocking으로 처리하면 어떻게 될까?&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GBIzN/btrsSbOASn3/kZswYRflFKXP3MPPPS6uCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GBIzN/btrsSbOASn3/kZswYRflFKXP3MPPPS6uCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GBIzN/btrsSbOASn3/kZswYRflFKXP3MPPPS6uCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGBIzN%2FbtrsSbOASn3%2FkZswYRflFKXP3MPPPS6uCk%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;640&quot; height=&quot;461&quot; data-origin-width=&quot;833&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Non-Blocking 방식은 기존의 Blocking I/O를 유발하는 메소드에 대해서 추가 설정을 통해 Non-Blocking 형태로 구성할 수 있다. 즉 기존에는 메소드 호출 시 작업이 완료될 때까지 기다렸지만, 설정 이후에는 메소드 결과가 즉시 반환하기 때문에, 하나의 소켓 서버 Thread가 여러개의 I/O를 처리할 수 있게되었다.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv6EOo/btrsXtz0MOl/j5MgrdCanmen0FzOVkjttK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv6EOo/btrsXtz0MOl/j5MgrdCanmen0FzOVkjttK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv6EOo/btrsXtz0MOl/j5MgrdCanmen0FzOVkjttK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv6EOo%2FbtrsXtz0MOl%2Fj5MgrdCanmen0FzOVkjttK%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;640&quot; height=&quot;275&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Non-Blocking 설정 이후로는 위 그림과 같이 단일 Thread에서 여러 사용자의 Channel과 매핑되어 데이터를 처리할 수 있게 되었다. 위 그림만 보면 Thread 개수를 줄일 수 있으니 성능이 많이 향상될 것으로 보인다. 하지만 다음과 같은 문제가 존재한다.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;965&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUxmU9/btrsUceXSTl/LpWZs6rpGNkDp2U3kafT6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUxmU9/btrsUceXSTl/LpWZs6rpGNkDp2U3kafT6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUxmU9/btrsUceXSTl/LpWZs6rpGNkDp2U3kafT6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUxmU9%2FbtrsUceXSTl%2FLpWZs6rpGNkDp2U3kafT6K%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;640&quot; height=&quot;351&quot; data-origin-width=&quot;965&quot; data-origin-height=&quot;529&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Blocking I/O 방식으로 처리할 경우에는 순차적으로 처리하므로 Blocking 메소드를 벗어났다는 것은 해당 처리가 완료되었음이 어느정도 보장된다. 하지만 Non-Blocking 방식은 작업 요청과 별개로 바로 리턴이 되기 때문에 실제 요청 여부를 확인하기 위해서는 주기적으로 소켓 정보를 Polling하여 처리 가능 여부를 확인해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;List&amp;lt;SocketChannel&amp;gt; channels = new ArrayList&amp;lt;&amp;gt;();
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9002));
serverSocket.configureBlocking(false);

while (true) {
  SocketChannel channel = serverSocket.accept();
  if (null != channel) {   
    socketChannel.configureBlocking(false);
    channels.add(channel);
  }

  Iterator&amp;lt;SocketChannel&amp;gt; iterator = channels.iterator();
  while (iterator.hasNext()) {
    SocketChannel channel = iterator.next();
    ..(Read 요청 확인 수행 및 처리)...
  }
}&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;위 코드는 configureBlocking 설정을 통해서 Blocking 방식 API를 Non-Blocking 방식으로 변경한 예제이다. 코드를 살펴보면, accept 혹은 read 수행하면 바로 리턴되므로 요청 확인을 위해서 지속적으로 무한 Loop를 수행하며 확인 과정이 필요하다. 예를 들어 현재 100개의 연결이 이루어져 channels List에 등록되어있다면, 매번 100번의 연결에 대하여 요청 여부를 확인한다.&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;위와 같은 방식의 경우 무한 Loop로 인하여 CPU overhead가 지속 발생하므로 Non-Blocking 기법만 적용해서는 성능 향상의 효과를 크게 얻을 수 없다. 그렇다면 이러한 문제는 어떻게 해결할까? IO Multiplexing에 대해서 알아보자.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IO Multiplexing&lt;/h3&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csbrKm/btrsSvTBR7e/6UE7fVqKymZTehsUGG14N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csbrKm/btrsSvTBR7e/6UE7fVqKymZTehsUGG14N1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csbrKm/btrsSvTBR7e/6UE7fVqKymZTehsUGG14N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsbrKm%2FbtrsSvTBR7e%2F6UE7fVqKymZTehsUGG14N1%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;183&quot; data-origin-width=&quot;1174&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IO Multiplexing 방식은 하나의 Channel을 통해 여러 개의 연결을 관리하는 방식으로 해당 방식에서는 소켓 관리를 OS에서 직접 관리한다. 따라서 사용자 코드에서는 OS에 관리 대상 소켓 정보를 등록하는 단계가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(※ 본 포스팅에서 Kafka는 Linux 환경에서 동작함을 가정하므로 Socket은 FileDescriptor로 취급됨을 참고하자.)&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;1611&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ci3mOZ/btrsYdX0jS3/wCfZMDV5ZkvEhqGRc9bDrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ci3mOZ/btrsYdX0jS3/wCfZMDV5ZkvEhqGRc9bDrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ci3mOZ/btrsYdX0jS3/wCfZMDV5ZkvEhqGRc9bDrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fci3mOZ%2FbtrsYdX0jS3%2FwCfZMDV5ZkvEhqGRc9bDrk%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;1611&quot; height=&quot;356&quot; data-origin-width=&quot;1611&quot; data-origin-height=&quot;356&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록 이후에는 OS에서 File descriptor 목록을 가지고 있고, 내부적으로 데이터를 처리해야 될 대상이 발견되면, 해당 정보를 이후 Client에서 요청 시 반환하는 역할을 담당한다. 즉 이전에는 Client에서 직접 처리 요청 대상을 관리했다면, Monitor 역할을 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;위와 같은 방법을 적용하면, 사용자 코드에서 Connection 개수 여부와 관계없이 처리 대상만 OS로부터 전달받으므로 CPU overhead를 줄일 수 있으므로 적은 Thread로 많은 처리 요청을 수행할 수 있다.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2119&quot; data-origin-height=&quot;783&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LlhyA/btrsScGKigL/8e6qvLMCtrw1cEbgeoJac1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LlhyA/btrsScGKigL/8e6qvLMCtrw1cEbgeoJac1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LlhyA/btrsScGKigL/8e6qvLMCtrw1cEbgeoJac1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLlhyA%2FbtrsScGKigL%2F8e6qvLMCtrw1cEbgeoJac1%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;2119&quot; height=&quot;783&quot; data-origin-width=&quot;2119&quot; data-origin-height=&quot;783&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 NIO는 이를 위해서 Selector를 활용한다. Selector는 OS와 사용자 코드 상의 가교 역할을 수행한다. 따라서 사용자 코드에서 Selector에게 처리 대상 Channel 등록을 요청하면, OS에 해당 정보를 전달한다. 그 이후에는 주기적으로 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;(※ JVM 6이상 환경에서 Linux Kernel 2.6 이상을 사용하면, 기본 Selector의 구현체로 Linux의 epoll이 사용된다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;ServerSocketChannel serverSocket  = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9002));
serverSocket.configureBlocking(false);

Selector selector = Selector.open();
SelectionKey selectionKey = serverSocket.register(selector, SelectionKey.OP_ACCEPT);

while (true) {

  //select() 메소드는 Blocking 방식으로 동작함.
  selector.select();
  
  Set&amp;lt;SelectionKey&amp;gt; selectionKeys = selector.selectedKeys();
  Iterator&amp;lt;SelectionKey&amp;gt; iterator = selectionKeys.iterator();

  while (iterator.hasNext()) {
    SelectionKey key = iterator.next();
    if (key.isAcceptable()) {
        SocketChannel channel = (ServerSocketChannel)key.channel().accept();
        channel.configureBlocking(false);
        channel.register(selector, SelectionKey.OP_READ);
    } else if (key.isReadable()) {
        SocketChannel channel = (SocketChannel)key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int read = channel.read(buffer);
        
        ...(Read 처리)...        
    }
}&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;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1659&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CoXKY/btrsTdkQt3w/NHN9jggJsfKTZZor7PEKIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CoXKY/btrsTdkQt3w/NHN9jggJsfKTZZor7PEKIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CoXKY/btrsTdkQt3w/NHN9jggJsfKTZZor7PEKIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCoXKY%2FbtrsTdkQt3w%2FNHN9jggJsfKTZZor7PEKIK%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;1659&quot; height=&quot;263&quot; data-origin-width=&quot;1659&quot; data-origin-height=&quot;263&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Server 소켓을 생성하고 open() 메소드를 통해 Selector를 생성한다. 해당 메소드는 Linux 내부에 epoll Object를 생성한다.&lt;/li&gt;
&lt;li&gt;Client가 IP 및 Port 정보를 통해서 접속 요청을 할 것이다. 그러면 OS는 바인딩된 내부 오브젝트에 반영한다.&lt;/li&gt;
&lt;li&gt;사용자 코드에서 select() 메소드를 호출하면, 접속 요청이 존재하므로 해당 정보를 반환한다.&lt;/li&gt;
&lt;li&gt;사용자 코드에서 해당 접속 요청을 받아들인 이후에 Read 요청이 들어오면 이를 감지하기 위해 Kernel에 Read 이벤트에 대한 수신을 받을 수 있도록 요청한다. 이때 호출되는 register()를 통해 내부에 epoll_ctl및 epoll_wait 시스템 콜이 호출된다.&lt;/li&gt;
&lt;li&gt;Linux Kernel은 해당 요청을 다룰 수 있는 connection이 존재하는지 확인 후 client와 연결한다.&lt;/li&gt;
&lt;li&gt;연결이 성공적으로 이루어지면, Selector에게 알림을 통지하고 내부적으로 Channel을 생성한다.&lt;/li&gt;
&lt;li&gt;사용자가 데이터 fetch 요청을 전달하면 내부 Buffer에 이를 저장한다. 이때 Buffer의 위치는 direct 방식과 아닐 경우에 따라서 달라질 수 있는데, 이는 나중에 다루도록 한다.&lt;/li&gt;
&lt;li&gt;Channel은 연결 역할을 수행할 뿐 데이터 fetch는 Buffer를 통해 이루어진다.&lt;/li&gt;
&lt;li&gt;Selector는 지속적으로 poll을 수행하여 Channel에 등록된 Buffer의 내용을 읽어간다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;( ※ 위 그림에서 Channel과 Buffer는 연결된 Client 마다 생성된다.)&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;위 코드 중 가장 중요한 것은 select()이다. 이는 해당 메소드 또한 Blocking 방식이기 때문이다. 따라서 Selector를 활용한 방식은 완벽한 비동기 방식은 아니므로 Synchronous Non-Blocking 방식이라고 볼 수 있다.&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;Java의 NIO에 대해서 정리하자면, 기존 동기 방식의 API로 인한 동시성 저하를 막고자 Non-Blocking API를 제공하며, IO Multiplexing을 통해 처리량을 높일 수 있다. 하지만 완전한 방식의 비동기 방식은 아니다.&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka Broker Network 구조&lt;/h3&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&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;1611&quot; data-origin-height=&quot;1144&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7OPEy/btrsSuUFMb8/loi4Tc6Pj5ojjGIKoH4CL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7OPEy/btrsSuUFMb8/loi4Tc6Pj5ojjGIKoH4CL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7OPEy/btrsSuUFMb8/loi4Tc6Pj5ojjGIKoH4CL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7OPEy%2FbtrsSuUFMb8%2Floi4Tc6Pj5ojjGIKoH4CL1%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;1611&quot; height=&quot;1144&quot; data-origin-width=&quot;1611&quot; data-origin-height=&quot;1144&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 학습한 Java의 NIO를 바탕으로 Kafka Broker내의 Network 통신을 위한 구조를 살펴보자. Broker 구조는 크게 Socket Server, Request Handler Pool, API 세 가지로 이루어져있다. 해당 컴포넌트에 무엇이 있는지 하나씩 살펴보자.&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;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Socket Server&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Socket Server는 사용자 접속 및 요청을 담당하는 역할을 담당하며, Acceptor, Processor, Request Channel로 이루어진 Request-Plane 세트이다.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;690&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbNxiD/btrsUE92dPb/5OGOESzx5kCreuSKqjeE1K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbNxiD/btrsUE92dPb/5OGOESzx5kCreuSKqjeE1K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbNxiD/btrsUE92dPb/5OGOESzx5kCreuSKqjeE1K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbNxiD%2FbtrsUE92dPb%2F5OGOESzx5kCreuSKqjeE1K%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;640&quot; height=&quot;454&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;690&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 그림에는 1개의 Plane을 묘사했지만, 실제로는 data-plane과 control-plane 총 2개의 plane이 존재한다. 여기서 control-plane은 Broker와 Controller 간의 통신을 위해 연결된 전용 네트워크이며, data-plane은 Broker 끼리 혹은 client의 요청을 처리하기 위한 네트워크이다.&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;그렇다면, Request-Plane 구성 요소인 Acceptor, Processor, Request 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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Acceptor&lt;/b&gt;&lt;/span&gt;는 Client의 접속 요청을 감지하는 문지기의 역할을 수행한다. Acceptor를 통해 연결 요청을 전달받으면, 하위에 존재하는 Processor 중 하나에게 Read/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;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Processor&lt;/b&gt;&lt;/span&gt;는 연결된 Socket에 대하여 Read/Write 요청이 전달되는 것을 감지하고, 이를 Request Channel의 Request Queue에 전달하는 역할과 실제 작업이 완료된 이후 결과를 전달받아 사용자에게 반환하는 것을 담당한다.&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;color: #ee2323;&quot;&gt;&lt;b&gt;Request Channel&lt;/b&gt;&lt;/span&gt;은 모든 Processor, Handler, API가 공유하는 전역 저장소로써, 사용자의 요청이 전달되면 해당 정보를 보관하고 처리가 완료되면 요청한 Processor에게 결과를 반환하는 역할을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wHCXE/btrsVJwPbqe/2P3mHqxoT8sfaUnQh9k9Q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wHCXE/btrsVJwPbqe/2P3mHqxoT8sfaUnQh9k9Q0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wHCXE/btrsVJwPbqe/2P3mHqxoT8sfaUnQh9k9Q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwHCXE%2FbtrsVJwPbqe%2F2P3mHqxoT8sfaUnQh9k9Q0%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;1101&quot; height=&quot;606&quot; data-origin-width=&quot;1101&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;496&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctzC35/btrsSb2eHJy/VKk5R2fCwaukcCs0ChBve0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctzC35/btrsSb2eHJy/VKk5R2fCwaukcCs0ChBve0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctzC35/btrsSb2eHJy/VKk5R2fCwaukcCs0ChBve0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctzC35%2FbtrsSb2eHJy%2FVKk5R2fCwaukcCs0ChBve0%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;867&quot; height=&quot;496&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;496&quot;/&gt;&lt;/span&gt;&lt;/figure&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Socket Server의 구조를 보면, Acceptor가 여러개의 Processor를 가지고 있고 Processor는 Request Channel과 연관이 있음을 알 수 있다. Socket Server에는 data-plane과 control-plane 두 개가 존재한다고 이전에 설명했는데, data-plane의 경우 Acceptor는 여러개의 Processor를 가질 수 있으며, 해당 설정은 num.network.threads 설정을 통해서 개수를 조절할 수 있다. 반면 control-plane의 경우는 Processor가 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;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Request Handler&lt;/h4&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;680&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ARhCY/btrsS7YUVkg/djA1VcV6C3AjdkMAQhZrYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ARhCY/btrsS7YUVkg/djA1VcV6C3AjdkMAQhZrYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ARhCY/btrsS7YUVkg/djA1VcV6C3AjdkMAQhZrYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FARhCY%2FbtrsS7YUVkg%2FdjA1VcV6C3AjdkMAQhZrYk%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;520&quot; height=&quot;420&quot; data-origin-width=&quot;842&quot; data-origin-height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request Handler는 ReuqestChannel에서 Request 정보를 가져와 API에게 처리를 요청하고 요청 결과를 다시 RequestChannel에 전달하는 역할을 담당한다. RequestHandler는 1개가 아니라 여러개의 Thread로 구성될 수 있으며, 이는 num.io.threads 속성을 통해 개수를 조정할 수 있다.&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;이전 Kafka 버전(0.7)에서는 Request Handler가 따로 존재하지 않았고 Processor를 통해 직접 처리를 수행하였다. 하지만 Network Read/Write 요청을 감지하는 영역과 I/O를 처리하는 부분이 하나의 Thread안에 있으므로 탄력적으로 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;따라서 I/O와 Network 처리를 위한 Thread를 분리함으로써, 현재와 같은 모습을 갖추게 되었다.&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;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;API&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;request.header.apiKey match {
        case ApiKeys.PRODUCE =&amp;gt; handleProduceRequest(request, requestLocal)
        case ApiKeys.FETCH =&amp;gt; handleFetchRequest(request)
        ...(중략)...
        case _ =&amp;gt; throw new IllegalStateException(s&quot;No handler for request api key ${request.header.apiKey}&quot;)
      }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API는 Client가 요청한 정보를 기반으로 Kafka 내부 모듈에 필요한 메소드를 호출하는 역할을 담당한다. Kafka Protocol에는 위와 같이 어떤 요청인지 header에 포함시키도록 규정되었다. 따라서 Kafka API가 요구하는 Spec에 맞게 작성하면, 이를 Parsing 하여 개별 모듈로 Routing을 시켜준다. 요청 처리가 완료되면, RequestHelper를 통해 RequestChannel로 전달한다.&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Kafka Network 구조에 대해서 큰 틀에서 살펴봤다. 이번에는 각 모듈끼리 어떠한 상호작용을 거쳐 동작하는지 살펴보자. 먼저 큰 흐름 속에서 어떻게 동작하는지 보고 이후 코드 레벨에서 보다 자세하게 살펴보도록 하자.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;100&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;100&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;188&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JQ21v/btrsWL8Zj7B/5J2BQ9ajOydtKDDNSdTpnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JQ21v/btrsWL8Zj7B/5J2BQ9ajOydtKDDNSdTpnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JQ21v/btrsWL8Zj7B/5J2BQ9ajOydtKDDNSdTpnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJQ21v%2FbtrsWL8Zj7B%2F5J2BQ9ajOydtKDDNSdTpnK%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;1378&quot; height=&quot;188&quot; data-origin-width=&quot;1378&quot; data-origin-height=&quot;188&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;100&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로 살펴볼 것은 Client가 접속 요청 시도시 내부 동작 과정이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 접속 요청을 한다.&lt;/li&gt;
&lt;li&gt;Acceptor가 해당 접속 요청을 수락하고, 자신이 보유한 Processor 중 하나에게 할당한다. Processor는 해당 요청을 자신이 보유한 Kafka Selector에 요청하여 이후 Client로부터 데이터 처리 요청이 왔을 경우 감지할 수 있도록 사전 준비한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client 접속 요청이 완료되면, Processor는 사용자 요청을 처리할 수 있는 단계가 된다. 이후 사용자 요청이 발생했을 때 처리 과정을 살펴보자.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;100&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;949&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7QXRY/btrsWLBaOH3/kVv6SDF5nToJggZNEVcRT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7QXRY/btrsWLBaOH3/kVv6SDF5nToJggZNEVcRT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7QXRY/btrsWLBaOH3/kVv6SDF5nToJggZNEVcRT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7QXRY%2FbtrsWLBaOH3%2FkVv6SDF5nToJggZNEVcRT0%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;1170&quot; height=&quot;949&quot; data-origin-width=&quot;1170&quot; data-origin-height=&quot;949&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;100&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;100&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;사용자가 데이터 fetch 요청을 하면, Kernel은 이를 감지한다.&lt;/li&gt;
&lt;li&gt;Processor에서 Kafka Selector에게 데이터 fetch 요청 이후 해당 요청을 Request Channel의 Request Queue에 저장한다. 이때 향후 처리 결과를 자신에게 포워딩 하기 위해 Queue 삽입시 자신의 Processor Id를 함께 추가한다.&lt;/li&gt;
&lt;li&gt;Request Handler에서 Request Queue에 존재하는 요청을 fetch한다.&lt;/li&gt;
&lt;li&gt;해당 요청을 API에게 전달한다.&lt;/li&gt;
&lt;li&gt;API는 요청을 처리한다음 자신이 보유한 Request Helper를 통해 RequestChannel로 전달한다.&lt;/li&gt;
&lt;li&gt;Request Channel은 Processor Id를 보고 해당 Processor의 Response Queue에 결과를 삽입한다.&lt;/li&gt;
&lt;li&gt;Processor는 Response Queue 내용을 확인하고 Client에게 결과를 전달한다.&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;지금까지 살펴본 내용은 큰 틀에서 컴포넌트간 상호 작용에 대해서 확인했다. 이번에는 코드 레벨에서 자세하게 각 모듈이 어떻게 구동되고 상호작용하는지 알아보자.&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;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Socket Server 동작 과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;// data-plane
private val dataPlaneProcessors = new ConcurrentHashMap[Int, Processor]()
private[network] val dataPlaneAcceptors = new ConcurrentHashMap[EndPoint, Acceptor]()
val dataPlaneRequestChannel = new RequestChannel(maxQueuedRequests, DataPlaneMetricPrefix, time, apiVersionManager.newRequestMetrics)

// control-plane
private var controlPlaneProcessorOpt : Option[Processor] = None
private[network] var controlPlaneAcceptorOpt : Option[Acceptor] = None
val controlPlaneRequestChannelOpt: Option[RequestChannel] = config.controlPlaneListenerName.map(_ =&amp;gt;
    new RequestChannel(20, ControlPlaneMetricPrefix, time, apiVersionManager.newRequestMetrics))&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;가장 먼저 살펴볼 것은 Socket Server에 속한 2개의 plane이 어떻게 구성되어있는지 살펴보자. 위 내용을 살펴보면, 2개의 plane이 서로 다른점이 몇 가지 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;data-plane의 경우 Acceptor, Processor가 여러개이지만, controlPlane의 경우 하나만 존재한다.&lt;/li&gt;
&lt;li&gt;data-plane의 경우 RequestChannel 내에 존재하는 RequestQueue의 크기를 queued.max.requests 속성 크기만큼 지정 가능한 반면, control-plane의 경우는 20개로 크기가 고정되어있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;def startup(startProcessingRequests: Boolean = true,
            controlPlaneListener: Option[EndPoint] = config.controlPlaneListener,
            dataPlaneListeners: Seq[EndPoint] = config.dataPlaneListeners): Unit = {
this.synchronized {
      createControlPlaneAcceptorAndProcessor(controlPlaneListener)
      createDataPlaneAcceptorsAndProcessors(config.numNetworkThreads, dataPlaneListeners)
      ...(중략)...
}&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;Socket Server를 생성하고 나면, 해당 Server를 기동하는 startup 메소드가 호출된다. 이때 개별 data, control plane 각각에 대하여 Acceptor와 Processor를 생성하는 메소드가 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;private def createDataPlaneAcceptorsAndProcessors(dataProcessorsPerListener: Int,
                                                    endpoints: Seq[EndPoint]): Unit = {
  endpoints.foreach { endpoint =&amp;gt;
    connectionQuotas.addListener(config, endpoint.listenerName)
    val dataPlaneAcceptor = createAcceptor(endpoint, DataPlaneMetricPrefix)
    addDataPlaneProcessors(dataPlaneAcceptor, endpoint, dataProcessorsPerListener)
    dataPlaneAcceptors.put(endpoint, dataPlaneAcceptor)      
  }
}

private def addDataPlaneProcessors(acceptor: Acceptor, endpoint: EndPoint, newProcessorsPerListener: Int): Unit = {
  ...(중략)...    
  for (_ &amp;lt;- 0 until newProcessorsPerListener) {
    val processor = newProcessor(nextProcessorId, dataPlaneRequestChannel, connectionQuotas, listenerName, securityProtocol, memoryPool, isPrivilegedListener)
    ...(중략)...
    dataPlaneRequestChannel.addProcessor(processor)
    nextProcessorId += 1
  }
  ...(중략)...
  acceptor.addProcessors(listenerProcessors, DataPlaneThreadPrefix)
}&lt;/code&gt;&lt;/pre&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;625&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF879o/btrsUclJkFM/BCXb7RZUqYIW2ezKfM4Br1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF879o/btrsUclJkFM/BCXb7RZUqYIW2ezKfM4Br1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF879o/btrsUclJkFM/BCXb7RZUqYIW2ezKfM4Br1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF879o%2FbtrsUclJkFM%2FBCXb7RZUqYIW2ezKfM4Br1%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;640&quot; height=&quot;363&quot; data-origin-width=&quot;1102&quot; data-origin-height=&quot;625&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 생성 메소드 중 data-plane 생성 코드를 살펴보자. 위와같이 listeners를 통해서 전달받은 endpoint 별로 acceptor가 생성되며, num.network.threads 개수만큼 processor 또한 생성 된다. processor 생성 이후 acceptor와 channel에 해당 processor를 등록한다.&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;해당 과정을 통해 Acceptor와 RequestChannel의 Processor 간의 매핑 관계를 이해할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Acceptor 동작과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;def run(): Unit = {
serverChannel.register(nioSelector, SelectionKey.OP_ACCEPT)
...(중략)...
try {
  while (isRunning) {
    try {
      acceptNewConnections()
      ...(중략)...
    }
    catch {
      ...(중략)...
    }
  }
}&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;설정 작업이 마무리되면, listeners에 매핑된 Endpoint 개수 만큼의 Kafka 쓰레드를 생성하여 Acceptor에게 할당한다. 위 코드는 Acceptor에게 쓰레드 할당 후 start() 호출 이후 수행 과정을 나타낸다.&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;NIO Selector를 통해 Accept 이벤트를 통지할 수 있도록 요청하면, 내부적으로 Kernel에 epoll 오브젝트가 생성되고, Accept 요청이 왔을 때 이를 수신받을 수 있음을 이전 NIO 개념을 학습하면서 살펴봤다.&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;소켓 정보 등록 후에는 무한 Loop를 통해 새로운 연결 요청이 있는지를 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;private def acceptNewConnections(): Unit = {
  val ready = nioSelector.select(500)
  if (ready &amp;gt; 0) {
    val keys = nioSelector.selectedKeys()
    val iter = keys.iterator()
    while (iter.hasNext &amp;amp;&amp;amp; isRunning) {
      try {
        val key = iter.next
        ...(중략)...
        if (key.isAcceptable) {
          accept(key).foreach { socketChannel =&amp;gt;
            ...(중략)...
            var processor: Processor = null
            do {
                ...(중략)...
                processor = synchronized {
                currentProcessorIndex = currentProcessorIndex % processors.length
                processors(currentProcessorIndex)
              }
              currentProcessorIndex += 1
            } while (!assignNewConnection(socketChannel, processor, retriesLeft == 0))
          }
        } else
          throw new IllegalStateException(&quot;Unrecognized key state for acceptor thread.&quot;)
      } catch {
        ...(중략)...
      }
    }
  }
}&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;이때 select() 메소드를 통해 Accept 요청이 들어왔는지를 OS에게 확인하는데, 해당 메소드는 Blocking 메소드이므로 무한 대기를 막기 위해 500ms 기간의 Timeout을 지정한다. 이 과정에서 Accept 요청이 들어온다면, 자신이 보유하고 있는 Processor 중 하나에게 향후 Read/Write 요청에 대한 처리를 담당하도록 한다. 이때 살펴볼 것은 Processor에게 균등한 분배를 위해서 Round-Robin 방식으로 접속 요청을 분배한다는 점이다.&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;Acceptor의 역할은 여기까지이고, 지금 부터는 위 코드를 통해 새로운 요청이 Processor에게 할당된 이후 처리 과정에 대해서 살펴보자.&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;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Processor 동작 과정&lt;/h4&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;30&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;30&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4llXf/btrsW9ohkXv/lukF4OyZuPNlEmkMWMQrT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4llXf/btrsW9ohkXv/lukF4OyZuPNlEmkMWMQrT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4llXf/btrsW9ohkXv/lukF4OyZuPNlEmkMWMQrT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4llXf%2FbtrsW9ohkXv%2FlukF4OyZuPNlEmkMWMQrT0%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;350&quot; height=&quot;314&quot; data-origin-width=&quot;350&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;30&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;30&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 Acceptor에서 Processor에게 요청을 할당한다고 했는데, 해당 과정은 어떻게 이루어질까? 먼저 Processor가 지닌 프로퍼티에 대해 먼저 살펴보자.&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가 아닌 Kafka Selector를 내부 프로퍼티로 가지고 있는 것을 확인할 수 있다. 여기서 Kafka Selector에는 내부에 NIO의 Selector를 포함하며, 그 외에 Kafka 데이터 송수신에 필요한 프로퍼티 및 내부 메소드를 지닌 클래스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;30&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yeShH/btrsXsummnn/An7fwQYu3cWDV3NuEkYZCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yeShH/btrsXsummnn/An7fwQYu3cWDV3NuEkYZCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yeShH/btrsXsummnn/An7fwQYu3cWDV3NuEkYZCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyeShH%2FbtrsXsummnn%2FAn7fwQYu3cWDV3NuEkYZCK%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;360&quot; height=&quot;366&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;30&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Selector에는 위 그림외에도 수많은 내부 프로퍼티가 존재하지만, 일부만 간추려서 알아보자. nioSelector는 Java NIO의 selector를 의미한다.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OMYzC/btrsTcfbghW/HnaMcUWkM7Mqk3Kg6EOCkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OMYzC/btrsTcfbghW/HnaMcUWkM7Mqk3Kg6EOCkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OMYzC/btrsTcfbghW/HnaMcUWkM7Mqk3Kg6EOCkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOMYzC%2FbtrsTcfbghW%2FHnaMcUWkM7Mqk3Kg6EOCkk%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;480&quot; height=&quot;174&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;channel은 Processor를 통해 연결된 Client와의 Channel을 의미하며, Connection Id 와 KafkaChannel로 이루어진 Map이다. 따라서 특정 Client와 연결 시 Connection Id 기준으로 해당 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;completedSends, completedReceives 및 disconnected는 데이터 송수신 및 close 처리 시, 해당 요청을 임시 저장하는 용도의 buffer로써 활용된다.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJqcg7/btrsSvsyJZZ/1lpvlWZNYgI9Qy6IyQkHHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJqcg7/btrsSvsyJZZ/1lpvlWZNYgI9Qy6IyQkHHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJqcg7/btrsSvsyJZZ/1lpvlWZNYgI9Qy6IyQkHHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJqcg7%2FbtrsSvsyJZZ%2F1lpvlWZNYgI9Qy6IyQkHHk%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;640&quot; height=&quot;217&quot; data-origin-width=&quot;928&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 Kafka Selector에 대해서 알아보고 이번에는 Processor의 또 다른 주요 프로퍼티 중 하나인 newConnections에 대해서 알아보자. 해당 자료구조는 Queue로써 Acceptor가 새로운 요청을 Processor에게 할당할 때, 해당 Queue에 입력이 된다.&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;이제 Processor의 내부 프로퍼티를 토대로 Processor 동작 과정에 대해 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;override def run(): Unit = {
  ...(중략)...
  try {
    while (isRunning) {
      try {
        configureNewConnections()
        processNewResponses()
        poll()
        processCompletedReceives()
        processCompletedSends()
        processDisconnected()
        closeExcessConnections()
      } catch {
        ...(중략)...
      }
    }
  }
  ...(중략)...
}&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;Processor 또한 Acceptor와는 별개의 Thread로 수행된다. 위 코드는 Processor 기동 시작 후 수행 과정을 나타내며, 무한 Loop를 통해서 동일한 작업을 지속 반복 수행하는 것을 확인할 수 있다. 위 코드와 같이 7개의 동작을 수행하는데, 전부다 살펴보지는 않고 주요 동작에 대해서만 살펴보자.&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;configureNewConnections()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;  private def configureNewConnections(): Unit = {
    var connectionsProcessed = 0
    while (connectionsProcessed &amp;lt; connectionQueueSize &amp;amp;&amp;amp; !newConnections.isEmpty) {
      val channel = newConnections.poll()
      try {
        ...(중략)...
        selector.register(connectionId(channel.socket), channel)
        connectionsProcessed += 1
      } catch {
        ...(중략)...
      }
    }
  }&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;configureNewConnections 메소드는 newConnections를 통해 새로운 Channel이 입력되면, Connection Id를 부여한 다음 해당 정보를 Kafka Selector에게 전달하여 궁극 적으로는 OS 내부에 해당 소켓 정보를 등록시킨다. 따라서, Processor가 지닌 Kafka Selector를 통해서 향후 Read/Write 요청이 들어왔을 때 이를 감지해 후속 작업을 처리할 수 있다.&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;processNewResponse()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;private def processNewResponses(): Unit = {
  var currentResponse: RequestChannel.Response = null
  while ({currentResponse = dequeueResponse(); currentResponse != null}) {
    val channelId = currentResponse.request.context.connectionId
    try {
      currentResponse match {
        case response: NoOpResponse =&amp;gt;
          ...(중략)...
        case response: SendResponse =&amp;gt;
          sendResponse(response, response.responseSend)
        case response: CloseConnectionResponse =&amp;gt;
          ...(중략)...
          close(channelId)
        ...(중략)...  
      }
    } catch {
        ...(중략)...
    }
  }
}&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;processNewResponse() 메소드는 RequestHandler를 통해 API 호출 후 Client에게 결과를 전달하는 과정을 처리한다. API 수행이 모두 완료되면, Channel을 통해 Processor의 개별 ResponseQueue에 결과가 적재된다.&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;그 이후 위 코드가 실행되면, dequeueResponse() 메소드를 통해 결과를 추출 이후 결과 유형에 따라서 처리를 달리 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;protected[network] def sendResponse(response: RequestChannel.Response, responseSend: Send): Unit = {
  val connectionId = response.request.context.connectionId
  ...(중략)...
  
  if (openOrClosingChannel(connectionId).isDefined) {
    selector.send(new NetworkSend(connectionId, responseSend))
    ...(중략)...
  }
}&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;만약 해당 요청이 SendResponse라면, 위 코드와 같이 Kafka Selector의 저장 Buffer에 임시 보관하도록 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;private def close(connectionId: String): Unit = {
  openOrClosingChannel(connectionId).foreach { channel =&amp;gt;
    ...(중략)...
    selector.close(connectionId)
    ...(중략)...
  }
}&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;만약 처리 유형이 CloseConnectionResponse 형태라면, Selector에게 close 요청을 전달하여 Channel을 정상적으로 종료하도록 한다. 그리고 Kafka Selector는 자신이 지닌 Client 연결 항목에서 해제하고 Channel을 종료시킨다.&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;poll()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;private def poll(): Unit = {
  val pollTimeout = if (newConnections.isEmpty) 300 else 0
  try selector.poll(pollTimeout)
  catch {
    ...(중략)...
  }
}&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;poll() 메소드는 KafkaSelector를 통해 데이터가 Channel에 존재하면, fetch를 요청하는 작업이다.&lt;/p&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;525&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wcZbF/btrsTclXlQT/1V3HMqc86cMKiP20P84Fgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wcZbF/btrsTclXlQT/1V3HMqc86cMKiP20P84Fgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wcZbF/btrsTclXlQT/1V3HMqc86cMKiP20P84Fgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwcZbF%2FbtrsTclXlQT%2F1V3HMqc86cMKiP20P84Fgk%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;525&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;525&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;div data-node-type=&quot;mediaSingle&quot; data-layout=&quot;center&quot; data-width=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 Kafka Selector의 내부 동작 방식은 복잡하지만, 핵심 부분만 도식화해보면 위 흐름과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Processor가 poll()을 통해 변경 대상 Channel 확인 및 데이터 fetch를 요청한다.&lt;/li&gt;
&lt;li&gt;Kafka Selector 내부 nioSelector를 통해서 Kernel에 변경 대상 Channel이 존재하는지를 요청한다.&lt;/li&gt;
&lt;li&gt;Kernel 내부에서 변화가 감지된 Channel 정보를 전달한다.&lt;/li&gt;
&lt;li&gt;해당 Channel이 데이터 수신이 가능한 상태라면 데이터를 추출하여 completedReceives에 저장한다. 만약 processNewResponse() 메소드 수행 결과 전달할 데이터가 존재한다면, Selector 쓰기 임시 버퍼에 저장되어있을 것이다. 해당 내용을 completedSends에 저장한다.&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;※ completedSends, completedReceives에 저장된 데이터는 다음에 확인할 processCompletedReceives()와 processCompletedSends() 과정을 통해서 처리된다.&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;processCompletedReceives()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;private def processCompletedReceives(): Unit = {
  selector.completedReceives.forEach { receive =&amp;gt;
    try {
      openOrClosingChannel(receive.source) match {
        case Some(channel) =&amp;gt;           
            ...(중략)...
          val connectionId = receive.source
          val context = new RequestContext(header, connectionId, channel.socketAddress,channel.principal, listenerName, securityProtocol,
                  channel.channelMetadataRegistry.clientInformation, isPrivilegedListener, channel.principalSerde)

          val req = new RequestChannel.Request(processor = id, context = context, startTimeNanos = nowNanos, memoryPool, receive.payload, requestChannel.metrics, None)

          ...(중략)...
          requestChannel.sendRequest(req)
          ...(중략)...                        
        case None =&amp;gt; ...(중략)...
      }
    } catch {
      ...(중략)...
    }
  }
  selector.clearCompletedReceives()
}&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;이전에 poll() 과정이 끝나고나면, 데이터 수신이 완료된 내용은 completedReceives에 저장됨을 확인했다. processCompletedReceives()는 해당 내용을 가져와서 처리를 수행하기 위해 Context를 만들고 이를 RequestChannel에 추가한다. 이때 데이터 처리 이후 자신의 Processor가 후속 작업을 처리하기 위해서 요청시 자신의 Processor Id를 파라미터로 넘기는 것을 참고하자.&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;Channel에 Request 요청을 넣은 이후에는 completedReceives 내용을 초기화하여 이후 중복 처리 되지 않도록한다.&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;processCompletedSends()&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;private def processCompletedSends(): Unit = {
  selector.completedSends.forEach { send =&amp;gt;
    try {
      ...(중략)...

      // Invoke send completion callback
      response.onComplete.foreach(onComplete =&amp;gt; onComplete(send))

      ...(중략)...
    } catch {
      ...(중략)...
    }
  }
  selector.clearCompletedSends()
}&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;발송할 데이터는 poll() 메소드 수행 과정을 통해 모두 completedSends에 저장되어있다. 이후 해당 메소드에서 실제 후속 작업 처리를 진행함으로써, Client에게 결과를 반환하고, completedSends를 모두 초기화 한다.&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;지금까지, Processor 동작 과정에 대해서 살펴봤다. 해당 내용을 정리하자면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Acceptor로 부터 Client를 할당 받는다.&lt;/li&gt;
&lt;li&gt;API로 부터 처리 결과를 전달받으면, Kafka Selector에 존재하는 임시 버퍼(각 Channel마다 존재)에 저장한다.&lt;/li&gt;
&lt;li&gt;Kafka Selector로부터 Client의 요청이 있는지 확인하며, 이 과정에서 사용자의 요청이 전달된다면, completedReceives에 저장하고, API 처리 결과를 completedSends에 한데 모은다.&lt;/li&gt;
&lt;li&gt;completedReceives은 사용자의 요청이므로 Request Channel에 전달하여 데이터 처리 요청하고, completedSends 내용은 결과를 Client에 반환하는 후속 작업을 처리한다.&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;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Request Handler 동작 과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;class KafkaRequestHandlerPool(val brokerId: Int,
                              val requestChannel: RequestChannel,
                              val apis: ApiRequestHandler,
                              time: Time,
                              numThreads: Int,
                              requestHandlerAvgIdleMetricName: String,
                              logAndThreadNamePrefix : String) extends Logging with KafkaMetricsGroup {
  ...(중략)...

  val runnables = new mutable.ArrayBuffer[KafkaRequestHandler](numThreads)
  for (i &amp;lt;- 0 until numThreads) {
    createHandler(i)
  }

  def createHandler(id: Int): Unit = synchronized {
    runnables += new KafkaRequestHandler(id, brokerId, aggregateIdleMeter, threadPoolSize, requestChannel, apis, time)
    KafkaThread.daemon(logAndThreadNamePrefix + &quot;-kafka-request-handler-&quot; + id, runnables(id)).start()
  }
  
  ...(중략)...
}&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;Request Handler는 KafkaRequestHandlerPool 내부에 존재한다. 따라서 먼저 KafkaRequestHandlerPool가 생성된다. 생성 당시 SocketServer와 연결될 Channel과 API로 전달할 APIRequestHandler가 인자로 같이 전달되는 것을 참고하자. 또한 RequestHandler는 단일 쓰레드로 동작하는 것이 아니라 num.io.threads 인자에 따라 개수 조절이 가능하므로 생성 당시 해당 값 또한 전달된다.&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;위 과정을 통해 KafkaHandler는 데몬쓰레드로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;def run(): Unit = {
  while (!stopped) {
    ...(중략)...
    val req = requestChannel.receiveRequest(300)
    req match {
      case RequestChannel.ShutdownRequest =&amp;gt;
        ...(중략)...
        completeShutdown()
        return

      case request: RequestChannel.Request =&amp;gt;
        try {
          ...(중략)...
          apis.handle(request, requestLocal)
        } catch {
          ...(중략)...
        }
        ...(중략)...
    }
  }
  completeShutdown()
}&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;RequestHandler의 역할은 단순하다. 만약 연결되어있는 Channel이 종료된다면, Handler의 역할이 더이상 필요 없으므로 종료한다. 반면 RequestChannel에서 Request가 존재한다면, API에게 처리를 위임한다.&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;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;API 동작 과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;class ControllerApis(val requestChannel: RequestChannel,
                     val authorizer: Option[Authorizer],
                     val quotas: QuotaManagers,
                     val time: Time,
                     val supportedFeatures: Map[String, VersionRange],
                     val controller: Controller,
                     val raftManager: RaftManager[ApiMessageAndVersion],
                     val config: KafkaConfig,
                     val metaProperties: MetaProperties,
                     val controllerNodes: Seq[Node],
                     val apiVersionManager: ApiVersionManager) extends ApiRequestHandler with Logging {

  val requestHelper = new RequestHandlerHelper(requestChannel, quotas, time)
  
  override def handle(request: RequestChannel.Request, requestLocal: RequestLocal): Unit = {
    try {
      request.header.apiKey match {
        case ApiKeys.FETCH =&amp;gt; handleFetch(request)
        case ApiKeys.FETCH_SNAPSHOT =&amp;gt; handleFetchSnapshot(request)
        ...(중략)...
        case _ =&amp;gt; throw new ApiException(s&quot;Unsupported ApiKey ${request.context.header.apiKey}&quot;)
      }
    } catch {
      case e: FatalExitError =&amp;gt; throw e
      case e: ExecutionException =&amp;gt; requestHelper.handleError(request, e.getCause)
      case e: Throwable =&amp;gt; requestHelper.handleError(request, e)
    }
  }
}&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;API는 사용자 요청을 라우터의 역할로써, Header에 명시된 API Key를 보고 요청을 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;def handleFetch(request: RequestChannel.Request): Unit = {
  authHelper.authorizeClusterOperation(request, CLUSTER_ACTION)
  handleRaftRequest(request, response =&amp;gt; new FetchResponse(response.asInstanceOf[FetchResponseData]))
}&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 class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;private def handleRaftRequest(request: RequestChannel.Request,
                              buildResponse: ApiMessage =&amp;gt; AbstractResponse): Unit = {
  val requestBody = request.body[AbstractRequest]
  ...(중략)...

  future.whenComplete { (responseData, exception) =&amp;gt;
    val response = if (exception != null) {
      requestBody.getErrorResponse(exception)
    } else {
      buildResponse(responseData)
    }
    requestHelper.sendResponseExemptThrottle(request, response)
  }
}&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;반환 메소드 안에서는 ResponseBody를 만든 이후에 requestHelper를 통하여 반환을 위임한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;def sendResponseExemptThrottle(request: RequestChannel.Request,
                               response: AbstractResponse,
                               onComplete: Option[Send =&amp;gt; Unit] = None): Unit = {
  ...(중략)...
  requestChannel.sendResponse(request, response, onComplete)
}&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;requestHelper는 해당 결과를 requestChannel에 전달함으로써 API 역할은 마무리된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Request Channel 동작 과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Request Channel은 Processor와 Handler 그리고 API가 상호 작용에 필수적인 컴포넌트로써 요청을 전달하고 결과를 수신받는 중간 버퍼의 역할을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;class RequestChannel(val queueSize: Int,
                     val metricNamePrefix: String,
                     time: Time,
                     val metrics: RequestChannel.Metrics) extends KafkaMetricsGroup {
  import RequestChannel._
  private val requestQueue = new ArrayBlockingQueue[BaseRequest](queueSize)
  private val processors = new ConcurrentHashMap[Int, Processor]()
  ...(중략)...

  def addProcessor(processor: Processor): Unit = {
    if (processors.putIfAbsent(processor.id, processor) != null)
      warn(s&quot;Unexpected processor with processorId ${processor.id}&quot;)

    ...(중략)...
  }
  
  ...(중략)...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RequestChannel의 핵심 프로퍼티는 requestQueue와 processors이다. 여기서 requestQueue는 사용자 요청을 저장하는 임시 버퍼의 역할을 수행하며, queueSize를 통해 전달된다. 해당 값은 이전에 Socket Server를 살펴볼 때 확인했듯이 data-plane과 control-plane에 따라서 서로 다른 값을 지니고 있다.&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;processors는 API로부터 결과를 반환할 때 Processor Id를 기반으로 빠르게 Processor 객체를 찾기 위한 자료구조로 사용되며, Socket Server의 구동 당시 Acceptor와 Processor가 만들어지고 나면, addProcessor 메소드를 통해서 Processor Id와 참조 객체를 전달받아 processors 자료구조에 삽입하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;private[network] def sendResponse(response: RequestChannel.Response): Unit = {
  ...(중략)...
  
  val processor = processors.get(response.processor)

  if (processor != null) {
    processor.enqueueResponse(response)
  }
}&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;이후 API로부터 결과를 전달받게되면, response에 저장된 processor Id를 기반으로 processors에서 참조 객체를 찾아 Processor에 위치한 Response Queue에 결과를 삽입한다.&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마무리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Kafka Broker 입장에서 네트워크 모델이 어떻게 구성되어있고 요청/응답이 어떤 식으로 이루어지는지 살펴봤다. 내부 구조를 살펴보면서, Kafka에 대한 이해가 조금 더 올라간 것 같다.&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;</description>
      <category>MSA/Kafka</category>
      <category>epoll</category>
      <category>kafka 구조</category>
      <category>kafka 네트워크</category>
      <category>kafka 아키텍처</category>
      <category>nio</category>
      <category>selector</category>
      <category>네트워크</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/169</guid>
      <comments>https://cla9.tistory.com/169#entry169comment</comments>
      <pubDate>Fri, 26 May 2023 10:32:46 +0900</pubDate>
    </item>
    <item>
      <title>7. [envoy-internals] Client 요청 전달 과정 이해하기 - 1</title>
      <link>https://cla9.tistory.com/213</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅은 그동안 envoy-internals 시리즈의 이해를 바탕으로 사용자가 Http 전달을 요청했을 때, Envoy 내부구조를 토대로 네트워크 요청이 어떻게 흘러가는지에 대해서 전체적인 흐름을 상세히 조망해보는 시간을 가져보려 합니다. 사실 envoy에 대해서 분석했던 계기 중 하나가 도대체 어떻게 사용자의 요청이 전달되는지에 대한 궁금증에서 출발했기 때문에 이번 포스팅을 위해서 이전 시리즈의 내용이 존재했다고 생각합니다. 따라서 이번 내용은 이전 내용에 대한 이해가 선행되어야하므로 이전 시리즈 내용을 정독하고 보시는 것을 추천드립니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2.&amp;nbsp; Worker 쓰레드 소켓 할당 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 시리즈 내용을 통해 Listener Manager에서 네트워크 요청 처리를 위해서 여러개의 Worker 쓰레드를 생성하는 것을 이해할 수 있었습니다. 이때 Worker 쓰레드 생성 갯수는 envoy 생성 당시 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;--concurrency&lt;/span&gt;&lt;/b&gt; 인자에 의해서 결정되는 것 또한 확인했습니다. 이번에는 Envoy 기동 과정 중 Worker 쓰레드 상호작용을 살펴보면서 Worker 쓰레드에서 어떻게 네트워크 요청을 처리하는지 살펴보겠습니다.&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;2331&quot; data-origin-height=&quot;2150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwTttk/btrQj5RYytK/egy9why08DyFo45ykoigm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwTttk/btrQj5RYytK/egy9why08DyFo45ykoigm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwTttk/btrQj5RYytK/egy9why08DyFo45ykoigm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwTttk%2FbtrQj5RYytK%2Fegy9why08DyFo45ykoigm0%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;2331&quot; height=&quot;2150&quot; data-origin-width=&quot;2331&quot; data-origin-height=&quot;2150&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;위 그림은 --concurrency 값이 2개이고 static_config, dynamic_config에 의해서 등록된 Listener 개수 또한 2개이면서 모두 TCP임을 가정했습니다. 이때 기동 과정을 살펴보면 다음과 같습니다.&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;1. Envoy 기동 과정에서 --concurrency 값을 살펴보고 Listener Manager에게 Worker 쓰레드 생성을 요청합니다. Listener Manager는 Worker 쓰레드를 요청만큼 생성합니다. 이 과정에서 Worker 쓰레드 내부에 Dispatcher와 Connection Handler가 생성됩니다.&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;2. Worker 쓰레드가 생성되는 과정에서 Envoy의 TLS를 관장하는 메인 쓰레드 InstanceImpl에 쓰레드를 등록합니다. 이 과정에서 InstanceImpl에서 Worker 쓰레드에 위치한 Dispatcher 정보를 registered_threads_ 에 저장할 수 있으며, 향후 Worker 쓰레드의 Dispatcher에 참조가 가능합니다.&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;3. Worker 쓰레드 생성이 마무리되면, Config 파일 파싱 도중 static configuration 정보를 등록하기 위해 Listener Manager에게 Config 정보 등록을 요청합니다. 해당 과정은 향후 LDS에 의해서도 생성될 수 있습니다. 이 과정에서 Listener Component Factory를 통해 &lt;span&gt;--concurrency 만큼&lt;span&gt; Socket을 생성합니다.&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;4. Envoy의 설정이 모두 완료되면, Listener 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;5. Listener Manager에서는 기동 과정에서 등록된 Listener Config 정보를 Worker 쓰레드에 모두 Bind하기 위해 개별 Worker 쓰레드에게 Listener 생성을 요청합니다.&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;6. Worker 쓰레드내에 존재하는 Connection Handler에 Listener를 생성하기 위해 먼저 Listener로부터 자신의 Worker 쓰레드 번호에 해당하는 Socket 정보를 얻어옵니다. 그리고 Worker 쓰레드에서 외부 요청을 참조하기 위해 Dispatcher에게 Socket 정보를 전달하면서 Listener 생성을 요청합니다.&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;connection_handler_impl.cc&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;details-&amp;gt;addActiveListener(
    config, address, listener_reject_fraction_, disable_listeners_,
    std::make_unique&amp;lt;ActiveTcpListener&amp;gt;(
        *this, config, runtime,
        socket_factory-&amp;gt;getListenSocket(worker_index_.has_value() ? *worker_index_ : 0),
        address, config.connectionBalancer(*address)));&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;active_tcp_listener.cc&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;ActiveTcpListener::ActiveTcpListener(Network::TcpConnectionHandler&amp;amp; parent,
                                     Network::ListenerConfig&amp;amp; config, Runtime::Loader&amp;amp; runtime,
                                     Network::SocketSharedPtr&amp;amp;&amp;amp; socket,
                                     Network::Address::InstanceConstSharedPtr&amp;amp; listen_address,
                                     Network::ConnectionBalancer&amp;amp; connection_balancer)
    : OwnedActiveStreamListenerBase(
          parent, parent.dispatcher(),
          parent.dispatcher().createListener(std::move(socket), *this, runtime, config.bindToPort(),
                                             config.ignoreGlobalConnLimit()),
          config),
      tcp_conn_handler_(parent), connection_balancer_(connection_balancer),
      listen_address_(listen_address) {
  connection_balancer_.registerHandler(*this);
}&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;p data-ke-size=&quot;size16&quot;&gt;7. Dispatcher는 전달받은 Socket 정보를 토대로 Listener를 생성하여 반환합니다. 이때 주의해서 살펴볼 것은 사용자가 접속했을 때, 인자로 전달받은 TcpListenerCallbacks에게 Accept 요청을 수행하는데 호출되는 주체가 Connection Handler에서 전달한 ActiveTcpListener라는 것입니다. 따라서 향후 사용자가 접속하게되면, 그에 대한 Accept 처리는 ActiveTcpListener가 담당하게됩니다.&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;dispatcher_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Network::ListenerPtr DispatcherImpl::createListener(Network::SocketSharedPtr&amp;amp;&amp;amp; socket,
                                                    Network::TcpListenerCallbacks&amp;amp; cb,
                                                    Runtime::Loader&amp;amp; runtime, bool bind_to_port,
                                                    bool ignore_global_conn_limit) {
  return std::make_unique&amp;lt;Network::TcpListenerImpl&amp;gt;(*this, random_generator_, runtime,
                                                    std::move(socket), cb, bind_to_port,
                                                    ignore_global_conn_limit);
}&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;8. 생성된 Listener에서는 소켓 정보를 libevent에 등록하여 향후 Client가 접속을 요청했을 때 libevent에 의해 요청을 전달받을 수 있도록 등록합니다.&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;tcp_listener_impl.cc&lt;/p&gt;
&lt;pre class=&quot;elixir&quot;&gt;&lt;code&gt;socket_-&amp;gt;ioHandle().initializeFileEvent(
    dispatcher, [this](uint32_t events) -&amp;gt; void { onSocketEvent(events); },
    Event::FileTriggerType::Level, Event::FileReadyType::Read);&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;p data-ke-size=&quot;size16&quot;&gt;위와같이 8단계를 거치게되면, 생성된 모든 Worker 쓰레드에서 Listener 및 소켓을 생성하고 Client 요청을 수신받을 수 있는 상태가 완료됩니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Client Connection 연결 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Envoy를 기동하는 과정에서 Worker 쓰레드가 생성되고, Listener 및 Socket을 생성하는 것을 살펴봤습니다.&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;tcp_listener_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;TcpListenerImpl::TcpListenerImpl(Event::DispatcherImpl&amp;amp; dispatcher, Random::RandomGenerator&amp;amp; random,
                                 Runtime::Loader&amp;amp; runtime, SocketSharedPtr socket,
                                 TcpListenerCallbacks&amp;amp; cb, bool bind_to_port,
                                 bool ignore_global_conn_limit)
    : BaseListenerImpl(dispatcher, std::move(socket)), cb_(cb), random_(random), runtime_(runtime),
      bind_to_port_(bind_to_port), reject_fraction_(0.0),
      ignore_global_conn_limit_(ignore_global_conn_limit) {
  if (bind_to_port) {
    socket_-&amp;gt;ioHandle().initializeFileEvent(
        dispatcher, [this](uint32_t events) -&amp;gt; void { onSocketEvent(events); },
        Event::FileTriggerType::Level, Event::FileReadyType::Read);
  }
}&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;이때 dispatcher를 통해서 libevent에서 해당 소켓에 이벤트가 감지되면, onSocketEvent 메소드를 호출하도록 위 코드와 같이 등록됩니다. 즉 이 과정을 통해서 각각의 Worker 쓰레드에 존재하는 Listener는 dispatcher에 의해 libevent로 등록되었으므로 사용자가 OS로부터 Socket 생성을 요청했을 때, OS는 등록된 socket 중 하나를 임의로 선정하여 요청을 전달할 수 있게됩니다.&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;978&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dswNd5/btrQ0vIDSZj/Lqj9qn4VzkIokKaKSGVRb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dswNd5/btrQ0vIDSZj/Lqj9qn4VzkIokKaKSGVRb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dswNd5/btrQ0vIDSZj/Lqj9qn4VzkIokKaKSGVRb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdswNd5%2FbtrQ0vIDSZj%2FLqj9qn4VzkIokKaKSGVRb0%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;640&quot; height=&quot;161&quot; data-origin-width=&quot;978&quot; data-origin-height=&quot;246&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;그렇다면 Listener 설정이 모두 완료된 이후 Client로부터 Connection 요청이 들어오면 어떠한 과정을 거치게될까요?&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;1391&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EpSf7/btrQj5Ert9x/gOAMTfz7AQUbJYuZk2HTt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EpSf7/btrQj5Ert9x/gOAMTfz7AQUbJYuZk2HTt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EpSf7/btrQj5Ert9x/gOAMTfz7AQUbJYuZk2HTt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEpSf7%2FbtrQj5Ert9x%2FgOAMTfz7AQUbJYuZk2HTt1%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;640&quot; height=&quot;430&quot; data-origin-width=&quot;1391&quot; data-origin-height=&quot;934&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;먼저 Socket Event가 감지되면, 위와 같이 두개의 Worker 쓰레드 중 누가 해당 요청을 처리해야하는지 선택해야합니다. 이때 Worker 쓰레드 선정에 대한 결정은 이전에 설명했듯이 전적으로 OS가 수행합니다. 따라서 가령 위와같이 Worker_0번이 선택되었으면, 해당 쓰레드의 onSocketEvent가 실행될 것입니다.&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;tcp_listener_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void TcpListenerImpl::onSocketEvent(short flags) {
  ASSERT(bind_to_port_);
  ASSERT(flags &amp;amp; (Event::FileReadyType::Read));

  while (1) {
    if (!socket_-&amp;gt;ioHandle().isOpen()) {
      PANIC(fmt::format(&quot;listener accept failure: {}&quot;, errorDetails(errno)));
    }

    sockaddr_storage remote_addr;
    socklen_t remote_addr_len = sizeof(remote_addr);

    IoHandlePtr io_handle =
        socket_-&amp;gt;ioHandle().accept(reinterpret_cast&amp;lt;sockaddr*&amp;gt;(&amp;amp;remote_addr), &amp;amp;remote_addr_len);
    if (io_handle == nullptr) {
      break;
    }

    if (rejectCxOverGlobalLimit()) {
      io_handle-&amp;gt;close();
      cb_.onReject(TcpListenerCallbacks::RejectCause::GlobalCxLimit);
      continue;
    } else if (random_.bernoulli(reject_fraction_)) {
      io_handle-&amp;gt;close();
      cb_.onReject(TcpListenerCallbacks::RejectCause::OverloadAction);
      continue;
    }

    const Address::InstanceConstSharedPtr&amp;amp; local_address =
        local_address_ ? local_address_ : io_handle-&amp;gt;localAddress();

    const Address::InstanceConstSharedPtr remote_address =
        (remote_addr.ss_family == AF_UNIX)
            ? io_handle-&amp;gt;peerAddress()
            : Address::addressFromSockAddrOrThrow(remote_addr, remote_addr_len,
                                                  local_address-&amp;gt;ip()-&amp;gt;version() ==
                                                      Address::IpVersion::v6);
    cb_.onAccept(
        std::make_unique&amp;lt;AcceptedSocketImpl&amp;gt;(std::move(io_handle), local_address, remote_address));
  }
}&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;onSocketEvent 메소드가 호출되면, 가장 먼저 수행하는 것은 연결된 Connection 갯수가 Global 설정을 넘어섰는지 확인합니다. 이 과정에서 Global Limit이 지정되어있고 신규 연결 요청이 Limit을 넘어서게되면, 해당 소켓에 대한 연결은 Close하고 종결처리 합니다.&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;1335&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CXU1O/btrQZs0c8T0/uKmCIXXkB2vUw0uLz7CIv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CXU1O/btrQZs0c8T0/uKmCIXXkB2vUw0uLz7CIv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CXU1O/btrQZs0c8T0/uKmCIXXkB2vUw0uLz7CIv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCXU1O%2FbtrQZs0c8T0%2FuKmCIXXkB2vUw0uLz7CIv1%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;640&quot; height=&quot;262&quot; data-origin-width=&quot;1335&quot; data-origin-height=&quot;546&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;이때 Global Limit으로 지정될 수 있는 값은 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;overload.global_downstrea_max_connections&lt;/span&gt;&lt;/b&gt;에 의해서 지정될 수 있으며, 해당 값은 OverloadManager에 의해서 관리되는 값입니다. 따라서 현재 Socket에 Accepted된 개수가 해당 값을 넘었을 경우에는 Socket 연결을 해제합니다. 해당 값에 대한 자세한 설명은 &lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.envoyproxy.io/docs/envoy/latest/configuration/operations/overload_manager/overload_manager#limiting-active-connections&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;envoy 공식문서&lt;/a&gt;&lt;/span&gt;&lt;/u&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 요청이 Global Limit을 넘지 않았을 경우는 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;AcceptedSocket&lt;/span&gt;&lt;/b&gt;을 생성하고 Worker 쓰레드 내 Listener는 Socket 연결에 대한 Accept 처리를 위임합니다.&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;1681&quot; data-origin-height=&quot;891&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGj8ug/btrQ1gdbTRA/lSdaGO11Nz3pdaSI9Yc0q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGj8ug/btrQ1gdbTRA/lSdaGO11Nz3pdaSI9Yc0q1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGj8ug/btrQ1gdbTRA/lSdaGO11Nz3pdaSI9Yc0q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGj8ug%2FbtrQ1gdbTRA%2FlSdaGO11Nz3pdaSI9Yc0q1%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;1681&quot; height=&quot;891&quot; data-origin-width=&quot;1681&quot; data-origin-height=&quot;891&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;위 그림은 AcceptedSocket 클래스 구조를 나타냅니다. 위 내용을 통해서 우리는 AcceptedSocket을 만드는 이유에 대해서 유추해볼 수 있습니다. 코드를 살펴보면, global_accetped_socket_count_ 라는 값이 static으로 지정되어있음을 알 수 있습니다. 그리고 생성자, 소멸자 단계에서 해당 값이 증감하는 것 또한 알 수 있습니다.&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;이를 통해서 확인되는 사실은 Socket이 접속하면 현재 Accepted된 Socket 개수를 파악할 수 있습니다. 또한 새로운 Socket이 연결되었을 때 Global Limit을 넘는지 검증할 수 있는 기준을 제시합니다.&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;AcceptedSocket을 만들고나면 그 다음에는 해당 Socket을 Listener에서 Accept하는 과정이 진행됩니다.&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;tcp_listener_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1668222082217&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void TcpListenerImpl::onSocketEvent(short flags) {
  ...(중략)...
  cb_.onAccept(
        std::make_unique&amp;lt;AcceptedSocketImpl&amp;gt;(std::move(io_handle), local_address, remote_address));
}&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;이전에 살펴본 onSocketEvent 메소드의 마지막 줄을 살펴보면, 저장된 Callback에서 onAccept를 수행해달라고 요청하는 것을 볼 수 있습니다. 이는 TcpListenerImpl을 생성할 때, 해당 Callback 값이 Worker에 존재하는 ActiveTcpListener 이므로 해당 ActiveTcpListener가 실질적으로 Accept를 수행함을 의미합니다.&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;active_tcp_listener.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ActiveTcpListener::onAccept(Network::ConnectionSocketPtr&amp;amp;&amp;amp; socket) {
  if (listenerConnectionLimitReached()) {
    RELEASE_ASSERT(socket-&amp;gt;connectionInfoProvider().remoteAddress() != nullptr, &quot;&quot;);
    ENVOY_LOG(trace, &quot;closing connection from {}: listener connection limit reached for {}&quot;,
              socket-&amp;gt;connectionInfoProvider().remoteAddress()-&amp;gt;asString(), config_-&amp;gt;name());
    socket-&amp;gt;close();
    stats_.downstream_cx_overflow_.inc();
    return;
  }

  onAcceptWorker(std::move(socket), config_-&amp;gt;handOffRestoredDestinationConnections(), false);
}&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;ActiveTcpListener에서의 Accept 과정을 살펴보면 위 코드와 같습니다.&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;1004&quot; data-origin-height=&quot;255&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MUjId/btrQ0v9Lu0h/EK3mOSHLaJEaYNBwiKTKU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MUjId/btrQ0v9Lu0h/EK3mOSHLaJEaYNBwiKTKU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MUjId/btrQ0v9Lu0h/EK3mOSHLaJEaYNBwiKTKU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMUjId%2FbtrQ0v9Lu0h%2FEK3mOSHLaJEaYNBwiKTKU1%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;1004&quot; height=&quot;255&quot; data-origin-width=&quot;1004&quot; data-origin-height=&quot;255&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;p data-ke-size=&quot;size16&quot;&gt;먼저 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;listenerConnectionLimitReached&lt;/span&gt; &lt;/b&gt;메소드를 수행하면서 이를 위반할 경우 Socket을 Close하는 것을 볼 수 있습니다. 이는 이전의 Connection은 Envoy 전체의 Connection을 살펴본 것이라면, 이번에는 개별 listener 별로 Connection 제한이 있는지 검사하고 만약 지정된 값이 있을 경우 그 값을 넘어서게되면 Accept하지 않습니다. 해당 설정은 Listener에서 수행할 수 있으며 &lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/runtime&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;envoy 공식 문서&lt;/a&gt;&lt;/span&gt;&lt;/u&gt;에서 이에 대해서 소개하고 있으니 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ActiveTcpListener에서는 Limit 검사만 체크하고 다시 Socket에 대한 Accept는 onAcceptWorker 메소드 호출을 통해 후속 작업을 처리합니다.&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;active_tcp_listener.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ActiveTcpListener::onAcceptWorker(Network::ConnectionSocketPtr&amp;amp;&amp;amp; socket,
                                       bool hand_off_restored_destination_connections,
                                       bool rebalanced) {
  if (!rebalanced) {
    Network::BalancedConnectionHandler&amp;amp; target_handler =
        connection_balancer_.pickTargetHandler(*this);
    if (&amp;amp;target_handler != this) {
      target_handler.post(std::move(socket));
      return;
    }
  }

  auto active_socket = std::make_unique&amp;lt;ActiveTcpSocket&amp;gt;(*this, std::move(socket),
                                                         hand_off_restored_destination_connections);

  onSocketAccepted(std::move(active_socket));
}&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;이번에는 onAcceptWorker 메소드 내용을 살펴보겠습니다. 해당 코드를 살펴보면 connection_balancer에 의해서 TargetHandler를 구하는 것을 볼 수 있습니다.&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;그렇다면 connection_balancer는 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1668224405597&quot; class=&quot;stylus&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static_resources:
  listeners:
  - connection_balance_config:
      extend_balance:
        name: envoy.network.connection_balance.dlb
        typed_config:
          &quot;@type&quot;: type.googleapis.com/envoy.extensions.network.connection_balance.dlb.v3alpha.Dlb&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;envoy에서는 쓰레드간의 효율적인 Socket 분배를 위해서 만약 하드웨어에서 DLB 지원이 된다면, 이를 사용하여 Connection을 안정적으로 분배할 수 있는 기능을 제공합니다. 이때 위와 같이 connection_balance_config를 지정하면, 해당 설정을 토대로 connection_balancer가 Target을 지정합니다.&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;해당 기술은 내부적으로는 Intel DLB hardware를 통해 구현되며, 위와 같이 Config 설정이 지정되어있다면 다른 Target으로 Load를 분산시킬 수 있습니다. 만약 지정되지 않았다면, 현재 Worker 쓰레드에서 Accept 과정이 정상적으로 진행될 것입니다. 이와 관련된 자세한 내용은 &lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.envoyproxy.io/docs/envoy/latest/configuration/other_features/dlb&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;envoy 공식문서&lt;/a&gt;&lt;/span&gt;&lt;/u&gt;에 설명되어있으니 참고 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 본 포스팅에서는 DLB 설정이 지정되어있지 않았다고 가정하므로 현재 Worker 쓰레드에서 해당 처리를 진행한다고 가정하겠습니다.&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;active_tcp_listener.cc&lt;/p&gt;
&lt;pre id=&quot;code_1668225306855&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void ActiveTcpListener::onAcceptWorker(Network::ConnectionSocketPtr&amp;amp;&amp;amp; socket,
                                       bool hand_off_restored_destination_connections,
                                       bool rebalanced) {
  ...(중략)...

  auto active_socket = std::make_unique&amp;lt;ActiveTcpSocket&amp;gt;(*this, std::move(socket),
                                                         hand_off_restored_destination_connections);

  onSocketAccepted(std::move(active_socket));
}&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;connection_balancer에 의해서 선정된 target이 자기 자신이라면 해당 연결을 허용하기 위해 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;ActiveTcpSocket&lt;/span&gt;&lt;/b&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;ActiveTcpSocket까지 생성되면, 향후 Client의 요청은 해당 Socket을 통해서 모두 처리가됩니다.&amp;nbsp;&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;634&quot; data-origin-height=&quot;522&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8pS7N/btrQZQmho7C/pBCiNXqC8KIEF3RoAsQF5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8pS7N/btrQZQmho7C/pBCiNXqC8KIEF3RoAsQF5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8pS7N/btrQZQmho7C/pBCiNXqC8KIEF3RoAsQF5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8pS7N%2FbtrQZQmho7C%2FpBCiNXqC8KIEF3RoAsQF5k%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;480&quot; height=&quot;395&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;522&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;즉 이말은 이전에 Envoy를 처음 학습할 때 살펴봤듯이 Client가 Envoy에게 특정 API를 요청하면, 내부적으로 Listener Filters와 Filter Chains를 통과하면서 Upstream 대상을 찾고 전달한다고 했는데, 이 과정을 수행하는 주체가 해당 소켓이 됩니다. 따라서 Active Tcp Socket은 이를 지원하기 위해서 다양한 내부 프로퍼티가 있는데, 그 중 Filter와 관련된 것이 accept_filters 입니다. 해당 속성을 기반으로 향후 Listener Filters를 만들고 이를 수행하고 그 다음에는 Filter Chains를 생성하고 이를 수행하는 작업을 진행합니다. 그리고 해당 작업을 위해서 ActiveTcpSocket을 만든 이후 onSocketAccepted를 호출합니다.&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;active_stream_listener_base.h&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void onSocketAccepted(std::unique_ptr&amp;lt;ActiveTcpSocket&amp;gt; active_socket) {
  // Create and run the filters
  if (config_-&amp;gt;filterChainFactory().createListenerFilterChain(*active_socket)) {
    active_socket-&amp;gt;startFilterChain();
  } else {
    // If create listener filter chain failed, it means the listener is missing
    // config due to the ECDS. Then close the connection directly.
    active_socket-&amp;gt;socket().close();
    ASSERT(active_socket-&amp;gt;isEndFilterIteration());
  }

  // Move active_socket to the sockets_ list if filter iteration needs to continue later.
  // Otherwise we let active_socket be destructed when it goes out of scope.
  if (!active_socket-&amp;gt;isEndFilterIteration()) {
    active_socket-&amp;gt;startTimer();
    LinkedList::moveIntoListBack(std::move(active_socket), sockets_);
  } else {
    if (!active_socket-&amp;gt;connected()) {
      // If active_socket is about to be destructed, emit logs if a connection is not created.
      if (active_socket-&amp;gt;streamInfo() != nullptr) {
        emitLogs(*config_, *active_socket-&amp;gt;streamInfo());
      } else {
        // If the active_socket is not connected, this socket is not promoted to active
        // connection. Thus the stream_info_ is owned by this active socket.
        ENVOY_BUG(active_socket-&amp;gt;streamInfo() != nullptr,
                  &quot;the unconnected active socket must have stream info.&quot;);
      }
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드는 onSocketAccepted 메소드 내용입니다. 코드를 살펴보면, Socket이 Accepted 되면 가장 먼저 해당 소켓에 해당되는 ListenerFilterChain을 생성하고 FilterChain을 수행하는 것을 볼 수 있습니다. 즉 이 과정부터 해당 소켓은 각종 Filter들을 통과하면서 Upstream 연결이 이어지게됩니다.&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;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;1772&quot; data-origin-height=&quot;978&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctQdYe/btrQ65YltyG/g4fGNTUXwmVHKxKkljMbu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctQdYe/btrQ65YltyG/g4fGNTUXwmVHKxKkljMbu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctQdYe/btrQ65YltyG/g4fGNTUXwmVHKxKkljMbu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctQdYe%2FbtrQ65YltyG%2Fg4fGNTUXwmVHKxKkljMbu1%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;1772&quot; height=&quot;978&quot; data-origin-width=&quot;1772&quot; data-origin-height=&quot;978&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;이전 내용을 토대로 ActiveTcpListener 에서 ActiveTcpSocket을 만든 것을 확인했습니다. 그리고 createListenerFactory 메소드를 호출하면서 생성한 ActiveTcpSocket을 전달했습니다.&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;그러면 내부적으로는 Listener Config에 이미 저장되어있는 listener_filter_factories 내부에 매핑된 Factory Callback을 하나씩 실행시키면서 ActiveTcpSocket의 accept_filters에 Filter를 하나씩 생성하는 과정을 거칩니다. 해당 과정을 코드로 살펴보면 다음과 같습니다.&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;listener_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1668415774451&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bool ListenerImpl::createListenerFilterChain(Network::ListenerFilterManager&amp;amp; manager) {
  if (Configuration::FilterChainUtility::buildFilterChain(manager, listener_filter_factories_)) {
    return true;
  } else {
    ENVOY_LOG(debug, &quot;New connection accepted while missing configuration. &quot;
                     &quot;Close socket and stop the iteration onAccept.&quot;);
    missing_listener_config_stats_.extension_config_missing_.inc();
    return false;
  }
}&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;위 코드는 ActiveTcpListener 에서 ActiveTcpSocket을 생성 후 ListenerFilterChain을 생성하기 위해 createListenerFilterChain 메소드를 호출하였을 때 과정을 나타냅니다. 코드를 살펴보면, buildFilterChain 함수 호출을 통해 자신이 보유하고 있는 listener_filter_factories_ 목록을 전달하는 것을 볼 수 있습니다.&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;configuration_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FilterChainUtility::buildFilterChain(Network::ListenerFilterManager&amp;amp; filter_manager,
                                          const Filter::ListenerFilterFactoriesList&amp;amp; factories) {
  for (const auto&amp;amp; filter_config_provider : factories) {
    auto config = filter_config_provider-&amp;gt;config();
    if (!config.has_value()) {
      return false;
    }
    auto config_value = config.value();
    config_value(filter_manager);
  }

  return true;
}&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;buildFilterChain 내부를 살펴보면, 전달받은 factories를 순회하면서 callback 함수를 순차적으로 수행시키는 것을 볼 수 있습니다. 그리고 해당 callback을 수행할 때 전달받은 ActiveTcpSocket 정보를 다시 넘기는 것을 확인할 수 있습니다.&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;config.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;[listener_filter_matcher, config](Network::ListenerFilterManager&amp;amp; filter_manager) -&amp;gt; void {
  filter_manager.addAcceptFilter(listener_filter_matcher, std::make_unique&amp;lt;Filter&amp;gt;(config));
};&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;위 코드는 http instpector Listener Filter의 Factory 코드이며, 개별 Listener Filter Factory의 리턴 값은 위와 같이 FilterManager 즉 ActiveTcpSocket 정보로 받아서&amp;nbsp;addAcceptFilter 메소드를 호출하고 있는 것을 볼 수 있습니다. 또한 인자를 통해서 Factory에서 보유하고 있는 Filter 정보를 새롭게 생성하여 전달하는 것을 볼 수 있습니다.&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;active_tcp_socket.h&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;void addAcceptFilter(const Network::ListenerFilterMatcherSharedPtr&amp;amp; listener_filter_matcher,
                     Network::ListenerFilterPtr&amp;amp;&amp;amp; filter) override {
  accept_filters_.emplace_back(
      std::make_unique&amp;lt;GenericListenerFilter&amp;gt;(listener_filter_matcher, std::move(filter)));
}&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;그리고 ActiveTcpSocket에서는 전달받은 Filter를 accept_filters에 추가함으로써 Listener Filter Chain을 완성합니다.&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;active_stream_listener_base.h&lt;/p&gt;
&lt;pre id=&quot;code_1668423176646&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void onSocketAccepted(std::unique_ptr&amp;lt;ActiveTcpSocket&amp;gt; active_socket) {  
  ...(중략)...
    active_socket-&amp;gt;startFilterChain();
  ...(후략)...
}&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;FilterChain이 완성되면, startFilterChain()을 통해 ListenerFilterChain을 차례로 수행하도록 요청합니다.&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;active_tcp_socket.h&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;void startFilterChain() { continueFilterChain(true); }&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;startFilterChain()은 다시 내부에 continueFilterChain 메소드에 처리를 위임합니다.&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;active_tcp_socket.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ActiveTcpSocket::continueFilterChain(bool success) {
  if (success) {
    bool no_error = true;
    if (iter_ == accept_filters_.end()) {
      iter_ = accept_filters_.begin();
    } else {
      iter_ = std::next(iter_);
    }

    for (; iter_ != accept_filters_.end(); iter_++) {
      Network::FilterStatus status = (*iter_)-&amp;gt;onAccept(*this);
      if (status == Network::FilterStatus::StopIteration) {        
        if (!socket().ioHandle().isOpen()) {          
          no_error = false;
          break;
        } else {
          // If the listener maxReadBytes() is 0, then it shouldn't return
          // `FilterStatus::StopIteration` from `onAccept` to wait for more data.
          ASSERT((*iter_)-&amp;gt;maxReadBytes() != 0);
          if (listener_filter_buffer_ == nullptr) {
            if ((*iter_)-&amp;gt;maxReadBytes() &amp;gt; 0) {
              createListenerFilterBuffer();
            }
          } else {
            // If the current filter expect more data than previous filters, then
            // increase the filter buffer's capacity.
            if (listener_filter_buffer_-&amp;gt;capacity() &amp;lt; (*iter_)-&amp;gt;maxReadBytes()) {
              listener_filter_buffer_-&amp;gt;resetCapacity((*iter_)-&amp;gt;maxReadBytes());
            }
          }
          if (listener_filter_buffer_ != nullptr) {
            listener_filter_buffer_-&amp;gt;activateFileEvent(Event::FileReadyType::Read);
          }
          // Waiting for more data.
          return;
        }
      }
    }
    // Successfully ran all the accept filters.
    if (no_error) {
      newConnection();
    } else {
      // Signal the caller that no extra filter chain iteration is needed.
      iter_ = accept_filters_.end();
    }
  }

  // Filter execution concluded, unlink and delete this ActiveTcpSocket if it was linked.
  if (inserted()) {
    unlink();
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;continueFilterChain&lt;span&gt; &lt;/span&gt;&lt;/span&gt;코드 내용을 자세히 살펴보면, 생성된 Listener Filters(accept_filters)를 순회하면서 onAccept를 통해 Filter로직을 수행하는 것을 볼 수 있습니다.&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;710&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blAxzL/btrRebWT9Eu/VuoD2Ri0mHTBpnRzAL8Mbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blAxzL/btrRebWT9Eu/VuoD2Ri0mHTBpnRzAL8Mbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blAxzL/btrRebWT9Eu/VuoD2Ri0mHTBpnRzAL8Mbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblAxzL%2FbtrRebWT9Eu%2FVuoD2Ri0mHTBpnRzAL8Mbk%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;580&quot; height=&quot;431&quot; data-origin-width=&quot;710&quot; data-origin-height=&quot;527&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;만약 Filter onAccept 순회 도중에 Stop을 해야될 경우가 존재한다면, 이유를 살펴보고 요청을 중지하던지 아니면 설정 변경 후 재수행을 수행하도록 작성되어있습니다.&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;그리고 모든 Filter 순회가 종료되면, 사용자 요청에 대한 Metadata 및 요청 정보를 모두 분석하였으므로 그 다음에는 &lt;b&gt;newConnection()&lt;/b&gt; 메소드를 호출하여 본격적으로 &lt;b&gt;downstream&lt;/b&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;active_tcp_socket.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ActiveTcpSocket::newConnection() {
  connected_ = true;

  // Check if the socket may need to be redirected to another listener.
  Network::BalancedConnectionHandlerOptRef new_listener;

  if (hand_off_restored_destination_connections_ &amp;amp;&amp;amp;
      socket_-&amp;gt;connectionInfoProvider().localAddressRestored()) {
    // Find a listener associated with the original destination address.
    new_listener =
        listener_.getBalancedHandlerByAddress(*socket_-&amp;gt;connectionInfoProvider().localAddress());
  }

  // Reset the file events which are registered by listener filter.
  // reference https://github.com/envoyproxy/envoy/issues/8925.
  if (listener_filter_buffer_ != nullptr) {
    listener_filter_buffer_-&amp;gt;reset();
  }

  if (new_listener.has_value()) {
    ...(중략)...
    new_listener.value().get().onAcceptWorker(std::move(socket_), false, false);
  } else {
    // Set default transport protocol if none of the listener filters did it.
    if (socket_-&amp;gt;detectedTransportProtocol().empty()) {
      socket_-&amp;gt;setDetectedTransportProtocol(&quot;raw_buffer&quot;);
    }
    accept_filters_.clear();
    // Create a new connection on this listener.
    listener_.newConnection(std::move(socket_), std::move(stream_info_));
  }
}&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;newConnection 메소드를 호출하면, 다른 Listener로 Redirect 해야할 필요가 있는지를 살펴보고 listener에 새로운 Connection 할당을 요청합니다.&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;active_stream_listener_base.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ActiveStreamListenerBase::newConnection(Network::ConnectionSocketPtr&amp;amp;&amp;amp; socket,
                                             std::unique_ptr&amp;lt;StreamInfo::StreamInfo&amp;gt; stream_info) {
  // Find matching filter chain.
  const auto filter_chain = config_-&amp;gt;filterChainManager().findFilterChain(*socket);
  if (filter_chain == nullptr) {
    ...(중략)...
    socket-&amp;gt;close();
    return;
  }
 ...(후략)...
}&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;1943&quot; data-origin-height=&quot;674&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4sDGi/btrReKLu84h/cZpxMkKALaXNKUlYFWfJlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4sDGi/btrReKLu84h/cZpxMkKALaXNKUlYFWfJlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4sDGi/btrReKLu84h/cZpxMkKALaXNKUlYFWfJlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4sDGi%2FbtrReKLu84h%2FcZpxMkKALaXNKUlYFWfJlK%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;1943&quot; height=&quot;674&quot; data-origin-width=&quot;1943&quot; data-origin-height=&quot;674&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;newConnection 메소드를 살펴보면, 먼저 Listener의 FilterChainManager로부터 해당 소켓 정보에 해당하는 Filter Chain을 찾는 과정을 수행합니다. 그리고 이 과정에서 매칭되는 Filter Chain을 찾지 못한다면, 연결된 Socket을 종료하고 마칩니다.&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;filter_chain_manager_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;const Network::FilterChain*
FilterChainManagerImpl::findFilterChain(const Network::ConnectionSocket&amp;amp; socket) const {
  if (matcher_) {
    return findFilterChainUsingMatcher(socket);
  }

  const auto&amp;amp; address = socket.connectionInfoProvider().localAddress();

  const Network::FilterChain* best_match_filter_chain = nullptr;
  // Match on destination port (only for IP addresses).
  if (address-&amp;gt;type() == Network::Address::Type::Ip) {
    const auto port_match = destination_ports_map_.find(address-&amp;gt;ip()-&amp;gt;port());
    if (port_match != destination_ports_map_.end()) {
      best_match_filter_chain = findFilterChainForDestinationIP(*port_match-&amp;gt;second.second, socket);
      if (best_match_filter_chain != nullptr) {
        return best_match_filter_chain;
      } else {
        // There is entry for specific port but none of the filter chain matches. Instead of
        // matching catch-all port 0, the fallback filter chain is returned.
        return default_filter_chain_.get();
      }
    }
  }
  // Match on catch-all port 0 if there is no specific port sub tree.
  const auto port_match = destination_ports_map_.find(0);
  if (port_match != destination_ports_map_.end()) {
    best_match_filter_chain = findFilterChainForDestinationIP(*port_match-&amp;gt;second.second, socket);
  }
  return best_match_filter_chain != nullptr
             ? best_match_filter_chain
             // Neither exact port nor catch-all port matches. Use fallback filter chain.
             : default_filter_chain_.get();
}&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;위 코드는 Filter Chain Manager로 부터 Filter Chain을 찾는 과정을 보여줍니다.&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;Filter Chain Manager는 Listener 기동 시점에 설정 정보를 파싱하여 Filter Chain 정보를 모두 가지고 있습니다. 따라서 Socket의 Address 정보를 토대로 원하는 Filter Chain을 찾아서 반환해줍니다. 만약에 상응하는 Filter Chain 정보를 찾지 못한 경우에는 Default Filter Chain을 반환합니다.&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;active_stream_listener_base.cc&lt;/p&gt;
&lt;pre id=&quot;code_1667479605283&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void ActiveStreamListenerBase::newConnection(Network::ConnectionSocketPtr&amp;amp;&amp;amp; socket,
                                             std::unique_ptr&amp;lt;StreamInfo::StreamInfo&amp;gt; stream_info) {
  ...(중략)...
  stream_info-&amp;gt;setFilterChainName(filter_chain-&amp;gt;name());
  auto transport_socket = filter_chain-&amp;gt;transportSocketFactory().createDownstreamTransportSocket();
  auto server_conn_ptr = dispatcher().createServerConnection(
      std::move(socket), std::move(transport_socket), *stream_info);
  if (const auto timeout = filter_chain-&amp;gt;transportSocketConnectTimeout();
      timeout != std::chrono::milliseconds::zero()) {
    server_conn_ptr-&amp;gt;setTransportSocketConnectTimeout(
        timeout, stats_.downstream_cx_transport_socket_connect_timeout_);
  }
  server_conn_ptr-&amp;gt;setBufferLimits(config_-&amp;gt;perConnectionBufferLimitBytes());
  ...(중략)...
  const bool empty_filter_chain = !config_-&amp;gt;filterChainFactory().createNetworkFilterChain(
      *server_conn_ptr, filter_chain-&amp;gt;networkFilterFactories());
  if (empty_filter_chain) {
    ...(중략),,,
    server_conn_ptr-&amp;gt;close(Network::ConnectionCloseType::NoFlush);
  }
  newActiveConnection(*filter_chain, std::move(server_conn_ptr), std::move(stream_info));
}&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;매칭되는 Filter Chain을 찾았으면, 위 코드와 같은 과정을 거칩니다. 주요 내용을 살펴보면 다음과 같습니다.&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;1. Downstream을 위한 Transport Socket을 생성합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. dispatcher에게 ServerConnection을 요청합니다.&amp;nbsp;&lt;/b&gt;&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;2412&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/botTne/btrReJZ8mQD/BscKLZJXMxYSW8mUqqnP6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/botTne/btrReJZ8mQD/BscKLZJXMxYSW8mUqqnP6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/botTne/btrReJZ8mQD/BscKLZJXMxYSW8mUqqnP6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbotTne%2FbtrReJZ8mQD%2FBscKLZJXMxYSW8mUqqnP6K%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;2412&quot; height=&quot;673&quot; data-origin-width=&quot;2412&quot; data-origin-height=&quot;673&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;dispatcher_impl.cc&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;Network::ServerConnectionPtr
DispatcherImpl::createServerConnection(Network::ConnectionSocketPtr&amp;amp;&amp;amp; socket,
                                       Network::TransportSocketPtr&amp;amp;&amp;amp; transport_socket,
                                       StreamInfo::StreamInfo&amp;amp; stream_info) {
  ASSERT(isThreadSafe());
  return std::make_unique&amp;lt;Network::ServerConnectionImpl&amp;gt;(
      *this, std::move(socket), std::move(transport_socket), stream_info, true);
}&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;이때 내부적으로 dispatcher에서는 ServerConnectionImpl을 생성하는 것을 볼 수 있습니다.&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;connection_impl.cc&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;ServerConnectionImpl::ServerConnectionImpl(Event::Dispatcher&amp;amp; dispatcher,
                                           ConnectionSocketPtr&amp;amp;&amp;amp; socket,
                                           TransportSocketPtr&amp;amp;&amp;amp; transport_socket,
                                           StreamInfo::StreamInfo&amp;amp; stream_info, bool connected)
    : ConnectionImpl(dispatcher, std::move(socket), std::move(transport_socket), stream_info,
                     connected) {}&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;생성되는 ServerConnectionImpl의 생성자는 위와 같으며, 부호 생성자에게 전달받은 인자들을 넘깁니다.&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;connection_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;ConnectionImpl::ConnectionImpl(Event::Dispatcher&amp;amp; dispatcher, ConnectionSocketPtr&amp;amp;&amp;amp; socket,
                               TransportSocketPtr&amp;amp;&amp;amp; transport_socket,
                               StreamInfo::StreamInfo&amp;amp; stream_info, bool connected)
    : ConnectionImplBase(dispatcher, next_global_id_++),
      transport_socket_(std::move(transport_socket)), socket_(std::move(socket)),
      stream_info_(stream_info), filter_manager_(*this, *socket_),
      write_buffer_(dispatcher.getWatermarkFactory().createBuffer(
          [this]() -&amp;gt; void { this-&amp;gt;onWriteBufferLowWatermark(); },
          [this]() -&amp;gt; void { this-&amp;gt;onWriteBufferHighWatermark(); },
          []() -&amp;gt; void { /* TODO(adisuissa): Handle overflow watermark */ })),
      read_buffer_(dispatcher.getWatermarkFactory().createBuffer(
          [this]() -&amp;gt; void { this-&amp;gt;onReadBufferLowWatermark(); },
          [this]() -&amp;gt; void { this-&amp;gt;onReadBufferHighWatermark(); },
          []() -&amp;gt; void { /* TODO(adisuissa): Handle overflow watermark */ })),
      write_buffer_above_high_watermark_(false), detect_early_close_(true),
      enable_half_close_(false), read_end_stream_raised_(false), read_end_stream_(false),
      write_end_stream_(false), current_write_end_stream_(false), dispatch_buffered_data_(false),
      transport_wants_read_(false) {

  if (!connected) {
    connecting_ = true;
  }

  Event::FileTriggerType trigger = Event::PlatformDefaultTriggerType;

  // We never ask for both early close and read at the same time. If we are reading, we want to
  // consume all available data.
  socket_-&amp;gt;ioHandle().initializeFileEvent(
      dispatcher_, [this](uint32_t events) -&amp;gt; void { onFileEvent(events); }, trigger,
      Event::FileReadyType::Read | Event::FileReadyType::Write);

  transport_socket_-&amp;gt;setTransportSocketCallbacks(*this);

  // TODO(soulxu): generate the connection id inside the addressProvider directly,
  // then we don't need a setter or any of the optional stuff.
  socket_-&amp;gt;connectionInfoProvider().setConnectionID(id());
  socket_-&amp;gt;connectionInfoProvider().setSslConnection(transport_socket_-&amp;gt;ssl());
}&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;1353&quot; data-origin-height=&quot;1414&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BKEQh/btrRcOIa9Ff/QXmdrLTsI4kUla4UocA2D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BKEQh/btrRcOIa9Ff/QXmdrLTsI4kUla4UocA2D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BKEQh/btrRcOIa9Ff/QXmdrLTsI4kUla4UocA2D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBKEQh%2FbtrRcOIa9Ff%2FQXmdrLTsI4kUla4UocA2D0%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;1414&quot; data-origin-width=&quot;1353&quot; data-origin-height=&quot;1414&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;ConnectionImpl 생성자 내부를 살펴보면, Socket 내부에 Write/Read 전용 Buffer를 설정하는 것을 볼 수 있습니다. 해당 Buffer는 사용자가 HTTP 요청을 전달했을 때 데이터를 읽는 용도 그리고 upstream으로부터 응답 데이터가 전달되었을 때 기록하는 용도로 사용됩니다.&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;또한 downstream을 위한 transport_socket 생성 및 Filter Chains 관리를 위한 filter_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;중요하게 살펴볼 부분은 향후 Socket 내부에 Read/Write 이벤트가 감지되면 이를 통지받고 후속 작업을 처리하기 위해 Dispatcher에게 onFileEvent()를 등록하는 부분입니다. 이는 내부적으로 다시 libevent에게 등록이 되며, 향후 Client가 HTTP 요청을 전달하게 되면, 해당 메소드로 요청 항목이 전달되어 후속 작업을 수행할 수 있습니다.&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;&lt;b&gt;3. Filter Chain으로부터 생성해야할 Filter 목록을 확인하여 Filter Chains(Network Filters)를 생성합니다.&lt;/b&gt;&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;1798&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d18BT3/btrRb7BuhW2/QLF9MNmfkfZ9K9ukUgVNT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d18BT3/btrRb7BuhW2/QLF9MNmfkfZ9K9ukUgVNT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d18BT3/btrRb7BuhW2/QLF9MNmfkfZ9K9ukUgVNT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd18BT3%2FbtrRb7BuhW2%2FQLF9MNmfkfZ9K9ukUgVNT0%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;1798&quot; height=&quot;673&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;673&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;ServerConnection을 생성하고 나면, 그 다음에는 Filter Chains를 생성하는 작업을 수행합니다. 이를 위해 Listener 에게 FilterChain 생성을 요청합니다. 이때 생성되는 Filter Chains는 ServerConnection 내부에 매핑되어야하기 때문에 해당 정보를 Filter Chains와 같이 전달합니다.&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;listener_impl.cc&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;bool ListenerImpl::createNetworkFilterChain(
    Network::Connection&amp;amp; connection,
    const std::vector&amp;lt;Network::FilterFactoryCb&amp;gt;&amp;amp; filter_factories) {
  return Configuration::FilterChainUtility::buildFilterChain(connection, filter_factories);
}&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;요청을 전달받은 Listener는 Filter Chains 생성 처리를 buildFilterChain util 함수에 위임합니다.&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;configuration_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FilterChainUtility::buildFilterChain(Network::FilterManager&amp;amp; filter_manager,
                                          const std::vector&amp;lt;Network::FilterFactoryCb&amp;gt;&amp;amp; factories) {
  for (const Network::FilterFactoryCb&amp;amp; factory : factories) {
    factory(filter_manager);
  }

  return filter_manager.initializeReadFilters();
}&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;buildFilterChains는 전달받은 Filter를 순차적으로 순회하면서 ServerConnection 내부에 생성하도록 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;config.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;return [singletons, filter_config, &amp;amp;context,
        clear_hop_by_hop_headers](Network::FilterManager&amp;amp; filter_manager) -&amp;gt; void {
  auto hcm = std::make_shared&amp;lt;Http::ConnectionManagerImpl&amp;gt;(
      *filter_config, context.drainDecision(), context.api().randomGenerator(),
      context.httpContext(), context.runtime(), context.localInfo(), context.clusterManager(),
      context.overloadManager(), context.mainThreadDispatcher().timeSource());
  if (!clear_hop_by_hop_headers) {
    hcm-&amp;gt;setClearHopByHopResponseHeaders(false);
  }
  filter_manager.addReadFilter(std::move(hcm));
};&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;가령 위 코드는 Http Connection Manager Filter에서 반환하는 Callback Factory 메소드가 수행되었다고 가정해봤습니다.&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;1538&quot; data-origin-height=&quot;919&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EakBm/btrRcMQ7kLW/H4NtWLdjQKy65IgqvLNdIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EakBm/btrRcMQ7kLW/H4NtWLdjQKy65IgqvLNdIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EakBm/btrRcMQ7kLW/H4NtWLdjQKy65IgqvLNdIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEakBm%2FbtrRcMQ7kLW%2FH4NtWLdjQKy65IgqvLNdIK%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;1538&quot; height=&quot;919&quot; data-origin-width=&quot;1538&quot; data-origin-height=&quot;919&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;이때 인자로 ServerConnection을 전달받았으며, 해당 람다내에서는 HttpConnectionManager를 생성한 다음 ServerConnection 내부에 addReadFilter를 호출하여 Filter 정보를 등록하는 것을 볼 수 있습니다.&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;connection_impl.cc&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void ConnectionImpl::addReadFilter(ReadFilterSharedPtr filter) {
  filter_manager_.addReadFilter(filter);
}&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;이 경우 호출되는 ServerConnection 내부에서는 filter_manager를 통해 readFilter를 등록하도록 재요청합니다.&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;filter_manager_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1668441142952&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void FilterManagerImpl::addReadFilter(ReadFilterSharedPtr filter) {
  ASSERT(connection_.state() == Connection::State::Open);
  ActiveReadFilterPtr new_filter = std::make_unique&amp;lt;ActiveReadFilter&amp;gt;(*this, filter);
  filter-&amp;gt;initializeReadFilterCallbacks(*new_filter);
  LinkedList::moveIntoListBack(std::move(new_filter), upstream_filters_);
}&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;filter_manager에서 addReadFilter가 호출되면, 생성된 Filter에 ActiveReadFilter 정보를 전달하여 해당 Filter 내에서 향후 Callback할 수 있도록 initializeReadFilterCallbacks를 등록합니다. 그리고 자신의 upstream_filters_에 Read Filter를 등록하는 것을 볼 수 있습니다.&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;conn_manager_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void ConnectionManagerImpl::initializeReadFilterCallbacks(Network::ReadFilterCallbacks&amp;amp; callbacks) {
  read_callbacks_ = &amp;amp;callbacks;
  stats_.named_.downstream_cx_total_.inc();
  stats_.named_.downstream_cx_active_.inc();
  if (read_callbacks_-&amp;gt;connection().ssl()) {
    stats_.named_.downstream_cx_ssl_total_.inc();
    stats_.named_.downstream_cx_ssl_active_.inc();
  }

  read_callbacks_-&amp;gt;connection().addConnectionCallbacks(*this);

  if (!read_callbacks_-&amp;gt;connection()
           .streamInfo()
           .filterState()
           -&amp;gt;hasData&amp;lt;Network::ProxyProtocolFilterState&amp;gt;(Network::ProxyProtocolFilterState::key())) {
    read_callbacks_-&amp;gt;connection().streamInfo().filterState()-&amp;gt;setData(
        Network::ProxyProtocolFilterState::key(),
        std::make_unique&amp;lt;Network::ProxyProtocolFilterState&amp;gt;(Network::ProxyProtocolData{
            read_callbacks_-&amp;gt;connection().connectionInfoProvider().remoteAddress(),
            read_callbacks_-&amp;gt;connection().connectionInfoProvider().localAddress()}),
        StreamInfo::FilterState::StateType::ReadOnly,
        StreamInfo::FilterState::LifeSpan::Connection);
  }

  if (config_.idleTimeout()) {
    connection_idle_timer_ = read_callbacks_-&amp;gt;connection().dispatcher().createScaledTimer(
        Event::ScaledTimerType::HttpDownstreamIdleConnectionTimeout,
        [this]() -&amp;gt; void { onIdleTimeout(); });
    connection_idle_timer_-&amp;gt;enableTimer(config_.idleTimeout().value());
  }

  if (config_.maxConnectionDuration()) {
    connection_duration_timer_ = read_callbacks_-&amp;gt;connection().dispatcher().createTimer(
        [this]() -&amp;gt; void { onConnectionDurationTimeout(); });
    connection_duration_timer_-&amp;gt;enableTimer(config_.maxConnectionDuration().value());
  }

  read_callbacks_-&amp;gt;connection().setDelayedCloseTimeout(config_.delayedCloseTimeout());

  read_callbacks_-&amp;gt;connection().setConnectionStats(
      {stats_.named_.downstream_cx_rx_bytes_total_, stats_.named_.downstream_cx_rx_bytes_buffered_,
       stats_.named_.downstream_cx_tx_bytes_total_, stats_.named_.downstream_cx_tx_bytes_buffered_,
       nullptr, &amp;amp;stats_.named_.downstream_cx_delayed_close_timeout_});
}&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;등록된 Filter가 Http Connection Manager Filter임을 가정하였으므로, initializeReadFilter를 호출하면, 위 코드 구문이 호출될 것입니다. 해당 코드를 개략적으로 살펴보면, Callback을 자신의 프로퍼티에 등록하는 작업 외에 metric 정보를 갱신하는 것을 볼 수 있습니다. 또한 Timeout이 발생하면 이를 처리하기 위해 dispatcher에게 다양한 Timer 생성 및 Timeout 발생 시 처리 하도록 등록하는 등 Read Filter 동작과 관련된 기본 초기화 작업을 수행합니다.&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;Http Connection Manager의 경우는 Read Filter의 역할만을 수행하기 때문에 filter_manager 내부에 upstream_filters에만 filter가 추가됩니다. 하지만 FilterChain Factory 내부에는 Read Filter 뿐만 아니라 Writer Filter의 역할을 수행하는 것도 있고 Read/Write를 모두 수행하는 Filter가 존재합니다.&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;filter_manager_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void FilterManagerImpl::addWriteFilter(WriteFilterSharedPtr filter) {
  ASSERT(connection_.state() == Connection::State::Open);
  ActiveWriteFilterPtr new_filter = std::make_unique&amp;lt;ActiveWriteFilter&amp;gt;(*this, filter);
  filter-&amp;gt;initializeWriteFilterCallbacks(*new_filter);
  LinkedList::moveIntoList(std::move(new_filter), downstream_filters_);
}

void FilterManagerImpl::addFilter(FilterSharedPtr filter) {
  addReadFilter(filter);
  addWriteFilter(filter);
}&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;이 경우에는 람다내에서 ServerConnectionImpl에 Filter 생성을 요청할 때 addWriteFilter 혹은 addFilter 메소드를 호출합니다. 가령 addWriterFilter의 경우는 Writer Filter만의 역할을 수행하기 때문에 궁극적으로는 filter_manager의 downstream_filters_ 에 추가되면서 Read Filter 등록과 마찬가지로 ActiveWriterFilter 인스턴스를 만들어서 향후 Callback할 수 있도록 initializeWriteFilterCallbacks를 등록하는 것을 볼 수 있습니다.&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;반면 addFilter의 경우는 Read/Write를 모두 수행할 때 호출되기 때문에 addReadFilter와 addWriteFilter를 차례대로 호출합니다.&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;configuration_impl.cc&lt;/p&gt;
&lt;pre class=&quot;rust&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;bool FilterChainUtility::buildFilterChain(Network::FilterManager&amp;amp; filter_manager,
                                          const std::vector&amp;lt;Network::FilterFactoryCb&amp;gt;&amp;amp; factories) {
  for (const Network::FilterFactoryCb&amp;amp; factory : factories) {
    factory(filter_manager);
  }

  return filter_manager.initializeReadFilters();
}&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;Filter Factories로 부터 Filter 생성이 모두 완료되면, ServerConnectionImpl 내부 filter_manager에는 upstream 전용 filter와 downstream 전용 filter가 모두 생성되어있습니다. 그 과정이 완료되면 ServerConnectionImpl 내부의 initializeReadFilters()를 호출 합니다.&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;connection_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;bool ConnectionImpl::initializeReadFilters() { return filter_manager_.initializeReadFilters(); }&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;그리고 해당 요청을 전달받은 ServerConnectionImpl 내부에서는 다시 filter_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;filter_manager_impl.cc&lt;/p&gt;
&lt;pre class=&quot;zephir&quot;&gt;&lt;code&gt;bool FilterManagerImpl::initializeReadFilters() {
  if (upstream_filters_.empty()) {
    return false;
  }
  onContinueReading(nullptr, connection_);
  return true;
}

void FilterManagerImpl::onContinueReading(ActiveReadFilter* filter,
                                          ReadBufferSource&amp;amp; buffer_source) {
  // Filter could return status == FilterStatus::StopIteration immediately, close the connection and
  // use callback to call this function.
  if (connection_.state() != Connection::State::Open) {
    return;
  }

  std::list&amp;lt;ActiveReadFilterPtr&amp;gt;::iterator entry;
  if (!filter) {
    connection_.streamInfo().addBytesReceived(buffer_source.getReadBuffer().buffer.length());
    entry = upstream_filters_.begin();
  } else {
    entry = std::next(filter-&amp;gt;entry());
  }

  for (; entry != upstream_filters_.end(); entry++) {
    if (!(*entry)-&amp;gt;filter_) {
      continue;
    }
    if (!(*entry)-&amp;gt;initialized_) {
      (*entry)-&amp;gt;initialized_ = true;
      FilterStatus status = (*entry)-&amp;gt;filter_-&amp;gt;onNewConnection();
      if (status == FilterStatus::StopIteration || connection_.state() != Connection::State::Open) {
        return;
      }
    }

    StreamBuffer read_buffer = buffer_source.getReadBuffer();
    if (read_buffer.buffer.length() &amp;gt; 0 || read_buffer.end_stream) {
      FilterStatus status = (*entry)-&amp;gt;filter_-&amp;gt;onData(read_buffer.buffer, read_buffer.end_stream);
      if (status == FilterStatus::StopIteration || connection_.state() != Connection::State::Open) {
        return;
      }
    }
  }
}&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;해당 처리 과정을 살펴보면, 초기에 upstream_filters_에 등록할 때 ActiveReadFilter 인스턴스를 새로 생성해서 등록했는데 초기 initialized_ 값은 기본적으로 false 입니다. 따라서 위 코드에서는 등록된 upstream_filters_ 를 순회하면서 가장 먼저 onNewConnection() 작업을 수행할 것입니다.&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;여기에는 등록된 Filter가 Http Connection Manager 하나만 존재한다고 가정하기 때문에, 해당 Filter의 onNewConnection()이 호출됩니다.&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;connection_manager_impl.cc&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;Network::FilterStatus ConnectionManagerImpl::onNewConnection() {
  if (!read_callbacks_-&amp;gt;connection().streamInfo().protocol()) {
    // For Non-QUIC traffic, continue passing data to filters.
    return Network::FilterStatus::Continue;
  }
  // Only QUIC connection's stream_info_ specifies protocol.
  Buffer::OwnedImpl dummy;
  createCodec(dummy);
  ASSERT(codec_-&amp;gt;protocol() == Protocol::Http3);
  // Stop iterating through each filters for QUIC. Currently a QUIC connection
  // only supports one filter, HCM, and bypasses the onData() interface. Because
  // QUICHE already handles de-multiplexing.
  return Network::FilterStatus::StopIteration;
}&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;해당 코드를 잠시 살펴보면, QUIC 트래픽인지 확인하고 일반 HTTP 프로토콜이라면 해당 Filter 사용이 가능하도록 구현되어있음을 확인할 수 있습니다.&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;여기까지 수행하면 Filter Chains를 생성하고 Filter의 초기화까지 수행하는 전 과정을 살펴볼 수 있습니다.&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;active_stream_listener_base.cc&lt;/p&gt;
&lt;pre id=&quot;code_1668442773342&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void ActiveStreamListenerBase::newConnection(Network::ConnectionSocketPtr&amp;amp;&amp;amp; socket,
                                             std::unique_ptr&amp;lt;StreamInfo::StreamInfo&amp;gt; stream_info) {
  ...(중략)...
  const bool empty_filter_chain = !config_-&amp;gt;filterChainFactory().createNetworkFilterChain(
      *server_conn_ptr, filter_chain-&amp;gt;networkFilterFactories());
  if (empty_filter_chain) {
    ...(중략),,,
    server_conn_ptr-&amp;gt;close(Network::ConnectionCloseType::NoFlush);
  }
  newActiveConnection(*filter_chain, std::move(server_conn_ptr), std::move(stream_info));
}&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;앞서 살펴본 긴 과정의 Filter Chains를 생성하고나면, Filter Chains가 존재하는지 여부를 bool 값으로 반환합니다. 만약에 Filter Chains 생성 과정에서 생성된 FilterChains가 전혀 존재하지 않는다면, Stream을 연결하여 작업을 이어나가는 것이 무의미하기 때문에 해당 Server Connection을 종료합니다.&amp;nbsp;그렇지 않을 경우 생성된 Filter Chains를 기반으로 ActiveConnection을 생성하는 과정을 진행합니다.&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;active_tcp_listener.cc&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;void ActiveTcpListener::newActiveConnection(const Network::FilterChain&amp;amp; filter_chain,
                                            Network::ServerConnectionPtr server_conn_ptr,
                                            std::unique_ptr&amp;lt;StreamInfo::StreamInfo&amp;gt; stream_info) {
  auto&amp;amp; active_connections = getOrCreateActiveConnections(filter_chain);
  auto active_connection =
      std::make_unique&amp;lt;ActiveTcpConnection&amp;gt;(active_connections, std::move(server_conn_ptr),
                                            dispatcher().timeSource(), std::move(stream_info));
  // If the connection is already closed, we can just let this connection immediately die.
  if (active_connection-&amp;gt;connection_-&amp;gt;state() != Network::Connection::State::Closed) {
    ENVOY_CONN_LOG(
        debug, &quot;new connection from {}&quot;, *active_connection-&amp;gt;connection_,
        active_connection-&amp;gt;connection_-&amp;gt;connectionInfoProvider().remoteAddress()-&amp;gt;asString());
    active_connection-&amp;gt;connection_-&amp;gt;addConnectionCallbacks(*active_connection);
    LinkedList::moveIntoList(std::move(active_connection), active_connections.connections_);
  }
}&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;여기서 ActiveConnection이란 생성된 Filter Chains를 사용하는 Connection이 얼마나 있는지를 관리하기 위한 자료구조 입니다. 따라서 getOrCreateActiveConnection 메소드를 호출함으로써, 먼저 해당 Filter에 존재하는 ActiveConnection이 있는지를 살펴봅니다.&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;active_stream_listener_base.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;ActiveConnections&amp;amp; OwnedActiveStreamListenerBase::getOrCreateActiveConnections(
    const Network::FilterChain&amp;amp; filter_chain) {
  ActiveConnectionCollectionPtr&amp;amp; connections = connections_by_context_[&amp;amp;filter_chain];
  if (connections == nullptr) {
    connections = std::make_unique&amp;lt;ActiveConnections&amp;gt;(*this, filter_chain);
  }
  return *connections;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;active_stream_listener_base.h&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;absl::flat_hash_map&amp;lt;const Network::FilterChain*, ActiveConnectionCollectionPtr&amp;gt;
    connections_by_context_;&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;active_stream_listener_base.h&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;class ActiveConnections : public Event::DeferredDeletable {
public:
  ActiveConnections(OwnedActiveStreamListenerBase&amp;amp; listener,
                    const Network::FilterChain&amp;amp; filter_chain);
  ~ActiveConnections() override;

  // listener filter chain pair is the owner of the connections
  OwnedActiveStreamListenerBase&amp;amp; listener_;
  const Network::FilterChain&amp;amp; filter_chain_;
  // Owned connections.
  std::list&amp;lt;std::unique_ptr&amp;lt;ActiveTcpConnection&amp;gt;&amp;gt; connections_;
};
&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;p data-ke-size=&quot;size16&quot;&gt;이때 ActiveConnections를 가져오는 메소드는 위 코드와 같으며, 내부적으로는 FilterChain 별로 ActiveConnection 목록을 관리하는 hash map을 통해서 참조하는 것을 확인할 수 있습니다.&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;ActiveConnections를 가져오면 신규로 생성하는 ActiveConnection을 만들고 ActiveConnections에 추가하는 것으로 Client의 접속 요청 이후 Connection 할당 과정은 마무리됩니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Client Connection 연결 과정 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 Client Connection 연결 과정을 코드를 통해서 어떻게 구성되는지 집중적으로 살펴봤습니다. 다만 지엽적인 내용까지 살펴보느라 전체적인 흐름을 이해하기 쉽지 않았을 수도 있습니다. 따라서 이번에는 큰 그림에서 컴포넌트간 메시지 교환을 중점으로 Client Connection이 어떻게 연결되는지 개략적으로 다시 살펴보도록 하겠습니다.&amp;nbsp;&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;2732&quot; data-origin-height=&quot;2112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpIrwj/btrRdaK7uJk/YpNyPUkzeSbumPKlLWckc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpIrwj/btrRdaK7uJk/YpNyPUkzeSbumPKlLWckc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpIrwj/btrRdaK7uJk/YpNyPUkzeSbumPKlLWckc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpIrwj%2FbtrRdaK7uJk%2FYpNyPUkzeSbumPKlLWckc0%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;2732&quot; height=&quot;2112&quot; data-origin-width=&quot;2732&quot; data-origin-height=&quot;2112&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;1. Libevent 로 부터 Listener 가 Listen 하고 있는 소켓으로 사용자의 Connection 연결 요청이 접수되었을 때, 내부적으로 존재하는 Worker 쓰레드 중 하나를 선정하여 해당 Listener의 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;onSocketEvent&lt;/b&gt; &lt;/span&gt;메소드를 호출합니다. 참고로 위 예시에서는 Listener 0에 요청이 전달되었을 때 Worker 0번 쓰레드가 담당한다고 가정하여 ActiveTcpListener 0이 수신받았습니다.&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;2. ActiveTcpListener에서는 전체 연결된 Connection 개수가 Global Limit을 넘었는지 확인하고 내부적으로 &lt;span&gt;AcceptedSocket을 거쳐 ActiveTcpSocket을 생성하여 Connection 연결을 Accept합니다.&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&gt;3. 생성된 Socket에서 Listener Filter Chain을 생성하기 위해 Listener Config에게 Listener Filter Chain 정보를 요청합니다.&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&gt;4. Listener Config는 Envoy 기동 과정에서 Parsing되었거나 LDS로 부터 갱신된 Listener Filter Chains 정보를 기반으로 Chains 내부에 존재하는 Factory Callback 메소드를 순회하면서 ActiveTcpSocket 내부에 있는 accept_filters에 &lt;span&gt;Listener&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;Filter를 생성하여 바인딩합니다.&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&gt;5. ActiveTcpSocket은 Listener Filters를 순회하면서 onAccept를 수행합니다.&amp;nbsp;&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;6. Filter Chains(Network Filters)를 생성하기 위해 ActiveTcpListener는 Listener Config 로부터 보유하고 있는 Filter Chains 정보를 요청합니다.&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;7. Listener Config 내부에 존재하는 filter_chain_manager에는 Trie, HashMap 등을 비롯하여 Filter Chain 정보를 빠르게 찾기 위한 다양한 자료구조가 존재하는데, 이를 활용하여 사용자 요청한 Filter Chain 정보를 제공합니다.&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;8. ActiveTcpListener는 ServerConnetionImpl을 생성하기 위해 Dispatcher에 요청합니다.&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;9. Dispatcher는 ServerConnectionImpl을 생성합니다.&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;10. 생성된 ServerConnectionImpl은 사용자가 접속 연결 이후 실질적으로 HTTP 요청 시, 이벤트를 수신 받아야되기 때문에 Dispatcher에게 Socket 이벤트를 전달하도록 등록 요청합니다.&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;11. Didpatcher는 Libevent에게 ServerConnectionImpl이 요청한 정보를 등록합니다. 이후 해당 Socket에 이벤트가 감지되면 ServerConnectionImpl이 등록한 onFileEvent 메소드를 호출합니다.&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;12. ActiveTcpListener는 Filter Chains를 생성하기 위해 Listner Config에게 처리를 요청합니다.&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;13. Listener Config는 Util 함수를 활용하여 Filter Chains를 생성합니다. 이때 Filter의 특성에 따라 Read Filter인 경우에는 ServerConnectionImpl 내에 있는 filter_manager의 upstream_filters_에 매핑되고 Write Filter라면 downstream_filters_에 매핑됩니다. 만약 Read/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;14. 생성된 Filter Chain에 연결된 Server Connection 정보를 관리하기 위해 ActiveTcpSocket 내부에 있는 connections_by_contexts_에 신규로 생성된 ActiveConnection 정보를 등록합니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 사용자가 HTTP 요청을 통해 envoy의 기능을 이용하고자 할 때 내부적으로 Listener 구성이 어떻게 되어있는지 그리고 Client Connection이 어떻게 할당되는지에 대해서 집중적으로 알아봤습니다.&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;envoy를 처음 학습하면, 이러한 과정이 마법처럼 느껴지는데 코드를 통해서 살펴보니 동작 과정에 대해서 어느정도 이해할 수 있었던 것 같습니다. 다음 포스팅에서는 연결이 완료된 이후 실제 HTTP 요청이 전달될 때 처리 과정에 대해서 살펴보겠습니다.&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <category>Envoy</category>
      <category>envoy architecture</category>
      <category>envoy http</category>
      <category>envoy listener</category>
      <category>envoy 구조</category>
      <category>envoy 아키텍처</category>
      <category>envoy 연결</category>
      <category>Istio</category>
      <category>istio architecture</category>
      <category>istio 아키텍처</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/213</guid>
      <comments>https://cla9.tistory.com/213#entry213comment</comments>
      <pubDate>Thu, 25 May 2023 12:14:05 +0900</pubDate>
    </item>
    <item>
      <title>6. [envoy-internals] Http Connection Manager</title>
      <link>https://cla9.tistory.com/221</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 시리즈 내용을 통해서 Envoy에서 가장 중요한 2가지 컴포넌트인 Listener Manager와 Cluster 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;&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;1055&quot; data-origin-height=&quot;527&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H4BfP/btr6EVEPzGt/jgBQb0bjR7m1nwQ9MAokjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H4BfP/btr6EVEPzGt/jgBQb0bjR7m1nwQ9MAokjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H4BfP/btr6EVEPzGt/jgBQb0bjR7m1nwQ9MAokjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH4BfP%2Fbtr6EVEPzGt%2FjgBQb0bjR7m1nwQ9MAokjK%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;1055&quot; height=&quot;527&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;527&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;p data-ke-size=&quot;size16&quot;&gt;다시 한번 전체 과정을 간략하게 표현해보면 다음과 같습니다.사용자의 접속 요청이 전달되면, Listener에서 요청을 전달받고 이에 부합하는 Cluster를 찾아 Downstream과 Upstream을 연결합니다. 이때 만약 사용자 요청이 Http 연결일 경우에는 Listener에서 Cluster Manager로 Cluster를 요청하는 주체가 Listener가 보유하고 있는 Network Filter 중 하나인 HttpConnectionManager입니다. 해당 컴포넌트는 Listener 내부에 존재하는 Filter이지만, 사용자 요청처리를 수행하는데 있어 중요한 역할을 수행하기 때문에. 이번 포스팅에서는 HttpConnectionManager의 기능에 대해서 살펴보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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;2. HttpConnectionManager&lt;/h2&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;2063&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clQdWM/btr6BjNsjw3/3T9bDGf5rBXSJMYOlhiY4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clQdWM/btr6BjNsjw3/3T9bDGf5rBXSJMYOlhiY4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clQdWM/btr6BjNsjw3/3T9bDGf5rBXSJMYOlhiY4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclQdWM%2Fbtr6BjNsjw3%2F3T9bDGf5rBXSJMYOlhiY4k%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;2063&quot; height=&quot;658&quot; data-origin-width=&quot;2063&quot; data-origin-height=&quot;658&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;이전 포스팅을 통해 Listener 하위에는 위 그림과 같이 Filter Chains를 관리하는 filter_chain_manager가 존재함을 확인했습니다.&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;652&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFcgpG/btr6K398kaO/34wa2RWJZjIvks4qXtXECk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFcgpG/btr6K398kaO/34wa2RWJZjIvks4qXtXECk/img.png&quot; data-alt=&quot;출처 :https://www.envoyproxy.io/docs/envoy/latest/intro/life_of_a_request#listener-tcp-accept&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFcgpG/btr6K398kaO/34wa2RWJZjIvks4qXtXECk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFcgpG%2Fbtr6K398kaO%2F34wa2RWJZjIvks4qXtXECk%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;504&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :https://www.envoyproxy.io/docs/envoy/latest/intro/life_of_a_request#listener-tcp-accept&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 구조 내부에는 위 그림과 같이 Trie 구조가 존재하는데, 이는 사용자가 Listener를 통해서 연결을 원하는 domain을 전달했을 때, 해당 요청에 부합되는 Filter Chain을 찾기 위한 용도로 사용됩니다. 따라서 각각의 Trie 별로 매칭되는 노드에는 Filter들이 Chain 형식으로 매핑되어있습니다.&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;따라서 사용자가 접속을 요청하면, 내부적으로는 해당 Trie를 검색해서 사용자의 요청에 부합되는 항목의 Filter Chain 목록을 반환합니다. 그리고 해당 Chain 목록을 전달받으면, Chain을 탐색하면서 매핑된 Factory Callback을 실행하여 Filter Chain을 구성합니다.&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;Envoy가 제공하는 Network Filter는 굉장히 많은 종류가 있는데요. 그 중 HttpConnectionManager는 Http 요청을 처리하는 Filter로써 이에 대해서 다루어보고자 합니다.&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;1982&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xzIiI/btr6EUlEEK6/o7DAoBo4LkB3Q24Vk0oInK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xzIiI/btr6EUlEEK6/o7DAoBo4LkB3Q24Vk0oInK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xzIiI/btr6EUlEEK6/o7DAoBo4LkB3Q24Vk0oInK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxzIiI%2Fbtr6EUlEEK6%2Fo7DAoBo4LkB3Q24Vk0oInK%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;1982&quot; height=&quot;594&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;594&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;p data-ke-size=&quot;size16&quot;&gt;HttpConnectionManager는 Network Filter이므로 앞서 설명한 것과 같이 사용자가 명시한 도메인에 해당되는 Network Filter 목록에 Http 처리가 매핑되어있을 경우 Filter Chain에 포함되어있을 것입니다.&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;이때 사용자가 Listener에 접속을 요청할 때마다 위 그림과 같이 내부적으로 Upstream Connection을 만듭니다. 위 그림은 현재 2개의 Connection이 생성되었음을 가정했습니다. 이때 각각의 ServerConnection은 별개의 Network Filter Chain을 가지고 있게되고 만약 2개의 Connection이 모두 Http 처리를 담당해야한다면 위 그림과 같이 2개의 별개 HttpConnectionManager가 생성될 것입니다.&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;그렇다면 HttpConnectionManager는 어떻게 구성되어있을까요?&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;552&quot; data-origin-height=&quot;729&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXKbwL/btr7dV5dNkZ/WnEGrvfm8v7Kz1UxnHUK9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXKbwL/btr7dV5dNkZ/WnEGrvfm8v7Kz1UxnHUK9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXKbwL/btr7dV5dNkZ/WnEGrvfm8v7Kz1UxnHUK9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXKbwL%2Fbtr7dV5dNkZ%2FWnEGrvfm8v7Kz1UxnHUK9k%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;552&quot; height=&quot;729&quot; data-origin-width=&quot;552&quot; data-origin-height=&quot;729&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;생성된 HttpConnectionManager는 대략 위와 같은 모습을 구성하고 있습니다. 물론 그림으로 표현한 속성 이외에 다양한 프로퍼티가 존재하지만, 핵심이라고 생각하는 몇 개만 표현했습니다. 그렇다면 각각의 속성은 무엇이며 어떤 과정을 거쳐 생성될까요? 핵심적인 요소에 대해서 하나씩 살펴보겠습니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. RDS&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 살펴볼 것은 RDS입니다. 해당 컴포넌트는 이전에 살펴본 HttpConnectionManager의 속성 중 config_와 관련이 있습니다. 해당 config안에는 HttpConnectionManager를 구성하는데 있어 필요한 속성이 지정되어있는데요. 그중 Route 관련 속성은 route_config_provider입니다.&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;1982&quot; data-origin-height=&quot;594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xzIiI/btr6EUlEEK6/o7DAoBo4LkB3Q24Vk0oInK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xzIiI/btr6EUlEEK6/o7DAoBo4LkB3Q24Vk0oInK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xzIiI/btr6EUlEEK6/o7DAoBo4LkB3Q24Vk0oInK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxzIiI%2Fbtr6EUlEEK6%2Fo7DAoBo4LkB3Q24Vk0oInK%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;1982&quot; height=&quot;594&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;594&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;이전에 살펴본 그림에서 HttpConnectionManager와 SingletonManager가 연관관계를 맺고 있는 것을 확인했는데요. SingletonManager가 가진 속성 중에서 RouteConfigProviderManager가 RDS 처리를 담당하고 있습니다.&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;그렇다면 왜 SingletonManager에 의해서 해당 속성이 관리될까요? 위 그림을 살펴보면 HttpConnectionManager는 사용자의 Connection 별로 여러개가 생성됨을 볼 수 있습니다. 하지만 RDS 처리 또한 각각의 HttpConnectionManager를 통해 관리되어야한다면, RDS 처리를 위한 overhead 또한 증가하게되고 무엇보다 동일한 정보가 중복 관리되기 때문에 관리 용이성 또한 증가합니다. 따라서&amp;nbsp;전역적으로 하나의 인스턴스만을 생성함으로써 데이터를 한 곳에서 관리하고 모든 HttpConnectionManager가 이를 공유하도록 싱글톤 패턴이 적용되어있습니다.&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;size16&quot;&gt;config.cc&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;Utility::Singletons Utility::createSingletons(Server::Configuration::FactoryContext&amp;amp; context) {
  std::shared_ptr&amp;lt;Http::TlsCachingDateProviderImpl&amp;gt; date_provider =
      context.singletonManager().getTyped&amp;lt;Http::TlsCachingDateProviderImpl&amp;gt;(
          SINGLETON_MANAGER_REGISTERED_NAME(date_provider), [&amp;amp;context] {
            return std::make_shared&amp;lt;Http::TlsCachingDateProviderImpl&amp;gt;(
                context.mainThreadDispatcher(), context.threadLocal());
          });

  Router::RouteConfigProviderManagerSharedPtr route_config_provider_manager =
      context.singletonManager().getTyped&amp;lt;Router::RouteConfigProviderManager&amp;gt;(
          SINGLETON_MANAGER_REGISTERED_NAME(route_config_provider_manager), [&amp;amp;context] {
            return std::make_shared&amp;lt;Router::RouteConfigProviderManagerImpl&amp;gt;(context.admin());
          });

  Router::ScopedRoutesConfigProviderManagerSharedPtr scoped_routes_config_provider_manager =
      context.singletonManager().getTyped&amp;lt;Router::ScopedRoutesConfigProviderManager&amp;gt;(
          SINGLETON_MANAGER_REGISTERED_NAME(scoped_routes_config_provider_manager),
          [&amp;amp;context, route_config_provider_manager] {
            return std::make_shared&amp;lt;Router::ScopedRoutesConfigProviderManager&amp;gt;(
                context.admin(), *route_config_provider_manager);
          });

  auto http_tracer_manager = context.singletonManager().getTyped&amp;lt;Tracing::HttpTracerManagerImpl&amp;gt;(
      SINGLETON_MANAGER_REGISTERED_NAME(http_tracer_manager), [&amp;amp;context] {
        return std::make_shared&amp;lt;Tracing::HttpTracerManagerImpl&amp;gt;(
            std::make_unique&amp;lt;Tracing::TracerFactoryContextImpl&amp;gt;(
                context.getServerFactoryContext(), context.messageValidationVisitor()));
      });

  std::shared_ptr&amp;lt;Http::DownstreamFilterConfigProviderManager&amp;gt; filter_config_provider_manager =
      Http::FilterChainUtility::createSingletonDownstreamFilterConfigProviderManager(
          context.getServerFactoryContext());

  return {date_provider, route_config_provider_manager, scoped_routes_config_provider_manager,
          http_tracer_manager, filter_config_provider_manager};
}&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;코드를 살펴보면 위와 같이 가장 먼저 하는 것은 SingletonManager에 등록된 인스턴스 중에서&amp;nbsp; RouteConfigProviderManager, ScopedRoutesConfigProviderManager 와 더불어 다양한 Manager 등 다양한 Manager를 가져오는 작업을 선행합니다.&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;이때 중요한 것은 앞서 언급한 2가지 Manager이며, RouteConfigProviderManager는 route_config 정보를 기반으로 RDS 혹은 StaticRouteConfig를 처리하는 인스턴스를 생성하는 역할을 수행합니다. 반면 &lt;span&gt;ScopedRoutesConfigProviderManager는 Listener 설정에 scoped_routes 설정이 존재할 때 해당 scoped_routes를 처리하는 인스턴스를 생성하는 역할을 수행합니다.&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;config.cc&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;switch (config.route_specifier_case()) {
case envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager::
    RouteSpecifierCase::kRds:
case envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager::
    RouteSpecifierCase::kRouteConfig:
  route_config_provider_ = Router::RouteConfigProviderUtil::create(
      config, context_.getServerFactoryContext(), context_.messageValidationVisitor(),
      context_.initManager(), stats_prefix_, route_config_provider_manager_);
  break;
case envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager::
    RouteSpecifierCase::kScopedRoutes:
  scoped_routes_config_provider_ = Router::ScopedRoutesConfigProviderUtil::create(
      config, context_.getServerFactoryContext(), context_.initManager(), stats_prefix_,
      scoped_routes_config_provider_manager_);
  break;
case envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager::
    RouteSpecifierCase::ROUTE_SPECIFIER_NOT_SET:
  PANIC_DUE_TO_CORRUPT_ENUM;
}&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;HttpConnectionManager를 구성하는 단계에서 전달받은 두 속성은 이후 route 관련 정보를 생성하는데 사용됩니다. 다만 두 속성 모두가 생성되지는 않으며, 기존에 지정된 설정내역을 살펴보고 하나를 생성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;즉 위 코드와 같이 config에 지정된 route_specifier에 의해서 결정됩니다. 만약 route_config 설정이 지정되어있다면, 기존에 SingletonManager로부터 부여받은 route_config_provider_manager를 전달하여 route 처리를 수행할 수 있는 route_config_provider를 전달받습니다. 반면 scoped_route라면 scoped_route_config_provider_manager를 전달하여 scoped_route_config_provider를 전달받습니다.&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;본포스팅에서는 route_config만 지정되어있음을 가정하며, 따라서 위 코드에서는 route_config_provider_ 만이 생성되었음을 전제로 진행하겠습니다. 또한 route를 처리하는 방식이 static이 아닌 dynamic xDS를 활용한 동적 변경을 가정하겠습니다.&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;1822&quot; data-origin-height=&quot;1154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8joZe/btr7dL2AEO8/ByOHGBxk5fldjPKm3zolZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8joZe/btr7dL2AEO8/ByOHGBxk5fldjPKm3zolZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8joZe/btr7dL2AEO8/ByOHGBxk5fldjPKm3zolZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8joZe%2Fbtr7dL2AEO8%2FByOHGBxk5fldjPKm3zolZK%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;1822&quot; height=&quot;1154&quot; data-origin-width=&quot;1822&quot; data-origin-height=&quot;1154&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;route_config_provider가 생성된 이후 RDS를 매핑하는 과정을 살펴보면 위그림과 같습니다.&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;1. HttpConnectionManager에 매핑되어있는 route_config_provider를 기반으로 route config를 분석할 수 있는 인스턴스를 생성합니다. 이후 RouteConfigProviderManager에 존재하는 dynamic_route_config_providers_ 로부터 rds 메시지 값을 해시한 결과를 기반으로 dynamic_route_config_providers_ Map에 존재하는지 살펴보고 만약 존재한다면, 해당 provider를 반환합니다.&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;2. dynamic_route_config_providers_에 존재하지 않는다면, RDS 생성을 위해 Cluster Manager로부터 Subscription factory를 요청합니다.&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;3.&amp;nbsp; xDS에 전달받기 원하는 타입 및 수신 callback을 Multiplexer에 등록합니다.&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;4. 등록이 완료된 이후 Subscription을 반환합니다.&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;5. RdsRouteConfigProviderImpl 인스턴스를 생성하고 그 안에 subscription을 바인딩합니다. 또한 dynamic_route_config_providers_ Map에 해당 인스턴스를 삽입함으로써 이후 동일한 요청이 전달되면, 새로운 provider를 생성하지 않고 매핑된 값을 반환합니다.&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;route_config_provider_manager_impl.h&lt;/p&gt;
&lt;pre id=&quot;code_1680746424182&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;auto subscription = std::make_shared&amp;lt;RdsRouteConfigSubscription&amp;gt;(
    std::move(config_update), std::move(resource_decoder), rds.config_source(),
    rds.route_config_name(), manager_identifier, factory_context,
    stat_prefix + absl::AsciiStrToLower(getRdsName()) + &quot;.&quot;,
    absl::AsciiStrToUpper(getRdsName()), manager_);
auto provider = std::make_shared&amp;lt;RdsRouteConfigProviderImpl&amp;gt;(std::move(subscription),
                                                             factory_context);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6. 해당 provider와 subscription 정보를 반환합니다.&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;7. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;RdsRouteConfigProviderImpl&lt;span&gt; 내부에서는 xDS API가 변경이 생겼을 때, 내부에서 ThreadLocalStorage에 존재하는 ThreadLocalConfig의 내용을 변경함으로써, 쓰레드 전체에 동일한 데이터를 공유할 수 있도록 유지합니다.&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;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;위와 같은 7가지 단계를 통해서 HttpConnectionManager가 생성될 때 Singleton Manager를 통해 전역적으로 RDS를 관리하는 하나의 provider를 공급받고, ThreadLocalStorage를 활용해서 모든 쓰레드에서 동일한 데이터에 대한 접근이 가능하도록 공유합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Http filter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpConnectionManager는 Http 처리를 담당합니다. 이때 해당 컴포넌트 내부에는 http 처리를 위한 무수한 filter가 존재합니다. 참고로&amp;nbsp;&amp;nbsp;&lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/http_filters&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;envoy 공식 문서&lt;/a&gt;&lt;/span&gt;&lt;/u&gt;를 살펴보면 지정할 수 있는 filter가 여러가지 있음을 확인할 수 있습니다. 그리고 그 중에는 Routing을 담당하는 Router Filter 또한 존재합니다.&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;따라서 사용자의 요청이 전달되면 Network Filter Chain이 수행되면서 HttpConnectionManager가 실행되고 그리고 그 안에서 다시 HttpConnectionManager가 보유한 Http Filter들이 수행되면서 사용자의 요청이 처리됩니다.&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;size16&quot;&gt;config.cc&lt;/p&gt;
&lt;pre class=&quot;arduino&quot; style=&quot;background-color: #2b2b2b; color: #a9b7c6;&quot;&gt;&lt;code&gt;Http::FilterChainHelper&amp;lt;Server::Configuration::FactoryContext,
                        Server::Configuration::NamedHttpFilterConfigFactory&amp;gt;
    helper(filter_config_provider_manager_, context_.getServerFactoryContext(), context_,
           stats_prefix_);
helper.processFilters(config.http_filters(), &quot;http&quot;, &quot;http&quot;, filter_factories_);&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;HttpConnectionManager를 구성하는 config 속성을 살펴보면, 해당 Config를 생성할 때 http_filters를 위한 factory를 생성하는 것을 볼 수 있습니다. 이때 이미 사용자가 지정하거나 LDS에 의해서 갱신된 Listener의 Config 정보를 살펴보면, 지정된 http filter 목록을 확인할 수 있습니다.&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;2014&quot; data-origin-height=&quot;729&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVUFrp/btr7cekghWT/ABqB1JH9BrUROOUI2dKyaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVUFrp/btr7cekghWT/ABqB1JH9BrUROOUI2dKyaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVUFrp/btr7cekghWT/ABqB1JH9BrUROOUI2dKyaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVUFrp%2Fbtr7cekghWT%2FABqB1JH9BrUROOUI2dKyaK%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;2014&quot; height=&quot;729&quot; data-origin-width=&quot;2014&quot; data-origin-height=&quot;729&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;따라서 FilterChainHelper를 통해서 Config 정보를 토대로 filter_factories라는 Filter를 생성하는 Factory 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;FilterChainHelper는 해당 요청을 전달받으면, DependencyManager를 통해서 전달된 Filter의 우선순위를 고려하여 정상적으로 입력이 되었는지를 검사합니다. 그리고 Filter간의 Dependency에 문제가 없게 Config가 전달되었다면, 이를 HttpConnectionManager가 보유한 filter_factories에 Filter 생성을 위한 Callback Factory 메소드를 매핑시킵니다.&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;위 과정을 거쳐 생성된 filter_factories_는 향후 사용자가 Http 요청을 전달하기 위해 Stream을 생성할 때 내부에 존재하는 filter_manager_로 해당 filter_factories_를 전달시켜 해당 filter_manager_가 http filter chain을 생성하고 수행할 수 있도록 처리를 위임합니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Codec&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에 살펴볼 것은 codec입니다. HttpConnectionManager에서 codec은 사용자의 요청 정보를 분석하는 역할을 수행합니다. 사용자가 전달한 raw데이터를 지정된 프로토콜 형태로 파싱하고 분석하여 처리하는 과정에서 사용됩니다.&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;1536&quot; data-origin-height=&quot;729&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baK5Xk/btr7dWqc81I/IH3w2zkORnSB1qts5YrFHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baK5Xk/btr7dWqc81I/IH3w2zkORnSB1qts5YrFHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baK5Xk/btr7dWqc81I/IH3w2zkORnSB1qts5YrFHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaK5Xk%2Fbtr7dWqc81I%2FIH3w2zkORnSB1qts5YrFHK%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;1536&quot; height=&quot;729&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;729&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;그림을 살펴보면, codec_은 HttpConnectionManager 내부에 있는 config에 의해서 생성할 수 있습니다. 이때 사용자의 Http 요청은 Http 1.1, Http 2.0 혹은 Http 3.0(Quic) 형태일 수 있습니다. 위 세가지 프로토콜 모두 HttpConnectionManager가 처리하는데요. 각각의 처리방식과 포맷이 다르기 때문에 사용자의 요청이 전달되었을 때, 가정 먼저 수행하는 일은 사용자의 요청이 어떤 프로토콜 형태인지를 파악하는 것입니다.&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;config.cc&lt;/p&gt;
&lt;pre class=&quot;php&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Http::ServerConnectionPtr
HttpConnectionManagerConfig::createCodec(Network::Connection&amp;amp; connection,
                                         const Buffer::Instance&amp;amp; data,
                                         Http::ServerConnectionCallbacks&amp;amp; callbacks) {
  switch (codec_type_) {
  case CodecType::HTTP1:
    return std::make_unique&amp;lt;Http::Http1::ServerConnectionImpl&amp;gt;(
        connection, Http::Http1::CodecStats::atomicGet(http1_codec_stats_, context_.scope()),
        callbacks, http1_settings_, maxRequestHeadersKb(), maxRequestHeadersCount(),
        headersWithUnderscoresAction());
  case CodecType::HTTP2:
    return std::make_unique&amp;lt;Http::Http2::ServerConnectionImpl&amp;gt;(
        connection, callbacks,
        Http::Http2::CodecStats::atomicGet(http2_codec_stats_, context_.scope()),
        context_.api().randomGenerator(), http2_options_, maxRequestHeadersKb(),
        maxRequestHeadersCount(), headersWithUnderscoresAction());
  case CodecType::HTTP3:
#ifdef ENVOY_ENABLE_QUIC
    return std::make_unique&amp;lt;Quic::QuicHttpServerConnectionImpl&amp;gt;(
        dynamic_cast&amp;lt;Quic::EnvoyQuicServerSession&amp;amp;&amp;gt;(connection), callbacks,
        Http::Http3::CodecStats::atomicGet(http3_codec_stats_, context_.scope()), http3_options_,
        maxRequestHeadersKb(), maxRequestHeadersCount(), headersWithUnderscoresAction());
#else
    // Should be blocked by configuration checking at an earlier point.
    PANIC(&quot;unexpected&quot;);
#endif
  case CodecType::AUTO:
    return Http::ConnectionManagerUtility::autoCreateCodec(
        connection, data, callbacks, context_.scope(), context_.api().randomGenerator(),
        http1_codec_stats_, http2_codec_stats_, http1_settings_, http2_options_,
        maxRequestHeadersKb(), maxRequestHeadersCount(), headersWithUnderscoresAction());
  }
  PANIC_DUE_TO_CORRUPT_ENUM;
}&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;이를 Config에서 분석한 다음 Http 1.1일 경우에는 Http1::ServerConnectionImpl, Http 2.0일 경우에는 Http2:ServerConnectionImpl Http 3.0일 경우에는 QuicHttpServerConnectionImpl을 반환하여 HttpConnectionManager 내부에 존재하는 codec_에 매핑하는 작업을 수행합니다.&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;codec.h&lt;/p&gt;
&lt;pre id=&quot;code_1680251008556&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Connection {
public:
  virtual ~Connection() = default;

  /**
   * Dispatch incoming connection data.
   * @param data supplies the data to dispatch. The codec will drain as many bytes as it processes.
   * @return Status indicating the status of the codec. Holds any errors encountered while
   * processing the incoming data.
   */
  virtual Status dispatch(Buffer::Instance&amp;amp; data) PURE;

  /**
   * Indicate &quot;go away&quot; to the remote. No new streams can be created beyond this point.
   */
  virtual void goAway() PURE;

  /**
   * @return the protocol backing the connection. This can change if for example an HTTP/1.1
   *         connection gets an HTTP/1.0 request on it.
   */
  virtual Protocol protocol() PURE;

  /**
   * Indicate a &quot;shutdown notice&quot; to the remote. This is a hint that the remote should not send
   * any new streams, but if streams do arrive that will not be reset.
   */
  virtual void shutdownNotice() PURE;

  /**
   * @return bool whether the codec has data that it wants to write but cannot due to protocol
   *              reasons (e.g, needing window updates).
   */
  virtual bool wantsToWrite() PURE;

  /**
   * Called when the underlying Network::Connection goes over its high watermark.
   */
  virtual void onUnderlyingConnectionAboveWriteBufferHighWatermark() PURE;

  /**
   * Called when the underlying Network::Connection goes from over its high watermark to under its
   * low watermark.
   */
  virtual void onUnderlyingConnectionBelowWriteBufferLowWatermark() PURE;
};&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;프로토콜마다 처리 방법이 다양하지만, 모든 ServerConnectionImpl은 위와 같은 Interface 스펙을 준수합니다. 따라서 HttpConnectionManager에서는 위 interface에 정의된 메소드를 호출하여 처리를 위임할 수 있습니다. 다만 본 포스팅에서는 Http 1.1을 기준으로 처리 과정을 분석하기 때문에 Http1::ServerConnectionImpl이 반환되었다고 가정하겠습니다.&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;그렇다면 Http 1.1을 처리하기 위한 ServerConnectionImpl은 어떤 역할을 수행할까요? 해당 구조에 대해서 조금 더 자세히 살펴보겠습니다.&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;1837&quot; data-origin-height=&quot;929&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7SwxC/btr7ebgpjwU/5NobdYR4vGytGB8zZv9eS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7SwxC/btr7ebgpjwU/5NobdYR4vGytGB8zZv9eS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7SwxC/btr7ebgpjwU/5NobdYR4vGytGB8zZv9eS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7SwxC%2Fbtr7ebgpjwU%2F5NobdYR4vGytGB8zZv9eS1%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;1837&quot; height=&quot;929&quot; data-origin-width=&quot;1837&quot; data-origin-height=&quot;929&quot;/&gt;&lt;/span&gt;&lt;/figure&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;ServerConnectionImpl을 살펴보면, 여러 속성이 있지만 그 중 가장 중요한 속성은 위 2가지 입니다. 먼저 Parser_에 대해서 알아보겠습니다.&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;style3&quot; /&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;5-1. Parser&lt;/h3&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;parser_의 역할은 Client로 부터 전달된 요청 내역을 Http 1.1 스펙에 맞게 분석하여 envoy가 원하는 형태로 데이터를 구성하는 작업을 담당합니다. 이때 envoy 내부에는 해당 내역을 처리하는 Parser가 2개가 존재합니다.&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;codec_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;ConnectionImpl::ConnectionImpl(Network::Connection&amp;amp; connection, CodecStats&amp;amp; stats,
                               const Http1Settings&amp;amp; settings, MessageType type,
                               uint32_t max_headers_kb, const uint32_t max_headers_count)
    : connection_(connection), stats_(stats), codec_settings_(settings),
      encode_only_header_key_formatter_(encodeOnlyFormatterFromSettings(settings)),
      processing_trailers_(false), handling_upgrade_(false), reset_stream_called_(false),
      deferred_end_stream_headers_(false), dispatching_(false), max_headers_kb_(max_headers_kb),
      max_headers_count_(max_headers_count) {
  if (Runtime::runtimeFeatureEnabled(&quot;envoy.reloadable_features.http1_use_balsa_parser&quot;)) {
    parser_ = std::make_unique&amp;lt;BalsaParser&amp;gt;(type, this, max_headers_kb_ * 1024, enableTrailers());
  } else {
    parser_ = std::make_unique&amp;lt;LegacyHttpParserImpl&amp;gt;(type, this);
  }
}&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;첫 번째는 LegacyHttpParserImpl로써 envoy가 기존부터 제공해온 Parser입니다. 이후 해당 Parser의 성능 개선을 위해 추가로 개발한 것이 BalsaParser입니다. 다만 BalsaParser는 아직 완전하지는 않으며, envoy에서 해당 Parser를 사용하려면&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;'&lt;span style=&quot;color: #a6bc00;&quot;&gt;envoy.reloadable_features.http1_use_balsa_parser&lt;/span&gt;' 옵션을 활성화했을 경우 사용할 수 있습니다. 참고로 본 포스팅에서는 기본으로 사용되는 LegacyHttpParserImpl에 대해서 다루어보겠습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1882&quot; data-origin-height=&quot;950&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQQJCj/btr7dxqKb65/AxKnqzxOI3m0D4vmkx7VhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQQJCj/btr7dxqKb65/AxKnqzxOI3m0D4vmkx7VhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQQJCj/btr7dxqKb65/AxKnqzxOI3m0D4vmkx7VhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQQJCj%2Fbtr7dxqKb65%2FAxKnqzxOI3m0D4vmkx7VhK%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;1882&quot; height=&quot;950&quot; data-origin-width=&quot;1882&quot; data-origin-height=&quot;950&quot;/&gt;&lt;/span&gt;&lt;/figure&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;parser_가 생성되면, 추후 parser_에게 사용자 요청을 처리하도록 위임할 것입니다. 이때 Parser가 처리 중간 중간마다 특정 event 즉 header 필드명이 무엇인지 하나씩 파악했거나, header 값을 분석했을 때 이를 요청자인 ServerConnection 에게 알려줘야 해당 데이터들을 전달받아 envoy가 원하는 형태로 데이터를 가공하거나 그 이후 처리해야할 비즈니스 로직을 수행할 수 있을 것입니다.&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;이때 ServerConnectionImpl은 기본적으로 ParserCallbacks Virtual Class를 상속받았으며, 그 안에 정의된 메소드는 해당 메소드가 호출될 때 수행해야할 비즈니스 로직이 구현되어있습니다. 구현해야할 메소드는 위와 같이 총 10개이며, 각각의 의미는 다음과 같습니다.&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;메소드명&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;onMessageBegin&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Request/Response가 시작될 때 호출되는 callback으로 반환 값으로 성공/실패 메시지가 반환됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;onUrl&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;URL data를 Parser가 분석했을 때 호출되는 callback으로 반환 값으로 성공/실패 메시지가 반환됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;onStatus&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Status data를 Parser가 분석헀을 때 호출되는 callback으로 반환 값으로 성공/실패 메시지가 반환됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;onHeaderField&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;header의 field 명을 수신받았을 때 호출되는 callback으로 반환 값으로 성공/실패 메시지가 반환됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;onHeaderValue&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;header의 value 값을 수신받았을 때 호출되는 callback으로 반환 값으로 성공/실패 메시지가 반환됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;onHeaderComplete&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;header 분석이 완료되었을 때 호출되는 callback&lt;span style=&quot;background-color: #f9f9f9;&quot;&gt;으로 반환 값으로 성공/실패 메시지가 반환됨&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;bufferBody&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;body data를 분석했을 때 호출되는 callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;onMessageComplete&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Parser가 HTTP 데이터를 모두 분석 완료했을 때 호출되는 callback으로 반환 값으로 성공/실패 메시지가 반환됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;onChunkHeader&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;chunk header를 받았을 때 호출되는 callback&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;이후 Parser에서는 처리 도중 중간 중간에 ParserCallbacks에 정의된 메소드를 호출함으로써, ServerConnectionImpl에게 Parsing 결과를 중간 중간 callback 하도록 구현되었습니다.&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;legacy_parser_impl.cc&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;Impl(http_parser_type type, void* data) : Impl(type) {
  parser_.data = data;
  settings_ = {
      [](http_parser* parser) -&amp;gt; int {
        auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
        return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onMessageBegin());
      },
      [](http_parser* parser, const char* at, size_t length) -&amp;gt; int {
        auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
        return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onUrl(at, length));
      },
      [](http_parser* parser, const char* at, size_t length) -&amp;gt; int {
        auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
        return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onStatus(at, length));
      },
      [](http_parser* parser, const char* at, size_t length) -&amp;gt; int {
        auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
        return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onHeaderField(at, length));
      },
      [](http_parser* parser, const char* at, size_t length) -&amp;gt; int {
        auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
        return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onHeaderValue(at, length));
      },
      [](http_parser* parser) -&amp;gt; int {
        auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
        return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onHeadersComplete());
      },
      [](http_parser* parser, const char* at, size_t length) -&amp;gt; int {
        static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data)-&amp;gt;bufferBody(at, length);
        return 0;
      },
      [](http_parser* parser) -&amp;gt; int {
        auto* conn_impl = static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data);
        return static_cast&amp;lt;int&amp;gt;(conn_impl-&amp;gt;onMessageComplete());
      },
      [](http_parser* parser) -&amp;gt; int {
        // A 0-byte chunk header is used to signal the end of the chunked body.
        // When this function is called, http-parser holds the size of the chunk in
        // parser-&amp;gt;content_length. See
        // https://github.com/nodejs/http-parser/blob/v2.9.3/http_parser.h#L336
        const bool is_final_chunk = (parser-&amp;gt;content_length == 0);
        static_cast&amp;lt;ParserCallbacks*&amp;gt;(parser-&amp;gt;data)-&amp;gt;onChunkHeader(is_final_chunk);
        return 0;
      },
      nullptr // on_chunk_complete
  };
}&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;즉 위 코드와 같이 LegacyHttpParserImpl 내부에는 Parsing 중간 중간 처리 결과를 반환할 수 있도록, settings_에 함수를 매핑했는데, 이때 ParserCallbacks에 정의된 규약에 따른 메소드를 호출하여 결과를 전달하는 것을 볼 수 있습니다.&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;style3&quot; /&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;5-2. Active Request&lt;/h3&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;이번에는 Http1::ServerConnectionImpl이 보유하고 있는 속성 중 두번째인 active_request_ 에 대해서 알아보겠습니다.&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;codec_impl.h&lt;/p&gt;
&lt;pre id=&quot;code_1680251093195&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  struct ActiveRequest : public Event::DeferredDeletable {
    ActiveRequest(ServerConnectionImpl&amp;amp; connection, StreamInfo::BytesMeterSharedPtr&amp;amp;&amp;amp; bytes_meter)
        : response_encoder_(connection, std::move(bytes_meter),
                            connection.codec_settings_.stream_error_on_invalid_http_message_) {}
    ~ActiveRequest() override = default;

    void dumpState(std::ostream&amp;amp; os, int indent_level) const;
    HeaderString request_url_;
    RequestDecoder* request_decoder_{};
    ResponseEncoderImpl response_encoder_;
    bool remote_complete_{};
  };&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;ActiveRequest는 위와 같이 Parser에 의해서 데이터를 Parsing 하는 과정에서 RequestDecoder, url, ResponseEncoder 등을 가지고 있는 구조체입니다. 해당 구조체를 통해서 Parsing 단계에서 connection 객체에 대한 작업 요청 및 url, encoder 지정 및 수행등을 담당합니다.&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;해당 자료구조가 사용되는 흐름을 보려면 사용자 요청을 처리하는 전단계를 살펴봐야하는데요. 이번 포스팅은 HttpConnectionManager의 특징에 대해서 살펴보기 때문에 ActiveRequest의 존재에 대해서만 이번 포스팅에서는 언급하고 해당 자료구조의 쓰임새는 다음 포스팅에서 보다 자세히 다루어보겠습니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Stream&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 HttpConnectionManager가 관리하는 Stream 목록에 대해서 살펴보겠습니다. Stream은 Http 요청을 전달하는 하나의 흐름으로써, 하나의 Connection을 맺은 상태로 Http 요청을 전달하기 위해 여러 Stream을 생성할 수 있습니다. 따라서 이러한 개별 Stream들의 그룹을 관리하기 위해서 HttpConnectionManager 내부에는 streams_라는 List를 보유하고 있습니다.&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;1478&quot; data-origin-height=&quot;847&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ATHuw/btr8jPi6oSQ/kj3Mshlqt2HweKA8IEeLZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ATHuw/btr8jPi6oSQ/kj3Mshlqt2HweKA8IEeLZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ATHuw/btr8jPi6oSQ/kj3Mshlqt2HweKA8IEeLZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FATHuw%2Fbtr8jPi6oSQ%2Fkj3Mshlqt2HweKA8IEeLZK%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;640&quot; height=&quot;367&quot; data-origin-width=&quot;1478&quot; data-origin-height=&quot;847&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;streams_ List는 ActiveStream을 포함하고 있는데, 해당 Stream 내부에는 Http 요청에 필요한 필수적인 항목들이 포함되어있습니다. 그렇다면 ActiveStream은 언제 생성될까요?&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;2072&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WwW7S/btr8neI8HPN/GwYFUQYiqzcHmormFWdFzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WwW7S/btr8neI8HPN/GwYFUQYiqzcHmormFWdFzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WwW7S/btr8neI8HPN/GwYFUQYiqzcHmormFWdFzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWwW7S%2Fbtr8neI8HPN%2FGwYFUQYiqzcHmormFWdFzk%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;2072&quot; height=&quot;966&quot; data-origin-width=&quot;2072&quot; data-origin-height=&quot;966&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;codec_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1680746261199&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Status ServerConnectionImpl::onMessageBeginBase() {
  if (!resetStreamCalled()) {
    ASSERT(active_request_ == nullptr);
    active_request_ = std::make_unique&amp;lt;ActiveRequest&amp;gt;(*this, std::move(bytes_meter_before_stream_));
    if (resetStreamCalled()) {
      return codecClientError(&quot;cannot create new streams after calling reset&quot;);
    }
    active_request_-&amp;gt;request_decoder_ = &amp;amp;callbacks_.newStream(active_request_-&amp;gt;response_encoder_);

    // Check for pipelined request flood as we prepare to accept a new request.
    // Parse errors that happen prior to onMessageBegin result in stream termination, it is not
    // possible to overflow output buffers with early parse errors.
    RETURN_IF_ERROR(doFloodProtectionChecks());
  }
  return okStatus();
}&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;과정을 간략하게 살펴보면, 이전에 Parser에 대해서 살펴봤을 때, Client가 데이터 처리를 요청하면 이를 분석해서 특정 Event마다 통지한다고 설명했습니다. 이때 onMessageBegin 이벤트가 발생하면 ServerConnection 에서는 ActiveRequest를 먼저 생성하고 ActiveRequest 하위에 request_decoder_ 속성에 새로운 Stream을 만듭니다. 이때 만들어지는 Stream이 ActiveStream입니다.&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;이후 생성된 ActiveStream은 위 과정에서 표현되지는 않았지만 Parsing 과정이 진행되면서 지속적으로 참조되고 Parsing이 완료되는 시점에서 Stream 내부에 매핑된 정보에 의하여 Http 전송이 이루어지게됩니다.&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;1200&quot; data-origin-height=&quot;833&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGD5H3/btr8gXCcjph/E59NwKvX1esAvRpuv6hF51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGD5H3/btr8gXCcjph/E59NwKvX1esAvRpuv6hF51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGD5H3/btr8gXCcjph/E59NwKvX1esAvRpuv6hF51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGD5H3%2Fbtr8gXCcjph%2FE59NwKvX1esAvRpuv6hF51%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;640&quot; height=&quot;444&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;833&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;이번에는 ActiveStream의 속성에 대해서 살펴보겠습니다. stream_id는 Stream 마다 생성되는 id로써, ActiveStream이 생성하는 당시에 임의의 값으로 지정됩니다.&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;filter_manager_ 는 이전에 Http filter에 대해서 잠깐 살펴볼 때 등장한 프로퍼티명으로써, ActiveStream 내에 Http filter 생성 및 처리를 위임하는데 관여하는 프로퍼티입니다. 사용자 요청을 처리하는 과정에서 Http filter 처리가 필요한 순간에 해당 인스턴스가 사용됩니다.&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;그 다음 살펴볼 것은 timer입니다. ActiveStream 내부에는 다양한 Timer가 존재하는데, 해당 timer의 역할은 timer가 지정된 시간내에 요구하는 조건이 충족되지 않으면, 해당 연결을 해제하는 역할을 수행합니다. 이때 각각의 Timer는 Dispatcher 로부터 Timer를 생성받아 지정된 시간내에 조건이 충족되면 Timer를 Reset 하여 다시 지정된 시간만큼을 대기하며, 시간이 초괴되면 연결을 해제합니다. 개별 Timer의 역할을 살펴보면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;타이머&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;기능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;stream_idle_timer_&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;해당 Stream이 연결되고 어떠한 활동도 일어나지 않았을 때, 지정된 idle 시간을 초과하면 Stream 해제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;request_timer_&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Stream이 생성되고 난 이후 Request를 시작하고 응답이 올 때까지 대기시간을 의미하며, 지정된 시간 초과하면 Stream 해제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;request_header_timer_&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Downstream에서 header를 전송하고 나서 이에 대한 응답이 올 때까지의 대기시간을 의미하며, 지정된 시간 초과하면 Stream 해제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;max_stream_duration_timer_&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Stream이 생성되고 종료될 때까지 지속할 수 있는 시간을 의미하며, 지정된 시간을 초과하면 Stream 해제&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;상기 Timer와 관련해서는 &lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;envoy 공식 문서&lt;/a&gt;&lt;/b&gt;&lt;/span&gt;&lt;/u&gt;에서 확인할 수 있으며, 적정한 값 설정을 통해서 Stream이 생성되고 무한정 대기하지 않도록 처리할 수 있습니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Timer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HttpConnectionManager에서 마지막으로 살펴볼 것은 내부 프로퍼티에 존재하는 Timer입니다. 방금전에 개별 Stream 내부에서 여러 Timer 들을 살펴봤었는데, HttpConnectionManager 또한 Timer를 보유하고 있습니다.&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;1007&quot; data-origin-height=&quot;729&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AiWAn/btr8azaNKlu/pIajafZHYYeHQIhGK0SPK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AiWAn/btr8azaNKlu/pIajafZHYYeHQIhGK0SPK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AiWAn/btr8azaNKlu/pIajafZHYYeHQIhGK0SPK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAiWAn%2Fbtr8azaNKlu%2FpIajafZHYYeHQIhGK0SPK0%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;640&quot; height=&quot;463&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;729&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;HttpConnectionManager가 보유한 주요 Timer는 위와같이 2개입니다. 이는 HttpConnectionManager가 생성하는 단계에서 Dispatcher에게 요청하여 Timer를 생성합니다.&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;connection_idle_timer_의 경우는 HttpConnectionManager가 생성된 이후 어떠한 Stream이 생성되지 않았을 때, idle 시간을 얼마나 부여할지를 측정하는 Timer입니다. 따라서 해당 시간 동안 Stream 연결이 이루어지지 않는다면, 연결을 해제합니다.&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;반면 connection_duration_timer_는 HttpConnectionManager가 생성되고 모든 처리가 완료될 때까지 즉 사용자의 연결 요청을 완수하는데 걸리는 데드라인을 측정하는 Timer입니다. 따라서 해당 시간 동안 모든 요청이 완료되지 않는다면, 연결을 해제합니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅을 통해서 HttpConnectionManager가 보유하고 있는 속성에 대해서 알아봤습니다. 다만 이번 포스팅만으로는 HttpConnectionManager가 보유하고 있는 컴포넌트가 어떻게 상호작용하여 사용자의 요청을 처리하는지 완벽하게 이해하기는 힘듭니다. 이 부분은 이 다음 포스팅에서 진행되는 Envoy 프록시 연결 과정을 관찰하면서 조금 더 자세하게 살펴보겠습니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <category>DevOps</category>
      <category>Envoy</category>
      <category>envoy architecture</category>
      <category>envoy 아키텍처</category>
      <category>http connection manager</category>
      <category>Istio</category>
      <category>istio architecture</category>
      <category>istio 아키텍처</category>
      <category>Service Mesh</category>
      <category>서비스메시</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/221</guid>
      <comments>https://cla9.tistory.com/221#entry221comment</comments>
      <pubDate>Wed, 24 May 2023 11:50:44 +0900</pubDate>
    </item>
    <item>
      <title>5. [envoy-internals] Listener Manager</title>
      <link>https://cla9.tistory.com/218</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅은 지난번에 이어서 Envoy의 주요 컴포넌트 중 하나인 Listener와 Listener를 관리하는 Listener Manager에 대해서 살펴보고자 합니다.&amp;nbsp;&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;1034&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/epjKJ2/btr5LCsaHMB/6Q4RkRLLKTCbYoRBndvwl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/epjKJ2/btr5LCsaHMB/6Q4RkRLLKTCbYoRBndvwl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/epjKJ2/btr5LCsaHMB/6Q4RkRLLKTCbYoRBndvwl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FepjKJ2%2Fbtr5LCsaHMB%2F6Q4RkRLLKTCbYoRBndvwl1%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;640&quot; height=&quot;237&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;383&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;style1&quot; /&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;2. Listener Manager 개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Listener Manager는 단어에서 유추할 수 있듯이 Listener를 관리하는 역할을 수행합니다. 즉 Listener를 생성하고 관리합니다. 두 번째 역할을 LDS를 생성하고 관리하는 역할을 겸합니다. 이번 포스팅에서는 이에 대해서 하나씩 살펴보겠습니다. 먼저 살펴볼 것은 Listener 생성 및 관리 기능입니다.&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;Listener Manager는 Listener를 생성 후 관리한다고 설명했습니다. 여기서 꼭 기억해야할 특징은 Listener Manager가 생성한 Listener는 단일 쓰레드 기반으로 동작하지 않는다는 점입니다. 그렇다면 Envoy는 왜 Listener를 단일 쓰레드로 동작하지 않고 멀티 쓰레드로 동작시켰을까요? 이를 이해하기 위해서 사례를 통해 먼저 사용자의 요청을 처리하는 쓰레드가 하나일 경우 야기되는 문제점을 살펴보겠습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;351&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDnMsU/btr5DTO0Jxc/rd84ulnklnXbNx58VFUGTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDnMsU/btr5DTO0Jxc/rd84ulnklnXbNx58VFUGTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDnMsU/btr5DTO0Jxc/rd84ulnklnXbNx58VFUGTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDnMsU%2Fbtr5DTO0Jxc%2Frd84ulnklnXbNx58VFUGTk%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;1089&quot; height=&quot;351&quot; data-origin-width=&quot;1089&quot; data-origin-height=&quot;351&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;envoy에 여러 API 요청이 들어오게되면, 그 중 하나가 쓰레드를 선점하게되고 해당 요청을 처리할 것입니다. 이때 만약 다른 API 요청이 전달된다면 어떻게 될까요? 쓰레드를 선점하고 있다면, 해당 쓰레드가 처리하는 작업 요청이 끝날 때까지 다른 요청은 대기해야할 것입니다. 따라서 이 경우 병렬성이 매우 떨어지게 됩니다. 특히나 다른 API 요청을 처리하는 Listener의 경우 미리 선점한 Listener의 작업에 의해 해당 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;따라서 envoy에서는 API 요청을 처리하는 쓰레드를 하나가 아니라 여러개를 구성하도록 설계되었습니다. 또한 생성된 Worker 쓰레드는 모든 Listener와 연결 되어있도록 구성되었습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RR4Qm/btr5vuJ3h9g/8mta8qppMiVLcl4f7KKhzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RR4Qm/btr5vuJ3h9g/8mta8qppMiVLcl4f7KKhzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RR4Qm/btr5vuJ3h9g/8mta8qppMiVLcl4f7KKhzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRR4Qm%2Fbtr5vuJ3h9g%2F8mta8qppMiVLcl4f7KKhzK%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;1298&quot; height=&quot;686&quot; data-origin-width=&quot;1298&quot; data-origin-height=&quot;686&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;envoy는 기동 당시 --concurrency 인자를 통해 생성할 Worker 쓰레드의 개수를 지정할 수 있습니다. 만약 위 그림과 같이 3으로 지정하였다면, 내부적으로는 3개의 Worker 쓰레드를 Listener Manager가 생성합니다. 또한 만약 Config에 지정된 Listener가 2개라면 위와 같이 2개의 Listener가 생성됩니다.&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;Listener와 Worker 쓰레드가 모두 생성되고나면, 모든 Worker 쓰레드는 모든 Listener를 Listen 하도록 합니다. 그렇다면 모든 Worker 쓰레드가 모든 Listener를 Listen 하는 것은 어떤 것을 의미할까요?&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;802&quot; data-origin-height=&quot;369&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Sa8W5/btr5BUHFJxR/z43TLQ8vpjnaHIE2FBcDcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Sa8W5/btr5BUHFJxR/z43TLQ8vpjnaHIE2FBcDcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Sa8W5/btr5BUHFJxR/z43TLQ8vpjnaHIE2FBcDcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSa8W5%2Fbtr5BUHFJxR%2Fz43TLQ8vpjnaHIE2FBcDcK%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;640&quot; height=&quot;294&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;369&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;만약 특정 Listener 별로 Worker 쓰레드를 할당할 수 있다면, 위와 같이 부하가 집중되는 Listener에 쓰레드를 많이 부여할 수 있습니다. 가령 위 그림과 같이 Listener0에 명시적으로 더 많은 쓰레드를 할당함으로써 Listener0 요청에 빠른 처리가 가능합니다.&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;하지만 만약 위와 같이 지정한 상황에서 갑자기 Listener 1쪽에 트래픽이 몰릴 경우에는 해당 Listener에 지정된 쓰레드는 하나이기 때문에 탄력적으로 대응할 수 없습니다.&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;따라서 envoy에서는 Active Listener 별로 영역을 정해서 Worker 쓰레드를 샤딩하지 않도록 구성되어있습니다. 즉 사용자 Connection이 요청되었을 때, 모든 Worker 쓰레드가 모든 Listener를 Listen하고 있기 때문에 어떠한 Worker 쓰레드에 사용자 Connection을 할당하더라도 문제가 없습니다. 또한 어떤 Worker 쓰레드에 사용자 요청을 할당하는 지에 대한 결정은 Kernel이 Socket을 Accept한 다음 결정합니다. 결국 Worker 쓰레드에 대한 Connection 할당 권한은 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;Connection이 Accept되어 Worker 쓰레드에 Socket이 배정되면, 해당 Socket은 이후에 Worker 쓰레드를 벗어나지 않습니다. 따라서 이후 제공되는 서비스는 최초 연결 시점에 할당된 Worker 쓰레드에서 이루어집니다.&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;지금까지 Listener Manager에서 생성되는 Worker 쓰레드에 대해서 살펴봤습니다. 그렇다면 Worker 쓰레드 생성 외에 Listener Manager는 어떤 역할을 하고 있을까요? Listener Manager가 담당하는 역할에 대해서 살펴보겠습니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Listener 생성&lt;/h2&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;900&quot; data-origin-height=&quot;801&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lfNwU/btr5AYcLQ46/orK3nSxUkKON2tNxsb7zi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lfNwU/btr5AYcLQ46/orK3nSxUkKON2tNxsb7zi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lfNwU/btr5AYcLQ46/orK3nSxUkKON2tNxsb7zi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlfNwU%2Fbtr5AYcLQ46%2ForK3nSxUkKON2tNxsb7zi1%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;900&quot; height=&quot;801&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;801&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;Listener Manager의 첫 번째 역할은 Listener 생성입니다. config 파일에 위치한 static_resources 정보에 포함된 Listener 정보를 살펴보고 그에따른 Listener를 생성합니다.&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;configuration_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679626176177&quot; class=&quot;reasonml&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void MainImpl::initialize(const envoy::config::bootstrap::v3::Bootstrap&amp;amp; bootstrap,
                          Instance&amp;amp; server,
                          Upstream::ClusterManagerFactory&amp;amp; cluster_manager_factory) {
  ...(중략)...

  const auto&amp;amp; listeners = bootstrap.static_resources().listeners();
  ENVOY_LOG(info, &quot;loading {} listener(s)&quot;, listeners.size());
  for (ssize_t i = 0; i &amp;lt; listeners.size(); i++) {
    ENVOY_LOG(debug, &quot;listener #{}:&quot;, i);
    server.listenerManager().addOrUpdateListener(listeners[i], &quot;&quot;, false);
  }

  ...(중략)...
}&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;이는 위 코드와 같이 Config 내용을 파싱한 이후 담겨있는 bootstrap 구조에서 static listener 정보를 추출한 결과를 토대로 Listener Manager에게 Listener를 생성하라고 요구하는 것을 볼 수 있습니다.&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;이때 생성되는 Listener는 어떤 과정을 거치며 내부적으로 어떤 속성을 가지고 있을까요?&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;1899&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciZkYz/btr5C4DhzwH/TFcTTzADTmiYkKnqorNvBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciZkYz/btr5C4DhzwH/TFcTTzADTmiYkKnqorNvBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciZkYz/btr5C4DhzwH/TFcTTzADTmiYkKnqorNvBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciZkYz%2Fbtr5C4DhzwH%2FTFcTTzADTmiYkKnqorNvBk%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;1899&quot; height=&quot;558&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;558&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;Envoy 관련 첫 포스팅에서 Client 요청이 전달되면, 위 그림과 같은 트래픽 흐름이 전달된다고 설명했습니다. 이때 Listener와 연결된 Listener Filters와 Network Filters는 Listener가 관리합니다. 따라서 Listener의 역할 중 하나는 Listener Filters와 Filter Chains(Network Filters)를 생성하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679626176179&quot; class=&quot;stylus&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static_resources:
  listeners:
  - name: listener_https
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 80
    listener_filters:
    - name: &quot;envoy.filters.listener.tls_inspector&quot;
      typed_config:
        &quot;@type&quot;: type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
    - name: &quot;envoy.filters.listener.http_inspector&quot;
      typed_config:
        &quot;@type&quot;: type.googleapis.com/envoy.extensions.filters.listener.http_inspector.v3.HttpInspector&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;p data-ke-size=&quot;size16&quot;&gt;가령 위와 같이 envoy config 파일에 listener_filters 기입되었다고 가정하면, Listener를 생성하는 단계에서 해당 필터들을 파악하고 생성하는 역할을 수행해야합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1999&quot; data-origin-height=&quot;439&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6KpI5/btr5AP7U6qS/S8RbZ91NEgYfYnwL1fdQDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6KpI5/btr5AP7U6qS/S8RbZ91NEgYfYnwL1fdQDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6KpI5/btr5AP7U6qS/S8RbZ91NEgYfYnwL1fdQDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6KpI5%2Fbtr5AP7U6qS%2FS8RbZ91NEgYfYnwL1fdQDk%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;1999&quot; height=&quot;439&quot; data-origin-width=&quot;1999&quot; data-origin-height=&quot;439&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;이를 위해서 Listener 내부에는 Listener Filters를 생성하는 listener_filter_factories와 Filter Chains를 생성하는 filter_chain_manager가 존재합니다.&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;1999&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c38O5q/btr5BJMUJ98/Kecopi5gkR53G61lKJZhrK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c38O5q/btr5BJMUJ98/Kecopi5gkR53G61lKJZhrK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c38O5q/btr5BJMUJ98/Kecopi5gkR53G61lKJZhrK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc38O5q%2Fbtr5BJMUJ98%2FKecopi5gkR53G61lKJZhrK%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;1999&quot; height=&quot;658&quot; data-origin-width=&quot;1999&quot; data-origin-height=&quot;658&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;Listener Filters는 Worker 쓰레드에 바인딩된 소켓마다 해당 Filter들이 생성되기 때문에, Filter를 생성하여 Listener가 보관하지 않고 Filter를 생성할 수 있는 Callback 메소드들의 Pointer를 Vector Container에 저장하고 있습니다. 따라서 Client가 접속을 요청했을 경우 Worker 쓰레드에 소켓이 할당되면, 이후 listener_filter_factories에 지정된 Callback 메소드들을 순회하면서 사용자에게 적합한 Filter 목록을 만들어줍니다.&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;652&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cck7nf/btr5DUmRpR5/bnhR5i5mDKNcK8mtkKF0tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cck7nf/btr5DUmRpR5/bnhR5i5mDKNcK8mtkKF0tk/img.png&quot; data-alt=&quot;출처 :https://www.envoyproxy.io/docs/envoy/latest/intro/life_of_a_request#listener-tcp-accept&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cck7nf/btr5DUmRpR5/bnhR5i5mDKNcK8mtkKF0tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcck7nf%2Fbtr5DUmRpR5%2FbnhR5i5mDKNcK8mtkKF0tk%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;504&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :https://www.envoyproxy.io/docs/envoy/latest/intro/life_of_a_request#listener-tcp-accept&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filter Chains(Network Filters)는 L3/L4를 담당하는 Filter로써 Listener Config 설정에 존재하는 Filter 목록들을 참조하여 내부적으로 여러 Filter Factory를 만듭니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679626176181&quot; class=&quot;less&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    filter_chains:
    - filter_chain_match:
        server_names: [&quot;acme.com&quot;]
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          &quot;@type&quot;: type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext
          common_tls_context:
            tls_certificates:
            - certificate_chain: {filename: &quot;certs/servercert.pem&quot;}
              private_key: {filename: &quot;certs/serverkey.pem&quot;}        
      filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          &quot;@type&quot;: type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          use_remote_address: true
          http2_protocol_options:
            max_concurrent_streams: 100
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: [&quot;acme.com&quot;]
              routes:
              - match:
                  path: &quot;/foo&quot;
                route:
                  cluster: some_service
          http_filters:
          - name: envoy.filters.http.router
            typed_config:
              &quot;@type&quot;: type.googleapis.com/envoy.extensions.filters.http.router.v3.Router&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;가령 위와같이 Listener 하위에 filter_chains를 정의했다고 가정해봅시다. 그러면 Listener를 생성하는 과정에서 해당 Config 내용을 확인하고 Transport Socket Factory와 Network Filter Factory를 구성합니다.&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;이때 filter_chain_manager의 역할은 사용자가 입력한 Filter Config 내용이 정상인지 검증을 하고 Filter 목록을 빠르게 찾기 위한 자료구조 및 Filter 생성 Callback을 생성하여 저장합니다. 이때 생성되는 Callback은 Listener와 마찬가지로 사용자가 실제 접속해서 Socket이 만들어졌을 때 해당 Socket에서 Network Filters 생성을 할때 사용됩니다.&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;1401&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uBd4a/btr5BlS4ejU/7afXSX0fsPzcYzBzIuvL6K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uBd4a/btr5BlS4ejU/7afXSX0fsPzcYzBzIuvL6K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uBd4a/btr5BlS4ejU/7afXSX0fsPzcYzBzIuvL6K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuBd4a%2Fbtr5BlS4ejU%2F7afXSX0fsPzcYzBzIuvL6K%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;1401&quot; height=&quot;618&quot; data-origin-width=&quot;1401&quot; data-origin-height=&quot;618&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;listener_manager_impl.cc&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;    for (auto&amp;amp; address : listener.addresses()) {
      listener.addSocketFactory(std::make_unique&amp;lt;ListenSocketFactoryImpl&amp;gt;(
          factory_, address, socket_type, listener.listenSocketOptions(), listener.name(),
          listener.tcpBacklogSize(), bind_type, creation_options, server_.options().concurrency()));
    }&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;지금까지 listener 생성 과정에서 Listener Filters와 Filter Chains(Network Filters)가 생성되는 것을 살펴봤습니다. 그 밖에 중요하게 살펴봐야할 점은 Listener가 외부 요청으로부터 Accept 하기 위한 Socket을 생성한다는 점입니다. 그리고 생성되는 Socket의 개수는 --concurrency로 전달된 인자 개수 만큼 생성됩니다.&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;그렇다면 왜 --concurrency 만큼 Socket을 생성할까요?&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;1401&quot; data-origin-height=&quot;840&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/urPUZ/btr5DTBwplp/Cyl1OwKSGKHrTJBsldyR20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/urPUZ/btr5DTBwplp/Cyl1OwKSGKHrTJBsldyR20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/urPUZ/btr5DTBwplp/Cyl1OwKSGKHrTJBsldyR20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FurPUZ%2Fbtr5DTBwplp%2FCyl1OwKSGKHrTJBsldyR20%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;1401&quot; height=&quot;840&quot; data-origin-width=&quot;1401&quot; data-origin-height=&quot;840&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;그 이유는 향후 Listener 별로 Worker 쓰레드에게 모두 바인딩을 수행하는데, 이때 Listener에서 생성한 Socket을 개별 쓰레드에서 참조가 가능하도록 하기 위함입니다.&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;listener_impl.cc&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;Network::SocketSharedPtr socket = factory.createListenSocket(
    local_address_, socket_type, options_, bind_type_, socket_creation_options_, worker_index);&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;따라서 Listener 생성 과정에서 Listner Component Factory가 Socket을 생성하여 개별 Worker 쓰레드에서 향후 참조할 수 있도록 사전 작업을 수행합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1 Worker 생성 및 Listener 바인드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;listener_manager_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679626176185&quot; class=&quot;lisp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  for (uint32_t i = 0; i &amp;lt; server.options().concurrency(); i++) {
    workers_.emplace_back(
        worker_factory.createWorker(i, server.overloadManager(), absl::StrCat(&quot;worker_&quot;, 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;Listener Manager가 생성될 때, 생성자 코드 내부에서는 위와 같이 전달받은 worker_factory를 통해서 Worker 쓰레드를 생성하는 것을 볼 수 있습니다. 그리고 생성하는 쓰레드의 개수는 인자로 전달된 --concurrency에 기반하고 있습니다.&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;listener_manager_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679626176185&quot; class=&quot;arduino&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void ListenerManagerImpl::startWorkers(GuardDog&amp;amp; guard_dog, std::function&amp;lt;void()&amp;gt; callback) {
  ...(중략)...
  uint32_t i = 0;

  ...(중략)...
  
  for (auto listener_it = active_listeners_.begin(); listener_it != active_listeners_.end();) {
    auto&amp;amp; listener = *listener_it;
    listener_it++;

   ...(중략)...
    for (const auto&amp;amp; worker : workers_) {
      addListenerToWorker(*worker, absl::nullopt, *listener,
                          [this, listeners_pending_init, callback]() {
                            if (--(*listeners_pending_init) == 0) {
                              stats_.workers_started_.set(1);
                              callback();
                            }
                          });
    }
  }
  for (const auto&amp;amp; worker : workers_) {
    ...(중략)...
    worker-&amp;gt;start(guard_dog, worker_started_running);
    ...(중략)...
    i++;
  }

  ...(후략)...
}&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;450&quot; data-origin-height=&quot;196&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFOWS6/btr5DUtA4zu/NdwAVlkzHoqXAxIt4FKFYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFOWS6/btr5DUtA4zu/NdwAVlkzHoqXAxIt4FKFYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFOWS6/btr5DUtA4zu/NdwAVlkzHoqXAxIt4FKFYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFOWS6%2Fbtr5DUtA4zu%2FNdwAVlkzHoqXAxIt4FKFYK%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;450&quot; height=&quot;196&quot; data-origin-width=&quot;450&quot; data-origin-height=&quot;196&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;이후 Envoy가 기동 될때, 위 코드와 같이 등록된 모든 active_listeners를 순회하면서, 모든 worker 쓰레드에 bind 하는 것을 볼 수 있습니다. 해당 내용에 대해서 조금 더 자세히 살펴보면 다음과 같습니다.&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;1626&quot; data-origin-height=&quot;1988&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LPVkS/btr5D2kTrfc/HDkDhvhXnLJk2n6N553FZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LPVkS/btr5D2kTrfc/HDkDhvhXnLJk2n6N553FZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LPVkS/btr5D2kTrfc/HDkDhvhXnLJk2n6N553FZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLPVkS%2Fbtr5D2kTrfc%2FHDkDhvhXnLJk2n6N553FZ0%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;1626&quot; height=&quot;1988&quot; data-origin-width=&quot;1626&quot; data-origin-height=&quot;1988&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;이전에 Listener 생성 과정을 살펴보면서, 위 그림과 같이 Worker 쓰레드 개수 만큼 개별 Listener에서 Socket을 생성함을 확인했습니다. 이때 생성된 Socket을 Worker 쓰레드에서 사용하기 위해서는 소켓을 각 쓰레드별로 바인딩하는 작업을 수행해야합니다.&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;connection_handler_impl.cc&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;for (auto&amp;amp; socket_factory : config.listenSocketFactories()) {
  auto address = socket_factory-&amp;gt;localAddress();
  // worker_index_ doesn't have a value on the main thread for the admin server.
  details-&amp;gt;addActiveListener(
      config, address, listener_reject_fraction_, disable_listeners_,
      std::make_unique&amp;lt;ActiveTcpListener&amp;gt;(
          *this, config, runtime,
          socket_factory-&amp;gt;getListenSocket(worker_index_.has_value() ? *worker_index_ : 0),
          address, config.connectionBalancer(*address)));
}&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;따라서 개별 쓰레드에서는 위 코드를 수행하면서, ActiveTcpListener를 생성하는 것을 확인할 수 있습니다. 위 코드에서 주목할 부분은 socket_factory(Listener)로부터 getListenSocket 메소드를 호출하면서 인자로 Worker 쓰레드의 index 번호를 넘기는 것을 확인할 수 있습니다.&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;listener_impl.cc&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;Network::SocketSharedPtr ListenSocketFactoryImpl::getListenSocket(uint32_t worker_index) {  
  ASSERT(worker_index &amp;lt; sockets_.size() &amp;amp;&amp;amp; sockets_[worker_index] != nullptr);
  return sockets_[worker_index];
}&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;이때 Listener 내부에서는 Worker 인덱스에 해당하는 Socket을 반환함으로써 Worker 쓰레드에서 해당 Listener를 참조할 수 있도록 합니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. LDS 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Listener Manager의 두 번째 역할은 LDS를 통한 dynamic resource 처리입니다. 이때 LDS의 생성은 Listener Component Factory에서 수행하며, 생성된 LDS에서 Listener Manager를 참조하도록 하여 LDS 정보가 수신되었을 때, Listener 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;LDS 생성 및 처리 과정을 보면 아래와 같습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1818&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvJrXC/btr5BLqogAQ/8aOF7WSOKcg9M10Iy6obUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvJrXC/btr5BLqogAQ/8aOF7WSOKcg9M10Iy6obUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvJrXC/btr5BLqogAQ/8aOF7WSOKcg9M10Iy6obUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvJrXC%2Fbtr5BLqogAQ%2F8aOF7WSOKcg9M10Iy6obUk%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;1818&quot; height=&quot;568&quot; data-origin-width=&quot;1818&quot; data-origin-height=&quot;568&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;1. Listener Component Factory로부터 LDS를 생성합니다.&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;2. Listener Manager를 LDS에서 참조하도록 합니다.&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;3. xDS API 수행을 위해 Cluster Manager로부터 Subscription Factory 인스턴스를 요청합니다.&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;4. Subscription Factory로부터 Subscription 생성을 요청합니다.&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;5. Subscription Factory에서는 gRPC 통신이 가능하도록 설정 후 Subscription을 발급합니다.&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;6. gRPC Mux를 통해 Management 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;7. LDS api_type 변경 이벤트 통지 시, 해당 Subscription에 알림을 전달합니다.&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;8. Subscription내에 매핑된 LDS 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;9. LDS에서는 Listener Manager를 참조하여 변경 사항에 맞추어 Listener 정보를 동기화 시킵니다.&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;위와 같은 과정을 거쳐 Listener Manager에서는 LDS 요청에 맞추어 최신 정보를 동기화시킵니다. 해당 과정의 대부분은 이전 Cluster Manager 설명 포스팅을 통해서 자세하게 소개하였으므로 해당 내용을 이해했다면, 위 과정의 흐름의 진행은 자연스러울 것입니다. 혹시 기억이 잘 안나신다면, 이전 포스팅 정독을 통해 관련 내용을 복기해보시는 것을 추천드립니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Listener Manager를 기동하면서 Worker 쓰레드 및 Listener 생성하여 이를 바인딩하는 역할과 LDS 처리역할에 대해서 살펴봤습니다. 또한 개별 Listener에서는 Configuration에 따라서 여러 Filter를 생성하기 위한 Callback 리스트를 관리하는 것을 살펴볼 수 있었습니다. 이 과정에서 생성된 정보등을 통해 향후 Client에서 Listener에 접속을 요청하면 이를 전달받아 후속 처리를 진행할 수 있습니다. 다만 이번 포스팅에서는 해당 내용에 대해서는 다루지 않으며, 이 다음 포스팅에서 조금 더 자세하게 접속 요청이 어떻게 처리되는지를 살펴보겠습니다.&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <category>Envoy</category>
      <category>envoy architecture</category>
      <category>envoy listener</category>
      <category>envoy 리스너</category>
      <category>envoy 아키텍처</category>
      <category>Istio</category>
      <category>istio architecture</category>
      <category>istio 아키텍처</category>
      <category>listener</category>
      <category>listener manager</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/218</guid>
      <comments>https://cla9.tistory.com/218#entry218comment</comments>
      <pubDate>Tue, 23 May 2023 10:16:15 +0900</pubDate>
    </item>
    <item>
      <title>4. [envoy-internals] Cluster Manager - Load Balancer</title>
      <link>https://cla9.tistory.com/217</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅을 통해서 Envoy Cluster Manager의 역할에 대해서 살펴봤습니다. Cluster Manager는 Cluster 관리 뿐만 아니라 Multiplexer 및 Subscription 생성에 관여합니다. 이번 포스팅에서는 Cluster Manager에서 관리하는 Cluster 내부에서 지정할 수 있는 Load Balancer 종류와 설정 방법에 대해서 다루고자합니다. 이번 포스팅 내용은 Envoy에 대한 설명 뿐만 아니라 Load Balancer 종류에 대한 알고리즘에 대한 개념 설명 위주로 알아보면서 어떻게 envoy가 Load Balancing을 처리하는지에 대해서 살펴보고자 합니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Envoy Loadbalancer&lt;/h2&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;391&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIlRhC/btr43YKWDXG/yJI0xvrmkiM13xGuQIxdwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIlRhC/btr43YKWDXG/yJI0xvrmkiM13xGuQIxdwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIlRhC/btr43YKWDXG/yJI0xvrmkiM13xGuQIxdwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIlRhC%2Fbtr43YKWDXG%2FyJI0xvrmkiM13xGuQIxdwk%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;300&quot; height=&quot;460&quot; data-origin-width=&quot;391&quot; data-origin-height=&quot;599&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;cluster_manager_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679379369069&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::ClusterEntry(
    ThreadLocalClusterManagerImpl&amp;amp; parent, ClusterInfoConstSharedPtr cluster,
    const LoadBalancerFactorySharedPtr&amp;amp; lb_factory)
    : parent_(parent), lb_factory_(lb_factory), cluster_info_(cluster) {
  priority_set_.getOrCreateHostSet(0);

  // TODO(mattklein123): Consider converting other LBs over to thread local. All of them could
  // benefit given the healthy panic, locality, and priority calculations that take place.
  if (cluster-&amp;gt;lbSubsetInfo().isEnabled()) {
    lb_ = std::make_unique&amp;lt;SubsetLoadBalancer&amp;gt;(
        cluster-&amp;gt;lbType(), priority_set_, parent_.local_priority_set_, cluster-&amp;gt;stats(),
        cluster-&amp;gt;statsScope(), parent.parent_.runtime_, parent.parent_.random_,
        cluster-&amp;gt;lbSubsetInfo(), cluster-&amp;gt;lbRingHashConfig(), cluster-&amp;gt;lbMaglevConfig(),
        cluster-&amp;gt;lbRoundRobinConfig(), cluster-&amp;gt;lbLeastRequestConfig(), cluster-&amp;gt;lbConfig(),
        parent_.thread_local_dispatcher_.timeSource());
  } else {
    switch (cluster-&amp;gt;lbType()) {
    case LoadBalancerType::LeastRequest: {
      ASSERT(lb_factory_ == nullptr);
      lb_ = std::make_unique&amp;lt;LeastRequestLoadBalancer&amp;gt;(
          priority_set_, parent_.local_priority_set_, cluster-&amp;gt;stats(), parent.parent_.runtime_,
          parent.parent_.random_, cluster-&amp;gt;lbConfig(), cluster-&amp;gt;lbLeastRequestConfig(),
          parent.thread_local_dispatcher_.timeSource());
      break;
    }
    case LoadBalancerType::Random: {
      ASSERT(lb_factory_ == nullptr);
      lb_ = std::make_unique&amp;lt;RandomLoadBalancer&amp;gt;(priority_set_, parent_.local_priority_set_,
                                                 cluster-&amp;gt;stats(), parent.parent_.runtime_,
                                                 parent.parent_.random_, cluster-&amp;gt;lbConfig());
      break;
    }
    case LoadBalancerType::RoundRobin: {
      ASSERT(lb_factory_ == nullptr);
      lb_ = std::make_unique&amp;lt;RoundRobinLoadBalancer&amp;gt;(
          priority_set_, parent_.local_priority_set_, cluster-&amp;gt;stats(), parent.parent_.runtime_,
          parent.parent_.random_, cluster-&amp;gt;lbConfig(), cluster-&amp;gt;lbRoundRobinConfig(),
          parent.thread_local_dispatcher_.timeSource());
      break;
    }
    case LoadBalancerType::ClusterProvided:
    case LoadBalancerType::LoadBalancingPolicyConfig:
    case LoadBalancerType::RingHash:
    case LoadBalancerType::Maglev:
    case LoadBalancerType::OriginalDst: {
      ASSERT(lb_factory_ != nullptr);
      lb_ = lb_factory_-&amp;gt;create();
      break;
    }
    }
  }
}&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;Envoy에서 Load Balancer는 ClusterEntry 내부에 존재하는 프로퍼티로써, ClusterEntry 생성 당시에 같이 생성됩니다. 이는 위 코드에서도 확인할 수 있으며, 생성자 내부에서 지정된 LoadBalancerType에 따라서 각기 다른 LoadBalancer가 지정됨을 확인할 수 있습니다.&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;그렇다면 Load Balancer 생성 이후 해당 과정은 어느 단계에서 이루어질까요?&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;1550&quot; data-origin-height=&quot;849&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfe8DZ/btr43ZJRkWL/lTIyosTKAs1twdn6E6aMV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfe8DZ/btr43ZJRkWL/lTIyosTKAs1twdn6E6aMV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfe8DZ/btr43ZJRkWL/lTIyosTKAs1twdn6E6aMV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfe8DZ%2Fbtr43ZJRkWL%2FlTIyosTKAs1twdn6E6aMV1%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;1550&quot; height=&quot;849&quot; data-origin-width=&quot;1550&quot; data-origin-height=&quot;849&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;이전 포스팅에서 Router로 부터 Connection Pool 획득 과정에 대해서 다루었습니다. 이 과정에서 Connection Pool을 획득하려면 먼저 host가 지정되어야합니다. 따라서 위 단계에서 4번째 단계 즉 Router가 Cluster 정보를 얻은 다음 Connection Pool을 요청하는 단계에서 Cluster Entry 내부에서는 가장먼저 Load Balancer에게 요청하여 어떤 host에 사용자 요청을 할당할 것인지를 요청합니다. 그리고&amp;nbsp;해당 host 정보를 토대로 Cluster Manager에게 해당 host에 대한 Connection Pool 요청을 수행합니다.&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;그렇다면 envoy에서 제공하는 Load Balancer는 어떤 종류가 있으며, 각 종류별로 어떻게 동작할까요? 이에 대해서 개념적으로 살펴보겠습니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Latency 관점&lt;/h2&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;671&quot; data-origin-height=&quot;379&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0dmul/btr45kHL1nX/Wu61atsoG4VCHzpjlIkUl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0dmul/btr45kHL1nX/Wu61atsoG4VCHzpjlIkUl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0dmul/btr45kHL1nX/Wu61atsoG4VCHzpjlIkUl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0dmul%2Fbtr45kHL1nX%2FWu61atsoG4VCHzpjlIkUl0%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;480&quot; height=&quot;271&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;379&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;p data-ke-size=&quot;size16&quot;&gt;하나의 Cluster 안에 여러개의 Host가 매핑되어있다고 가정해봅시다. 그러면 사용자가 연결 요청을 시도하면, Cluster 내부에 있는 여러 Host 중 하나를 Load Balancer가 선택할 것입니다. 이때 어떤 Host를 선택하는 것이 효율적일까요? 특정 Host에만 요청을 집중한다면, 결국 해당 Host의 처리 능력이 떨어지게되고 결국에는 해당 Host에 장애가 발생할 것입니다. 이를 처리하기 위해 응답속도 관점에서 고려할 수 있는 Load Balancing 알고리즘 종류에 대해 알아보겠습니다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1 Random&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째로 고려해볼 수 있는 것은 Random 알고리즘입니다. Random 알고리즘은 말 그대로 Load를 분배할 때, 임의로 아무 host에 배정함을 의미합니다. 따라서 해당 알고리즘은 특별할 것이 없으며, 굉장히 단순하고 구현하기가 쉽습니다. 즉 이말은 Load Balancing을 수행하는데 있어 overhead가 적다는 것을 의미합니다.&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;하지만 Random 알고리즘은 무작위로 부하를 분배하는 알고리즘이기 때문에 트래픽이 몰렸을 때, 완벽히 균등하게 모든 Host에 부하가 분배되지 못할 수 있습니다. 즉 운이 좋지 않으면 특정 노드에 부하가 몰릴 수 있음을 의미합니다. 따라서 해당 알고리즘은 트래픽이 적으며, Load Balancing 수행에 대한 overhead를 줄이고 싶을 때 사용하면 좋습니다.&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;envoy에서는 아래와 같이 lb_policy를 random으로 지정함으로써 Random 알고리즘을 사용하여 부하를 분산할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679448538908&quot; class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static_resources:
  clusters:
  - name: my_service
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: random
    load_assignment:
      cluster_name: my_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8080
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8081
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8082&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. Round Robin&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째로 살펴볼 것은 Round Robin 알고리즘입니다.&amp;nbsp;&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;1007&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhA2K6/btr5dfMA6AA/2IB8R05u2N1yiF5PkXBNCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhA2K6/btr5dfMA6AA/2IB8R05u2N1yiF5PkXBNCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhA2K6/btr5dfMA6AA/2IB8R05u2N1yiF5PkXBNCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhA2K6%2Fbtr5dfMA6AA%2F2IB8R05u2N1yiF5PkXBNCK%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;640&quot; height=&quot;343&quot; data-origin-width=&quot;1007&quot; data-origin-height=&quot;540&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;Round Robin 알고리즘은 보편적으로 많이 사용되는 Load Balancing 알고리즘으로 부하를 모든 Host 서버를 순환하면서 균등하게 분배하는 방식을 의미합니다. 가령 위 그림과 같이 6대의 Client 요청을 분배한다고 가정해봅시다. 그러면 3대의 B/E 서버에 순차적으로 하나씩 Client 요청을 분배합니다. 따라서 첫 요청은 Host 1에게 두번째 요청은 Host 2에게 분배하는 등 요청이 들어올 때마다 균등하게 분배합니다. 해당 알고리즘은 Random과 더불어 구현이 굉장히 단순합니다. Load Balancer 입장에서는 연결된 Host 중에서 이전에 분배했던 Host가 누구였는지 기억한다면, 그 다음에 분배해야될 대상이 누군지 알 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679449400207&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static_resources:
  clusters:
  - name: my_service
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    load_assignment:
      cluster_name: my_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8080
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8081
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8082&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;envoy에서는 위와같이 lb_policy를 round_robin으로 지정하여 Round Robin 알고리즘으로 부하 분산을 수행할 수 있습니다.&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;3-2-1 Weighted Round Robin&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Round Robin 알고리즘은 부하가 몰릴 경우에도 순차적으로 Host에게 부하를 분배하기 때문에 효율적으로 동작합니다. 하지만 여기에는 전제조건이 붙습니다. 그것은 모든 Host가 처리 시간이 동일함을 가정하였을 경우입니다. 이 경우 모든 서버의 하드웨어 스펙이 동일하지 않는다면 어떤 일이 벌어질까요?&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;805&quot; data-origin-height=&quot;883&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJKlMK/btr5fmjFkhp/NG12yz5OKyZRBEuNsuopy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJKlMK/btr5fmjFkhp/NG12yz5OKyZRBEuNsuopy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJKlMK/btr5fmjFkhp/NG12yz5OKyZRBEuNsuopy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJKlMK%2Fbtr5fmjFkhp%2FNG12yz5OKyZRBEuNsuopy0%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;640&quot; height=&quot;702&quot; data-origin-width=&quot;805&quot; data-origin-height=&quot;883&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;가령 Host 1의 하드웨어 사양이 나머지 두 개의 서버보다 2배 이상 좋다고 가정해봅시다. 이 경우 부하를 위와 같이 동일하게 분산하면, Host 1의 경우는 사용자의 요청을 다른 서버보다 빠르게 처리하여 idle 상태일 동안 나머지 서버에서는 여전히 사용자의 요청을 처리하는 경우가 발생합니다. 이 경우 요청 분배를 Host 1에 조금 더 할 수 있다면, 처리 속도를 높일 수 있음에도 불구하고 일반적인 Round Robin의 알고리즘의 경우에는 이를 처리할 수 없습니다. 따라서 이러한 하드웨어 성능 차이를 고려하기 위해 추가된 것이 가중치(Weight)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679450640754&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static_resources:
  clusters:
  - name: my_service
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: round_robin
    load_assignment:
      cluster_name: my_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8080
          load_balancing_weight: 2      
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8081
          load_balancing_weight: 1      
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8082
          load_balancing_weight: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가중치는 위와 같이 각각의 endpoint 별로 load_balancing_weight 속성을 추가할 수 있습니다. 위 예시는 해당 서버의 처리 능력을 고려하여 운영자가 임의로 매핑한 값이라고 가정해봅시다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7RXVG/btr49UCjOqo/KWEGkLDqiOEg8jKcZRNkKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7RXVG/btr49UCjOqo/KWEGkLDqiOEg8jKcZRNkKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7RXVG/btr49UCjOqo/KWEGkLDqiOEg8jKcZRNkKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7RXVG%2Fbtr49UCjOqo%2FKWEGkLDqiOEg8jKcZRNkKk%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;640&quot; height=&quot;715&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;938&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;이 경우 Round-Robin 방식으로 동작하지만, 각각의 Server 별로 가중치가 존재하기 때문에 이를 고려하여 위와같이 부하를 분배합니다. 즉 이전과 동일하게 6개의 요청을 전달받는다고 가정한다면, Host 1은 전체 부하의 33% 정도를 할당 받으며, Host 2는 17%, Host 3의 경우는 50%의 부하를 분배받는 것을 볼 수 있습니다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3 Least Request&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Random과 Round-Robin 알고리즘에 대해 살펴봤습니다. 위 두 알고리즘은 구현이 굉장히 쉽다는 장점이 존재합니다. 특히 Round-Robin의 경우는 부하가 집중될 때도 적은 overhead를 가지면서 적절히 부하를 분배할 수 있습니다. 하지만 Round-Robin에 가중치까지 적용하였다 할지라도 여기에는 이만큼 처리할 수 있다는 가정을 기반으로 처리하는 것이지 실제 처리량을 기반으로 Load Balancer가 부하를 처리하지는 않습니다.&amp;nbsp;&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;988&quot; data-origin-height=&quot;493&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cX2mSr/btr45jhQgSY/9OJKkSODLwBppL97GauXDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cX2mSr/btr45jhQgSY/9OJKkSODLwBppL97GauXDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cX2mSr/btr45jhQgSY/9OJKkSODLwBppL97GauXDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcX2mSr%2Fbtr45jhQgSY%2F9OJKkSODLwBppL97GauXDk%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;640&quot; height=&quot;319&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;493&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;가령 위 그림과 같이 Host 서버의 하드웨어 스펙이 모두 동일하다고 가정해봅시다. 또한 해당 host들이 매핑된 실제 서버에는 하나의 B/E만 존재하는 것이 아니라 여러 다른 app이 존재한다고 가정해봅시다. 이는 서버가 k8s 클러스터 등으로 구성되어있다면 충분히 가능한 시나리오일 것입니다.&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;이 경우 Round-Robin으로 분배하거나 Weighted Round-Robin을 적용하기 쉽지 않을 것입니다. 따라서 이 경우에는 실제 응답 요청을 처리하는 Connection 개수를 실시간으로 모니터링하면서 부하를 얼마나 감당할 수 있는지를 살펴보고 Load Balancer가 부하가 적은 쪽으로 전달하는 것이 효율적입니다.&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;810&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/O7E8f/btr5dqNUJ48/sNuSrgaiUV5JCJCTjkdzJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/O7E8f/btr5dqNUJ48/sNuSrgaiUV5JCJCTjkdzJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/O7E8f/btr5dqNUJ48/sNuSrgaiUV5JCJCTjkdzJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FO7E8f%2Fbtr5dqNUJ48%2FsNuSrgaiUV5JCJCTjkdzJ0%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;640&quot; height=&quot;427&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;540&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;가령 Least Request 알고리즘 환경에서는 위와 같이 현재 6개 Host에 부하를 분배한 상황에서 가령 Host 2에서 2번 요청에 대한 처리가 완료되어 Connection을 해제하였을 때, 그 다음 7번 요청을 Host 2에게 분배하는 방식입니다. 따라서 이를 지원하는 Load Balancer 타입에는 현재 연결된 active_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;또한 Least Request 알고리즘을 사용하는 환경에서도 서버마다 하드웨어 스펙등이 다를 수 있기 때문에 가중치를 적용할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679453533282&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static_resources:
  clusters:
  - name: my_service
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: least_request
    load_assignment:
      cluster_name: my_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8080
            load_balancing_weight: 2
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8081
            load_balancing_weight: 1
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8082
            load_balancing_weight: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령 위와 같이 envoy에서 지정했다면, 내부적으로는 아래와 같은 공식을 적용하여 가중치 값이 높은 쪽으로 부하가 분산됩니다. 참고로 해당 공식은 envoy 공식 문서에서 발췌하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679453732273&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;weight = load_balancing_weight / (active_requests + 1)^active_request_bias&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;참고로 active_request_bias 값은 load balancing 과정에서 active_requests에 대하여 우선 순위를 조정할 수 있는 값으로써 envoy config를 통해 지정할 수 있으며, 기본 값은 1.0입니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 가용성 관점&lt;/h2&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;553&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ByOOi/btr5nImjot6/7sjOVfqkcgI19X8Bk3FSwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ByOOi/btr5nImjot6/7sjOVfqkcgI19X8Bk3FSwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ByOOi/btr5nImjot6/7sjOVfqkcgI19X8Bk3FSwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FByOOi%2Fbtr5nImjot6%2F7sjOVfqkcgI19X8Bk3FSwK%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;640&quot; height=&quot;437&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;553&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;가령 위 그림과 같이 3개의 Host가 있고 각각의 Host에는 별개의 Cache 레이어가 존재하는 시스템이라고 가정해봅시다. 해당 시스템은 시스템 규모가 너무 커서 사용자별로 특정 데이터를 샤딩하여 각각의 Host 별개 Cache에 보관한다고 가정해봅시다. 이때 Cache Hit를 높이기 위해서는 특정 사용자 요청은 특정 Host로 보내져야 효율적일 것입니다.&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;하지만 Random, Round-Robin, Least Request 알고리즘은 이러한 특성을 보장해주지 않습니다. 그 이유는 해당 알고리즘은 해당 시점 전달된 트래픽 부하 분산에 초점이 맞추어져있기 때문입니다. 따라서 이전 요청 응답 전달 이후 같은 사용자 요청이 전달되었을 때, 각기 다른 Host로 전달될 가능성이 매우 높습니다. 그리고 어디로 배정될지 운영자 입장에서는 정확히 알 수 없기 때문에 운영상 불확실성을 가지고 있습니다. 이러한 불확실성은 대규모 시스템을 운영하는 입장에서는 엄청난 부담이 아닐 수 없습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y83rm/btr5gvHRKmH/ehSRGOKwzSHMzf7Pm3zyr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y83rm/btr5gvHRKmH/ehSRGOKwzSHMzf7Pm3zyr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y83rm/btr5gvHRKmH/ehSRGOKwzSHMzf7Pm3zyr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy83rm%2Fbtr5gvHRKmH%2FehSRGOKwzSHMzf7Pm3zyr0%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;640&quot; height=&quot;389&quot; data-origin-width=&quot;909&quot; data-origin-height=&quot;552&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;478&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V92k8/btr5pbpmBgq/V0bi4kWKRuF2apnqCxMldK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V92k8/btr5pbpmBgq/V0bi4kWKRuF2apnqCxMldK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V92k8/btr5pbpmBgq/V0bi4kWKRuF2apnqCxMldK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV92k8%2Fbtr5pbpmBgq%2FV0bi4kWKRuF2apnqCxMldK%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;256&quot; height=&quot;44&quot; data-origin-width=&quot;478&quot; data-origin-height=&quot;82&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;일반적으로 많이 사용하는 방법은 Hash 함수와 Modular를 활용하는 것입니다. 즉 위 그림과 같이 Host 목록에 대해서 특정 Key(사용자 Id) 등을 활용한 Hash 값을 토대로 Modular 연산을 적용하면, 특정 사용자 요청에 대해서는 매번 같은 결과 값이 나오게되니 아무리 요청을 많이 할지라도 특정 Host로 전달됨이 보장됩니다. 참고로 Modular 연산을 적용하는 이유는 아무리 Hash 값이 다르더라도 서버의 갯수만큼으로 Modular 연산을 수행하면, 서버 갯수 이내만큼의 값을 얻을 수 있기 때문입니다.&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;가령 Modular 연산 결과가 0이면 Host 1, 1이면 Host 2, 2이면 Host 3으로 할당된다고 가정했을 때, 사용자1의 Hash 값이 1이 나왔다면, 매번 Host 2로 전달이 보장됩니다.&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;위와 같이 살펴봤을 경우 Hash 와 Modular 연산 적용 또한 부하 부하를 적절히 분산하는데, 도움이 되는 것으로 보입니다. 하지만 여기에는 다음과 같은 문제가 있습니다.&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;1285&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BJ1SR/btr5qdUwPAh/TahWxmjcoMotx3zqAzBrf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BJ1SR/btr5qdUwPAh/TahWxmjcoMotx3zqAzBrf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BJ1SR/btr5qdUwPAh/TahWxmjcoMotx3zqAzBrf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBJ1SR%2Fbtr5qdUwPAh%2FTahWxmjcoMotx3zqAzBrf1%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;1285&quot; height=&quot;868&quot; data-origin-width=&quot;1285&quot; data-origin-height=&quot;868&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;가령 위와 같이 Host 1 ~ 3에 Connection과 사용자 데이터들이 Cache에 할당되어있다고 가정해봅시다. 이때 해당 비즈니스가 트래픽이 급격히 올라 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;위 그림을 살펴보면 기존 Modular 연산은 Host 개수가 3개기 때문에 1 ~ 3 Host에만 할당할 수 있을 뿐 Host 4에는 할당할 수 없습니다. 따라서 Host 4를 수용하기 위해서는 Host 4를 Host 리스트에 포함하고 나머지 연산 또한 4로 변경해야할 것입니다.&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;하지만 이 경우 지금까지 분배되었던 Connection과 데이터 저장에 커다란 이슈가 발생합니다.&amp;nbsp;&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;1285&quot; data-origin-height=&quot;868&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5ANsU/btr5t854v8o/5PlI9LRKZdFzXl6pwiiRhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5ANsU/btr5t854v8o/5PlI9LRKZdFzXl6pwiiRhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5ANsU/btr5t854v8o/5PlI9LRKZdFzXl6pwiiRhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5ANsU%2Fbtr5t854v8o%2F5PlI9LRKZdFzXl6pwiiRhK%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;1285&quot; height=&quot;868&quot; data-origin-width=&quot;1285&quot; data-origin-height=&quot;868&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;기존에 Hash 값이 11인 경우에는 원래대로라면 Host 3에 지속 라우팅이 되었어야 합니다. 하지만 연산 수식이 달라졌기 때문에, Host 4가 추가된 이후에는 수식의 결과에 따라(11%4=3) Host 4에 라우팅 될 것입니다. 따라서 Scale out 이후에도 서비스 안정성을 유지하기 위해서는 기존 Host 들에 연결된 Connection과 Cache된 데이터들에 대한 재분배가 필요합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1285&quot; data-origin-height=&quot;870&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPNBTo/btr5oNWMpJU/z0fSenjZIFtScktb7q0O20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPNBTo/btr5oNWMpJU/z0fSenjZIFtScktb7q0O20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPNBTo/btr5oNWMpJU/z0fSenjZIFtScktb7q0O20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPNBTo%2Fbtr5oNWMpJU%2Fz0fSenjZIFtScktb7q0O20%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;1285&quot; height=&quot;870&quot; data-origin-width=&quot;1285&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;&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;위 그림의 결과를 통해서 확인할 수 있는 사실은 Host 4 추가 이후 기존 데이터까지 포함해서 총 75%의 데이터 이동이 필요하며, 그만큼 Connection 또한 재분배가 일어난다는 점입니다. 이는 Scale out 수행해야하는 운영자 입장에서는 엄청난 부담으로 작용할 수 있습니다. 그 이유는 데이터 이동과 Connection을 분배하는 과정에서 발생하는 overhead로 인해 자칫 다른 Host에도 장애가 발생할 수 있기 때문입니다.&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;따라서 Hash와 Modular 연산을 통해서 부하를 분산하는 환경에서는 Scale Out이 발생할 때, Connection 분배와 데이터 이동을 최소화하는 방안을 강구해야합니다. 그렇다면 어떻게 이를 해결할 수 있을까요?&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;첫 번째로 고민해볼 수 있는 방법은 Scale Out 시에 기존 서버 대비 2배씩 확장하는 방법입니다.&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;1768&quot; data-origin-height=&quot;1345&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dIHQBf/btr5nKM13rU/uNz00pZQpxw4MGQDmizyBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dIHQBf/btr5nKM13rU/uNz00pZQpxw4MGQDmizyBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dIHQBf/btr5nKM13rU/uNz00pZQpxw4MGQDmizyBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdIHQBf%2Fbtr5nKM13rU%2FuNz00pZQpxw4MGQDmizyBk%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;1768&quot; height=&quot;1345&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;1345&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;가령 위 그림과 같이 기존 3대의 Host에서 Scale Out을 위해 서버를 2배로 확장했을 때 모습입니다. 이때 Connection 및 데이터 재분배 현황을 보면, 서버를 2배로 늘렸을 때 데이터 재분배 비율이 50% 정도로 낮아진 것을 볼 수 있습니다. 또한 데이터 이동 현황을 보면, 기존 Host가 보유하고 있던 데이터 중 일부가 다른 Host에게 그대로 전달된 것을 확인할 수 있습니다. 이는 데이터를 마이그레이션 해야하는 운영자 입장에서도 계획을 세우기 훨씬 수월하며, 분배에 대한 비효율도 다소 줄일 수 있습니다. 하지만 매번 Scale Out이 필요할 경우 서버를 2배씩 늘리는 것은 매우 비효율 적일 것입니다. 또한 이는 Scale Out시에는 도움이 될 수 있지만, Scale In을 수행하는 경우에는 문제가 발생합니다. Scale In을 수행할 때는 1/2씩 서버를 줄여야하는데 현실적으로 힘들기 때문입니다.&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 in/out이 잦은 Application을 사용하는 경우 단순한 Hash 함수와 Modular 연산의 적용은 운영 안정성이 저하되는 이슈를 낳습니다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1 Ring Hash(Consistent Hashing)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 살펴봤듯이 Hash와 Modular 기반으로 부하를 분산시에 Scale In/Out에 탄력적으로 대응하기 위해서는 서버 추가/삭제 이후 Connection과 연관된 데이터 재분배되는 양이 적어야 합니다. 이때 Consistent Hashing 알고리즘은 이에 대한 해결책을 제시해줍니다.&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;1006&quot; data-origin-height=&quot;977&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rZWl3/btr5oySSVwX/cRFMARbP8zunDpZyLzxKt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rZWl3/btr5oySSVwX/cRFMARbP8zunDpZyLzxKt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rZWl3/btr5oySSVwX/cRFMARbP8zunDpZyLzxKt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrZWl3%2Fbtr5oySSVwX%2FcRFMARbP8zunDpZyLzxKt0%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;640&quot; height=&quot;622&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;977&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;p data-ke-size=&quot;size16&quot;&gt;Consistent Hashing 방식은 위와 같이 Host 들이 하나의 Circle 안에 배치되어있다고 가정합니다. 그리고 각각의 Host가 담당하게될 Hash 값의 데이터 범위를 할당하여 그 안에 위치한 Connection을 담당하는 구조입니다. 이때 두 Host 사이에 위치한 Hash 값을 지닌 Connection은 시계 방향에 위치한 Host에 할당되는 것이 특징입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;Host&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;데이터 담당 범위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Host 1&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;해시값 &amp;lt;= 10724 OR 해시값 &amp;gt; 12345&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Host 2&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;10724 &amp;lt; 해시값 &amp;lt;= 11224&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;Host 3&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;11224 &amp;lt; 해시값 &amp;lt;= 12345&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;즉 위와 같이 각각의 Host 별로 Client 요청에 대한 Hash 값을 계산하였을 때, 해당 Hash 값이 담당하는 Host를 범위로써 관리합니다. 위와 같이 Ring &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Hash가&lt;span&gt; &lt;/span&gt;&lt;/span&gt;구축된 상태에서 Client 요청이 들어왔다고 가정해보겠습니다.&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;Case 1. 해시값 10765 요청 전달 시&lt;/b&gt;&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;1006&quot; data-origin-height=&quot;977&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfMpr5/btr5vtbG1Op/KDm6W8kbgYGcULKB6SKtR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfMpr5/btr5vtbG1Op/KDm6W8kbgYGcULKB6SKtR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfMpr5/btr5vtbG1Op/KDm6W8kbgYGcULKB6SKtR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfMpr5%2Fbtr5vtbG1Op%2FKDm6W8kbgYGcULKB6SKtR1%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;640&quot; height=&quot;622&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;977&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;해당 해시값이 10765이므로, 이는 Host1의 데이터 범위인 10724 보다 크고 Host 2의 해시값 범위인 11224보다 작으므로 Host 2에 할당될 것입니다. 또한 이후에 동일한 해시값이 입력된다면 지속적으로 Host 2에 할당됨이 보장됩니다.&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;Case 2. 해시값 12086 요청 전달 시&lt;/b&gt;&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;1006&quot; data-origin-height=&quot;1007&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QkQBl/btr5oyZVzFF/K2cnYyagcKQBs0SLMKKdT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QkQBl/btr5oyZVzFF/K2cnYyagcKQBs0SLMKKdT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QkQBl/btr5oyZVzFF/K2cnYyagcKQBs0SLMKKdT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQkQBl%2Fbtr5oyZVzFF%2FK2cnYyagcKQBs0SLMKKdT1%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;640&quot; height=&quot;641&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;1007&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;해시값이 12086의 요청이 전달되면, 11224보다 크고 12345보다 작으므로 Host 3에 Connection이 할당됩니다.&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;1079&quot; data-origin-height=&quot;1007&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BqVvh/btr5pzxfQwi/9wONVxlNjsL3LKxf6Skovk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BqVvh/btr5pzxfQwi/9wONVxlNjsL3LKxf6Skovk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BqVvh/btr5pzxfQwi/9wONVxlNjsL3LKxf6Skovk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBqVvh%2Fbtr5pzxfQwi%2F9wONVxlNjsL3LKxf6Skovk%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;640&quot; height=&quot;597&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;1007&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;이후 지속적인 Connection 연결이 요청되었고 그 결과 위와 같은 모습이 되었다고 가정해봅시다. 위 그림을 살펴보면 Host 1은 해시값이 5576과, 12567에 해당하는 Connection이 연결된 상황이라고 볼 수 있습니다. 만약 이 상황에서 Host 1에 장애가 발생하여 Down되었을 경우 어떻게 될까요?&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;1079&quot; data-origin-height=&quot;1007&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cK12ug/btr5oC2n05e/dEla3KHKEJXywg6HmTdFVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cK12ug/btr5oC2n05e/dEla3KHKEJXywg6HmTdFVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cK12ug/btr5oC2n05e/dEla3KHKEJXywg6HmTdFVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcK12ug%2Fbtr5oC2n05e%2FdEla3KHKEJXywg6HmTdFVK%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;640&quot; height=&quot;597&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;1007&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;Ring &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Hash를&lt;/span&gt; 살펴보면, Host 1이 없어졌을 경우 그 다음 연결된 Host는 Host 2임을 알 수 있습니다. 따라서 Consistent Hashing 알고리즘에서는 Host 1에 할당되었던 기존 Connection이 Host 2로 모두 이관됨을 확인할 수 있습니다.&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;1079&quot; data-origin-height=&quot;1007&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxEDq8/btr5rfL3YUl/IECQLxkiWzFB1Bjmaq4ABk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxEDq8/btr5rfL3YUl/IECQLxkiWzFB1Bjmaq4ABk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxEDq8/btr5rfL3YUl/IECQLxkiWzFB1Bjmaq4ABk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxEDq8%2Fbtr5rfL3YUl%2FIECQLxkiWzFB1Bjmaq4ABk%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;640&quot; height=&quot;597&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;1007&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;또한 위 결과를 통해 흥미로운 사실을 알 수 있습니다. Host 삭제에 따른 변화를 살펴보면, 기존 Host 1에 할당된 Connection만 영향을 받을 뿐 그외 나머지 Host에 연결된 Connection에는 전혀 영향을 받지 않는 것을 확인할 수 있습니다. 이를 통해 알 수 있는 사실은 Consistent Hashing 알고리즘을 사용할 경우에는 이전과 달리 Host 추가/삭제에 따른 영향도가 적어짐을 알 수 있습니다.&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;하지만 위와 같은 기본 Consistent Hashing 방식은 문제점이 존재합니다. 위 그림을 통해 살펴보면 Host 1이 Down 되었을 때 그 부하가 고스란히 Host 2에게 전달되는 것을 확인할 수 있습니다. 이는 Host 2 입장에서는 엄청난 부담으로 작용할 수 있습니다. 두 번째 문제는 Hash 값이 고르게 분포되지 않을 경우 특정 Host에 연결되는 Connection이 많을 수 있음을 의미합니다. 가령 위 예에서는 비교적 Hash 값이 고르게 분포되어 각각의 Host에서 Connection을 수행할 수 있었습니다. 하지만 상황에 따라서는 특정 Hash 범위 값이 지속 할당되면, 특정 Host에만 부하가 집중될 수 있습니다. 따라서 이를 보완하기 위해서 추가적으로 Virtual Node를 추가하는 방법을 많이 사용합니다.&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;4-2-1 Virtual Node&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 문제점이 서버 추가/제거 시 특정 Host에 Connection 재매핑이 몰린다는 특징이 있었습니다. 따라서 이러한 문제를 해결하기 위해서는 Virtual Node를 추가합니다. 이해를 돕기 위해 사례를 통해 확인해보겠습니다.&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;2006&quot; data-origin-height=&quot;1122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DqFso/btr5vtcCV7O/oF2870vZRgMW80m02gV9tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DqFso/btr5vtcCV7O/oF2870vZRgMW80m02gV9tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DqFso/btr5vtcCV7O/oF2870vZRgMW80m02gV9tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDqFso%2Fbtr5vtcCV7O%2FoF2870vZRgMW80m02gV9tk%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;2006&quot; height=&quot;1122&quot; data-origin-width=&quot;2006&quot; data-origin-height=&quot;1122&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;위와 같이 Host 별로 하나씩 Ring에 배치하는 것이 아니라 Ring Hash에 여러개의 Virtual Node를 생성하여 해시값을 부여하고 위 그림과 같이 배치를 수행합니다. 그러면 각각의 Node와 데이터 사이의 해시값 범위가 줄어들게 됩니다. 따라서 위 그림과 같이 Connection 별로 Hash 값을 수행한 결과가 조금 더 다양한 Host로 매핑될 수 있습니다.&amp;nbsp;&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;2031&quot; data-origin-height=&quot;1132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SYeft/btr5AzQ8WL5/wrcUfgdy8HkYxKx0ZE7hTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SYeft/btr5AzQ8WL5/wrcUfgdy8HkYxKx0ZE7hTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SYeft/btr5AzQ8WL5/wrcUfgdy8HkYxKx0ZE7hTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSYeft%2Fbtr5AzQ8WL5%2FwrcUfgdy8HkYxKx0ZE7hTK%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;2031&quot; height=&quot;1132&quot; data-origin-width=&quot;2031&quot; data-origin-height=&quot;1132&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;p data-ke-size=&quot;size16&quot;&gt;이러한 상황에서 Host 1에 Down이 발생하면 어떻게 될까요? 기존에 Host 1에 매핑되었던 Connection Hash 값 12567만 재배치를 수행하면 되기 때문에, 기존의 Consistent Hashing 방식 보다는 재배치되는 Connection 수가 많이 줄어들 수 있습니다.&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;이렇듯 Virtual Node를 Ring Hash에 추가하면, 재배치의 이점과 Virtual Node 개수가 증가할 수록 상대적으로 촘촘하게 Ring에 배치되기 때문에 부하를 보다 고르게 분산할 수는 있습니다. 다만 이러한 방법도 완벽하게 부하를 고르게 분산할 수는 없습니다.&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;Consistent Hashing 방식의 구현체는 다양하게 존재하는데, envoy는 위와 같이 내부에 virtual node가 추가된 형태의 자체 Consistent Hashing 방식으로 구현되어있습니다.&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;이때 각각의 host 별로 Virtual Node 할당을 위해서 envoy에서는 Configuration 설정에 &lt;span style=&quot;background-color: #fcfcfc; color: #404040;&quot;&gt;minimum_ring_size, &lt;span style=&quot;background-color: #fcfcfc; color: #404040;&quot;&gt;maximum_ring_size, 그리고 Hash 함수 등을 설정할 수 있으며, 각각의 Host 별로도 load_balancing_weight를 지정하여 개별 Host 별로 할당받는 hash bucket 개수를 조정할 수 있습니다.&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;이를 적용한 envoy configuration 예시는 다음과 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1679551303071&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static_resources:
  clusters:
  - name: my_service
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: ring_hash
    ring_hash_lb_config:
      minimum_ring_size: 1024
    load_assignment:
      cluster_name: my_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8080
          load_balancing_weight: 2      
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8081
          load_balancing_weight: 1      
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8082
          load_balancing_weight: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Consistent Hashing 방식에 대해서 살펴봤습니다. 그렇다면 Consistent Hashing 방식은 어떤 경우 사용하면 좋을까요? 이 방식은 Cluster와 Server의 변경이 잦은 경우 즉 대규모 환경에서 Reliability가 중요한 경우 Connection의 안정적인 재매핑을 위해 사용하면 좋습니다.&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;하지만 Ring Hash를 만들고 이를 유지하기 위해서는 기존에 소개한 다른 알고리즘(Random, Round-Robin, Least Request)에 비해 조금 더 많은 메모리 사용이 필요합니다. 또한 Cluster로부터 Server가 추가되거나 제거될 때 Hash Ring을 재구축 해야하기 때문에 update가 다른 알고리즘에 비해 조금 느린 특성이 있습니다.&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;따라서 해당 알고리즘을 사용한다면 이러한 특징을 참고하여 여러 알고리즘과 비교 및 PoC를 통해 선정하는 것이 좋습니다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2 Maglev&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 소개할 알고리즘은 Maglev 입니다. 해당 알고리즘은 Google에서 사용하는 Load Balancing 알고리즘으로 Consistent Hashing에 비해 부하를 보다 균등하게 분산해줄 수 있습니다. 또한 Consistent Hashing 알고리즘과 마찬가지로 서버의 삭제/수정 시에 Connection 재매핑 비율을 줄여줍니다. 해당 알고리즘 특징에 대해 살펴보면 다음과 같습니다.&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;Maglev 방식을 통해 부하를 분산하기 위해서는 2가지 자료구조가 필요합니다.&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;874&quot; data-origin-height=&quot;757&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKkXSQ/btr5AZioTMM/tEIPkOOr4ZkRKRkWzXpMN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKkXSQ/btr5AZioTMM/tEIPkOOr4ZkRKRkWzXpMN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKkXSQ/btr5AZioTMM/tEIPkOOr4ZkRKRkWzXpMN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKkXSQ%2Fbtr5AZioTMM%2FtEIPkOOr4ZkRKRkWzXpMN1%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;640&quot; height=&quot;554&quot; data-origin-width=&quot;874&quot; data-origin-height=&quot;757&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;첫 번째는 Lookup table입니다. Lookup table은 추후 Load Balancer가 어떤 Host로 보낼지 라우팅 정보를 기록하는 리스트입니다. 즉 라우팅 당시 해시 값을 Lookup table의 사이즈만큼으로 나머지 연산을 했을 때 나오는 index를 기준으로 Lookup table에 매핑된 Host로 라우팅을 수행합니다. 위 예시의 경우 Lookup table의 사이즈를 임의로 7로 선정했는데, Lookup table의 사이즈는 변경이 가능하며, 사이즈가 크면 클 수록 향후 서버 추가/삭제 등의 영향을 적게 받습니다.&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;두 번째 살펴볼 것은 Permutation table입니다. 해당 자료구조는 각 Host 별로 Lookup 테이블 인덱스 어디에 매핑될 지를 희망하는 우선순위를 나타냅니다. 따라서, Lookup table의 사이즈가 결정되면, 가장 먼저 수행하는 것은 각각의 Host 별로 어디에 매핑되기를 희망하는지를 해시 연산을 통해 계산합니다. 해당 과정을 Permutation이라고 부릅니다.&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;Permutation이 끝나고 나면, 위와 같이 각 Host 별로 우선순위를 결정할 수 있습니다. 그다음에 수행할 작업은 Permutation table을 토대로 Lookup table 내용을 채우는 작업입니다.&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;1168&quot; data-origin-height=&quot;757&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwKn0V/btr5E9Rxa0p/WdwxiunCGT26EbdCkHv8Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwKn0V/btr5E9Rxa0p/WdwxiunCGT26EbdCkHv8Gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwKn0V/btr5E9Rxa0p/WdwxiunCGT26EbdCkHv8Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwKn0V%2Fbtr5E9Rxa0p%2FWdwxiunCGT26EbdCkHv8Gk%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;640&quot; height=&quot;415&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;757&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;이 과정은 Popluation 이라고 부르며, 해당 과정을 통해 Maglev 알고리즘이 균등하게 부하를 분산할 수 있도록 Lookup table을 채워줍니다. 그렇다면 Popluation은 어떻게 이루어질까요? 위 Permuation table을 기초로 Popluation 과정을 조금 더 자세히 살펴보겠습니다.&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;313&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rAX59/btr5D3cYw1l/P62BEk6E88sJhEhxusU31k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rAX59/btr5D3cYw1l/P62BEk6E88sJhEhxusU31k/img.png&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44824.pdf&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rAX59/btr5D3cYw1l/P62BEk6E88sJhEhxusU31k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrAX59%2Fbtr5D3cYw1l%2FP62BEk6E88sJhEhxusU31k%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;460&quot; height=&quot;469&quot; data-origin-width=&quot;313&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;amp;nbsp;https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44824.pdf&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;N : Host 개수&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;M : Lookup table 크기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next[] : Permutation table 내에서 Host 별로 다음 index를 추적을 위한 배열&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;entry[] : Lookup table&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;Google의 Maglev 논문을 살펴보면, 위와 같이 Popluation 하는 과정에 대한 Pseudocode를 살펴볼 수 있습니다. 해당 과정을 몇 단계만 차례차례 살펴보면서 동작 과정에 대해서 이해해보겠습니다.&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;&lt;b&gt;Step 1.&lt;/b&gt;&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;1390&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciFziA/btr5LAOA57O/iANGR6IHbre4IVR7L6WtL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciFziA/btr5LAOA57O/iANGR6IHbre4IVR7L6WtL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciFziA/btr5LAOA57O/iANGR6IHbre4IVR7L6WtL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciFziA%2Fbtr5LAOA57O%2FiANGR6IHbre4IVR7L6WtL1%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;1390&quot; height=&quot;759&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;759&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;가장 먼저 수행하는 작업은 Lookup table과 Next 배열의 초기화입니다. 이때 Lookup table의 모든 원소는 -1로 Next 배열의 경우 배열의 index를 가르켜야 하기 때문에 0으로 초기화하는 것이 특징입니다.&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;Step 2.&lt;/b&gt;&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;1390&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/piB3e/btr5Bmj5xXX/hBe6KnXoihZLjIVXgoQNUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/piB3e/btr5Bmj5xXX/hBe6KnXoihZLjIVXgoQNUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/piB3e/btr5Bmj5xXX/hBe6KnXoihZLjIVXgoQNUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpiB3e%2Fbtr5Bmj5xXX%2FhBe6KnXoihZLjIVXgoQNUk%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;1390&quot; height=&quot;759&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;759&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;초기화가 완료되면, Host 별로 순차적으로 Lookup table을 갱신합니다. 먼저 0번 Host의 index가 현재 Next에서 0이므로 Permutation table의 0번 index를 살펴봅니다. 이때 Host 0은 Lookup table의 3번 index에 매핑되기 희망하므로 Lookup table의 해당 index을 탐색합니다. 이때 Lookup table에 해당 값이 -1 즉 아무 값도 매핑되지 않은 상태이므로 갱신이 가능합니다. 따라서 해당 index를 Host 0번 서버의 index인 0번으로 갱신합니다.&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;이후 Next 배열에서 그 다음 Permutation Table의 다음 참조 index를 가르키기 위해서 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;&lt;b&gt;Step 3.&lt;/b&gt;&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;1390&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLefEF/btr5zRrpIXo/3QKdHgq5VBqEwK7Si6KJwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLefEF/btr5zRrpIXo/3QKdHgq5VBqEwK7Si6KJwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLefEF/btr5zRrpIXo/3QKdHgq5VBqEwK7Si6KJwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLefEF%2Fbtr5zRrpIXo%2F3QKdHgq5VBqEwK7Si6KJwK%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;1390&quot; height=&quot;759&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;759&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;Host 0번이 매핑되었으므로 그 다음은 Host 1 차례입니다. Host 1의 현재 index가 0번이므로 Permutation table에서 해당 index를 살펴봅니다. 이때 Host 1은 Lookup table의 0번 index에 매핑되기 희망하므로 Lookup table의 해당 index를 탐색합니다. 살펴본 결과, 해당 index 값이 -1 이므로 선택될 수 있습니다. 따라서 Lookup table의 0번 index를 Host 1번의 값인 1로 갱신합니다. 또한 Next 배열에서 다음 값을 참조하기 위해서 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;&lt;b&gt;Step 4.&lt;/b&gt;&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;1390&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/drc3zr/btr5BJ7brvj/gpYjTz34oElyrJAM0qeaN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/drc3zr/btr5BJ7brvj/gpYjTz34oElyrJAM0qeaN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/drc3zr/btr5BJ7brvj/gpYjTz34oElyrJAM0qeaN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdrc3zr%2Fbtr5BJ7brvj%2FgpYjTz34oElyrJAM0qeaN1%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;1390&quot; height=&quot;759&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;759&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;그 다음은 Host 2번이 Lookup table에 매핑될 차례입니다. 이전과 마찬가지로 Next 배열에서 해당 Host의 Permutation table 참조 index가 0번이므로 0번을 살펴봅니다. 이때 Host 2는 Lookup table에서 3번 위치에 매핑되기를 희망합니다. 따라서 Lookup table이 3번 index를 살펴봅니다.&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;하지만 Host 0번에 기존에 선점하였기 때문에, 해당 index에 Host 2를 매핑할 수는 없습니다. 따라서 다른 Lookup table 위치를 탐색해야합니다.&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;1390&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVWLjl/btr5A5QcuYH/M2TvzHPrGuTa3txtLhoF81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVWLjl/btr5A5QcuYH/M2TvzHPrGuTa3txtLhoF81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVWLjl/btr5A5QcuYH/M2TvzHPrGuTa3txtLhoF81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVWLjl%2Fbtr5A5QcuYH%2FM2TvzHPrGuTa3txtLhoF81%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;1390&quot; height=&quot;759&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;759&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;이 경우에는 Permutation table의 다음 index 참조를 위해 먼저 Next 배열에 저장된 값을 1증가시킵니다. 이후 Permutation table에서 증가된 index 위치를 탐색합니다. 결과를 살펴보면, 두 번째로 선호하는 자리가 4번 index이기 때문에 Lookup table에서 해당 index를 탐색합니다. 살펴본 결과 해당 값은 -1이기 때문에 선점이 가능합니다. 따라서 Lookup table의 4번 index는 Host 2에게 배정합니다.&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;만약 또 충돌이 발생했다면, 방금전 수행했던 절차대로 Next 배열의 값을 1 증가시키고, Permutation table의 해당 위치를 탐색하여 Lookup table에 해당하는 index 값이 -1이 나올 때까지를 반복 수행하여 매핑을 수행합니다.&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;1390&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cd8NUN/btr5AQeFuHA/vHz2tdZUmuQrZ6rCcvCJm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cd8NUN/btr5AQeFuHA/vHz2tdZUmuQrZ6rCcvCJm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cd8NUN/btr5AQeFuHA/vHz2tdZUmuQrZ6rCcvCJm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcd8NUN%2Fbtr5AQeFuHA%2FvHz2tdZUmuQrZ6rCcvCJm1%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;1390&quot; height=&quot;759&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;759&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;Host 2의 매핑이 완료되었으면, Next 배열의 값을 1 증가 시켜, 다음 Permutation table 참조 index를 갱신합니다.&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;Step 5.&lt;/b&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;Host 2까지 매핑이 모두 완료되었으면, 다시 Host 0의 차례입니다. 따라서 Step 2-4 단계를 반복하면서, 개별 Host가 선호하는 Lookup table 갱신을 수행합니다. 해당 과정은 Lookup table이 완성될 때까지 반복합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;887&quot; data-origin-height=&quot;233&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nuZ6f/btr5AYjxcRh/LRlhkps8kIN3ZjAYhS8VUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nuZ6f/btr5AYjxcRh/LRlhkps8kIN3ZjAYhS8VUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nuZ6f/btr5AYjxcRh/LRlhkps8kIN3ZjAYhS8VUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnuZ6f%2Fbtr5AYjxcRh%2FLRlhkps8kIN3ZjAYhS8VUk%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;640&quot; height=&quot;168&quot; data-origin-width=&quot;887&quot; data-origin-height=&quot;233&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;Permutatiion 과정이 모두 끝나고 나면, 위 그림과 같이 Lookup table이 완성됩니다. 완성된 결과를 살펴보면, 모든 Host가 균등하게 배분되었음을 알 수 있습니다. 이는 Permutation 단계에서 각 Host 별로 돌아가면서 Lookup table을 순차적으로 갱신했기 때문입니다.&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;따라서 이후 Load Balancer는 Hash 값에 대한 Modular 연산을 통해서 Host에게 부하를 분산할 수 있게됩니다.&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;그렇다면 Maglev 알고리즘을 사용하는 상황에서 Host의 삭제가 발생했을 경우 어떻게 될까요?&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;1121&quot; data-origin-height=&quot;707&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzUTdA/btr5Bmj6Av2/WKROM7Cwcrjvco07v8YDz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzUTdA/btr5Bmj6Av2/WKROM7Cwcrjvco07v8YDz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzUTdA/btr5Bmj6Av2/WKROM7Cwcrjvco07v8YDz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzUTdA%2Fbtr5Bmj6Av2%2FWKROM7Cwcrjvco07v8YDz0%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;1121&quot; height=&quot;707&quot; data-origin-width=&quot;1121&quot; data-origin-height=&quot;707&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;이전과 마찬가지로 Permutation 과정을 진행합니다. 하지만 Permutation을 수행해도 기존 Host가 희망하는 해시값 기반 우선순위에는 변화가 없습니다. 따라서 Host 1이 제거될 경우 기존 table에서 Host 1 데이터들만 제거되어 갱신될 뿐, 기존 Host 들이 선호하는 값은 그대로입니다.&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;이후 Popluation을 진행하면, 위 그림의 Lookup table과 같이 변경됩니다. 이때 유심히 볼 점은 기존 Host들이 선호했던 Lookup table의 index 변화는 없는 상태에서 Host 1이 희망했던 index에만 다른 Host로 채워졌음을 알 수 있습니다. 따라서 이는 기존의 다른 Host들에게 연결된 Connection은 그대로 유지한채 기존 Host 1에 연결된 Connection만 재연결이 필요함을 의미합니다. 따라서 안정성을 확보할 수 있는 장점이 존재합니다.&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;지금까지 Maglev 방식에 대해서 살펴봤습니다. envoy에서는 다음과 같이 lb_policy를 maglev로 지정함으로써 해당 알고리즘을 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1679625117085&quot; class=&quot;cpp&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static_resources:
  clusters:
  - name: my_service
    connect_timeout: 0.25s
    type: strict_dns
    lb_policy: maglev
    maglev_lb_config:
      table_size: 65536
    load_assignment:
      cluster_name: my_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8080
          load_balancing_weight: 2      
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8081
          load_balancing_weight: 1      
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 8082
          load_balancing_weight: 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 위와 같이 maglev_lb_config를 통해 Lookup table의 크기를 변경할 수 있습니다. 참고로 Lookup table의 크기를 키우면 키울 수록 Host Down에 대한 Connection 재분배에 대한 영향도를 줄일 수 있으며, envoy에서는 기본값이 65536이고 최대 &lt;span style=&quot;background-color: #fcfcfc; color: #404040;&quot;&gt;5000011개로 지정할 수 있습니다.&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: #fcfcfc; color: #404040;&quot;&gt;이때 Lookup table 사이즈를 키우게되면, Connection 재분배 비율은 줄일 수 있으나 Memory를 그만큼 사용하며, Population에 드는 시간이 증가하므로 마찬가지로 적절한 PoC를 통해서 적정 값을 도출하는 것이 좋습니다.&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;color: #404040;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc;&quot;&gt;지금까지 Maglev에 대해서 살펴봤는데요. Maglev는 Consistent Hashing 방식에 비해 어떤 이점을 가지고 있을까요?&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;b&gt;&lt;span style=&quot;color: #404040;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc;&quot;&gt;1. Traffic Balancing&lt;/span&gt;&lt;/span&gt;&lt;/b&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;color: #404040;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc;&quot;&gt;Maglev의 경우는 ECMP(Equal-Cost Multipath) 라우팅을 추구합니다. Lookup table을 구성할 때 모든 Host가 동등한 비율로 배분하여 Traffic 전달 시에 보다 균등하게 부하를 분산할 수 있도록 추가합니다.&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;b&gt;&lt;span style=&quot;color: #404040;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc;&quot;&gt;2. 단순한 설정&lt;/span&gt;&lt;/span&gt;&lt;/b&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;Maglev의 경우는 Virtual Node를 생성하지 않습니다. Consistent Hasing 방식의 경우는 Virtual Node 생성을 위한 여러 기타 설정이나 균등하게 부하를 분산하기 위해 기타 Hashing 파라미터를 조정해야할 수 있지만 Maglev는 Table 사이즈를 조정하는 정도를 통해서 목표를 달성할 수 있습니다.&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;하지만 Maglev를 사용하기 위해서는 Permutation Table, Lookup table, Next 등 다양한 자료구조가 사용되며, Host 생성/추가 발생 시에 Permutation과 Popluation 작업이 지속 발생되기 때문에 상대적으로 높은 메모리와 CPU 비용이 요구됩니다. 따라서 무조건 Maglev 방식이 좋다고 말할 수는 없으며, 언제나 비즈니스 상황과 인프라 환경에 맞추어 테스트 후 적절한 방법을 사용하는 것이 좋다고 볼 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 envoy에서 제공하는 다양한 Load Balancing 알고리즘 방법에 대해서 살펴봤습니다. 업무 환경에 적용할 때는 비즈니스 환경의 규모 및 성능을 고려하여 적절한 Load Balancing 전략을 차용하는 것이 중요합니다. 따라서 각각의 알고리즘이 어떻게 동작하며, 어떠한 특징이 있는지 잘 이해하고 있는 것이 무엇보다 중요하다고 볼 수 있겠습니다.&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>MSA/Istio</category>
      <category>Envoy</category>
      <category>envoy architecture</category>
      <category>envoy load balancer</category>
      <category>envoy 아키텍처</category>
      <category>Istio</category>
      <category>istio architecture</category>
      <category>istio 아키텍처</category>
      <category>Load Balancer</category>
      <category>로드밸런싱</category>
      <category>이스티오</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/217</guid>
      <comments>https://cla9.tistory.com/217#entry217comment</comments>
      <pubDate>Fri, 19 May 2023 09:39:07 +0900</pubDate>
    </item>
    <item>
      <title>3. [envoy-internals] Cluster Manager - 역할</title>
      <link>https://cla9.tistory.com/216</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞선 두 개의 포스팅을 통해 envoy 고수준 아키텍처 구조 및 쓰레딩 모델에 대해서 학습하였습니다. 이번 포스팅 부터는 Envoy의 가장 핵심이되는 주요 컴포넌트에 대해서 각 컴포넌트가 세부적으로 어떻게 구성되어있는지 그리고 컴포넌트간에 상호 작용이 어떻게 이루어지는지에 대해 자세하게 살펴보겠습니다.&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;참고로 이번 포스팅의 내용은 TCP 기반 설정이 구성되었다는 가정하에 작성하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Envoy 구조&lt;/h2&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;1055&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EDpkn/btrXHBQEYkd/wKTDpN21ujQMWx8J7ULsu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EDpkn/btrXHBQEYkd/wKTDpN21ujQMWx8J7ULsu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EDpkn/btrXHBQEYkd/wKTDpN21ujQMWx8J7ULsu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEDpkn%2FbtrXHBQEYkd%2FwKTDpN21ujQMWx8J7ULsu1%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;640&quot; height=&quot;193&quot; data-origin-width=&quot;1055&quot; data-origin-height=&quot;318&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;이 시리즈의 첫 번째 포스팅에서 Listener, Endpoint, Cluster 개념에 대해서 간략하게 살펴봤습니다. 그 중 Endpoint의 경우 Cluster와 종속적인 관계를 지니므로 Cluster에서 Endpoint를 관리합니다. 따라서 envoy의 핵심 컴포넌트를 꼽자면 Listener와 Cluster라고 봐도 무방합니다. 그렇다면 Listener와 Cluster는 누가 관리하고 어떻게 생성될까요? 위 그림을 보면 Listener Manager와 Cluster Manager가 해당 컴포넌트를 관리하는 것을 짐작할 수 있습니다.&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;1407&quot; data-origin-height=&quot;492&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXfbq5/btrXIoQAaQO/fIAhaC8rkaCyuNshXPOfa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXfbq5/btrXIoQAaQO/fIAhaC8rkaCyuNshXPOfa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXfbq5/btrXIoQAaQO/fIAhaC8rkaCyuNshXPOfa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXfbq5%2FbtrXIoQAaQO%2FfIAhaC8rkaCyuNshXPOfa1%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;1407&quot; height=&quot;492&quot; data-origin-width=&quot;1407&quot; data-origin-height=&quot;492&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;실제로 envoy server 기동 코드를 살펴보면 config.yaml 파일을 읽어서 envoy 기동에 필요한 사용자 정의 설정을 파싱하는 작업을 선행합니다. 그 이후 주요 컴포넌트 기동에 필요한 항목등을 생성합니다.&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;이때 위 그림을보면 Listener Component Factory와 Cluster Manager Factory를 생성하는 것을 볼 수 있습니다. 여기서 Factory라는 말에서 의미가 바로 전달되듯이 해당 Factory 들은 Listener Manager와 Cluster 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;지금부터는 Listener Component Factory로부터 생성되는 Listener Manager의 세부 구조와 Cluster Manager Factory로부터 생성되는 Cluster Manager에 대해서 상세하게 알아보고자 합니다. 다만 이번 포스팅에서는 Cluster Manager에 대해서 중점적으로 알아보고 Listener는 다음 포스팅에서 다루겠습니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Subscription Factory 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cluster Manager는 Cluster Manager Factory로 부터 생성되는 컴포넌트로써, 이름을 통해 유추할 수 있듯이 Cluster를 관리하는 역할을 수행합니다. 따라서 이에 대한 이해를 위해서는 Cluster 관리가 어떻게 이루어지는지 먼저 학습하는 것이 좋습니다. 하지만 그 전에 Cluster Manager의 주요 역할 중 하나인 Subscription Factory 관리와 gRPC Multiplexer에 대해서 먼저 다루어보고자 합니다.&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;이를 먼저 다루는 이유는 Cluster Manager에서 관리하는 Cluster를 등록하는 방식에는 Static 방식과 Dynamic 방식이 존재하는데, Dynamic 방식의 Cluster Update 과정에 대해 이해하려면 Subscription과 gRPC Multiplexer에 대한 이해가 선행되어야하기 때문입니다.&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;방금 전 언급했듯이 Cluster Manager의 역할 중 하나는 Subscription Factory를 관리하는 것이라고 설명했습니다. 그렇다면, Subscription은 무엇이고 Subscription Factory는 어떤 역할을 수행할까요? 이 부분에 대해서 살펴보겠습니다.&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;이전에 Envoy는 xDS API를 통한 Resource(Listener, Endpoint 등)를 관리한다고 언급한바 있습니다.&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;1843&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B7fpG/btrXIiCRWXP/Xd4Ku0dJIijQkSvpqdxQok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B7fpG/btrXIiCRWXP/Xd4Ku0dJIijQkSvpqdxQok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B7fpG/btrXIiCRWXP/Xd4Ku0dJIijQkSvpqdxQok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB7fpG%2FbtrXIiCRWXP%2FXd4Ku0dJIijQkSvpqdxQok%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;1843&quot; height=&quot;558&quot; data-origin-width=&quot;1843&quot; data-origin-height=&quot;558&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;참고로 xDS API에는 gRPC, Rest API, 파일 동기화 방법이 있지만, 여기서는 gRPC 방식을 사용한다고 가정하겠습니다.&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;이때 gRPC 통신을 위해서 내부적으로 Multiplexer를 사용하는데, 자원 동기화 요청이 Envoy로 전달되면, 이를 수신받아 해당 Resource를 처리하는 모듈(CDS, LDS, EDS 등)에게 전달 해줘야합니다. 그리고 처리하는 모듈 쪽에서는 사전에 Callback을 등록하여 응답이 전달되면 해당 Callback을 실행할 수 있도록 합니다.&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;1324&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nCG6M/btr41IIqMC6/wKpMkKSrOFkzi6M8oE4u3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nCG6M/btr41IIqMC6/wKpMkKSrOFkzi6M8oE4u3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nCG6M/btr41IIqMC6/wKpMkKSrOFkzi6M8oE4u3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnCG6M%2Fbtr41IIqMC6%2FwKpMkKSrOFkzi6M8oE4u3k%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;1324&quot; height=&quot;195&quot; data-origin-width=&quot;1324&quot; data-origin-height=&quot;195&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;이 과정에서 중간 매개체 역할을 하는 것이 Subscription입니다. 즉 Resource를 처리하는 모듈에서 Config 대상 오브젝트 타입과 Callback을 Subscription 객체로 생성하여 등록합니다. 그리고 해당 Subscription 생성은 Subscription Factory가 수행하고, 추후 gRPC Multiplexer에 등록합니다. 이후 gRPC Multiplexer로 부터 Resource 동기화 요청이 수신되면, 해당 요청에 상응하는 Subscription의 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;예를 통해서 살펴보겠습니다. 가령 Listener Manager에서 LDS를 등록한다고 가정하겠습니다. 이 경우 먼저 Cluster Manager의 Subscription Factory로부터 Subscription 생성을 요청합니다. 이때 Subscription Factory는 내부 gRPC Multiplexer에 해당 정보를 등록합니다. 이후 LDS 요청 타입의 응답이 전달되었을 경우 Callback을 통해 Listener 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;span&gt;이때 Cluster Manager는 Subscription Factory를 내부 프로퍼티로 가지고 있고, 외부의 다른 컴포넌트로부터 Subscription 생성, 삭제 등의 처리를 위임받아 처리하는 역할을 수행합니다.&lt;/span&gt;&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;2059&quot; data-origin-height=&quot;817&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XeXau/btrX0KR5mUs/s3bUBX4KWdakeV6w6w0hJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XeXau/btrX0KR5mUs/s3bUBX4KWdakeV6w6w0hJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XeXau/btrX0KR5mUs/s3bUBX4KWdakeV6w6w0hJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXeXau%2FbtrX0KR5mUs%2Fs3bUBX4KWdakeV6w6w0hJ1%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;2059&quot; height=&quot;817&quot; data-origin-width=&quot;2059&quot; data-origin-height=&quot;817&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;이를 조금 더 자세히 표현하면, 개별 xDS API들은 SubscriptionBase 클래스를 상속받고 있으며, 해당 클래스에는 각각 Config Update에 대하여 수행해야할 메소드들이 정의되어있습니다. 따라서 각각의 API는 구현이 요구되는 메소드들에 대해서 모듈 특성에 따라 처리방법이 기술되어있습니다. 이후 xDS API를 사용하기 위해서 Cluster Manager에 존재하는 Subscription Factory로부터 Subscription을 요청하는데, 이때 Subscription Factory 내부에서는 Multiplexer에 해당 Callback 정보를 등록하고 생성된 Subscription을 반환합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. gRPC Multiplexer&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 xDS API 사용을 위해 Subscription Factory로부터 Subscription을 생성받는 과정에 대해서 살펴봤습니다. 이번에는 subscription 생성 이후 상호작용 수행하는 gRPC Multiplexer의 구성요소 및 동작 원리에 대해서 살펴보겠습니다.&amp;nbsp;&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;1096&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmsQtQ/btrXJfFzJwY/Je9NKlovqVPX8qjYDKExjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmsQtQ/btrXJfFzJwY/Je9NKlovqVPX8qjYDKExjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmsQtQ/btrXJfFzJwY/Je9NKlovqVPX8qjYDKExjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmsQtQ%2FbtrXJfFzJwY%2FJe9NKlovqVPX8qjYDKExjk%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;640&quot; height=&quot;260&quot; data-origin-width=&quot;1096&quot; data-origin-height=&quot;445&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;gRPC Multiplexer에는 SotW를 위한 GrpcMuxSotw, Delta XDS를 위한 GrpcMuxDelta 등 여러가지 GrpcMuxImpl을 위한 구현체가 존재합니다. 그리고 해당 구현체는 공통적으로 위 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;1. watchMap은 외부에서 생성한 Subscription을 기반으로 gRPC 통신 수행결과를 전달받아 Callback 함수를 실행시키기 위한 자료구조로써 Watch 객체를 생성하여 저장합니다. Watch 객체에는 사용자가 지정한 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;2. gRPC 통신을 위해서는 SubscriptionState가 필요합니다. 해당 객체는 SubscriptionStateFactory로 부터 생성이 가능하며, SubscriptionState에는 WatchMap에 존재하는 Watch 객체를 매핑하여, 향후 gRPC 응답이 반환되었을 때 이를 전달할 수 있도록 기능이 구현되어있습니다.&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;3. grpcStream은 gRPC 통신을 수행하는 주체로써 외부 Management 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;gRPC Multiplexer는 여러개가 존재할 수 있는데, 개별 Multiplexer들을 관리하기 위해서 외부에 AllMuxes가 존재합니다. AllMuxes는 생성되는 여러개의 GrpcMuxImpl의 정보를 관리하기 위한 자료구조로써 hash set 형태로 생성되는 모든 GrpcMuxImpl을 관리합니다.&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;지금까지 gRPC Multiplexer와 관련하여 컴포넌트 정보를 확인했습니다. 아직은 gRPC를 통한 동기화 과정을 살펴보지 않았기 때문에 위 컴포넌트가 어떻게 상호작용하며 어떤 역할을 수행하는지 와닿지 않을 수 있습니다. 따라서 실제 사용자가 Subscription을 요청하여 데이터가 처리되는 과정을 살펴보면서 각각의 컴포넌트가 어떻게 연계되는지를 살펴보겠습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2344&quot; data-origin-height=&quot;691&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qVCaK/btrXJhpRZ6b/qp9xyTM7Tt4Hf32kH3T8w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qVCaK/btrXJhpRZ6b/qp9xyTM7Tt4Hf32kH3T8w1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qVCaK/btrXJhpRZ6b/qp9xyTM7Tt4Hf32kH3T8w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqVCaK%2FbtrXJhpRZ6b%2Fqp9xyTM7Tt4Hf32kH3T8w1%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;2344&quot; height=&quot;691&quot; data-origin-width=&quot;2344&quot; data-origin-height=&quot;691&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;1. 이전에 설명했듯이 xDS API를 사용하려는 CDS, EDS, LDS 등은 Cluster Manager가 가지고 있는 Subscription Factory로부터 Subscription을 전달받아야합니다. 따라서 먼저 Cluster Manager로부터 Subscription Factory 정보를 얻어옵니다.&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;2. Subscription Factory는 CDS, EDS, LDS 등에 매핑되어있는 api_type(SotW, Delta 등)을 분석하여 적절한 GrpcMuxImpl 구현체를 반환하도록 요청합니다.&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;3. api_type에 따른 GrpcMuxImpl 구현체를 생성한 이후 Subscription에 해당정보를 매핑하여 반환합니다.&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;4. Subscription을 전달받으면, 이후 통신 수행을 위하 GrpcMuxImpl에게 통신 수행을 요청합니다.&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;5. GrpcMuxImpl은 WatchMap에 Subscription에서 요구하는 type_url 정보에 해당하는 Watch 정보가 존재하는지 확인합니다. 만약 존재하지 않는다면 새로운 Watch를 생성합니다. 이때 Watch에는 Subscription에 존재하는 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;6. WatchMap에 type_url에 해당하는 Watch 정보가 없을 경우 SubscriptionStateFactory로부터 새로운 SubscriptionState를 생성합니다. 이때 생성되는 SubscriptionState에는 5번 단계에서 생성한 Watch 정보를 포함시켜, 향후 gRPC 응답이 왔을 때 해당 Watch에게 응답 수신 행위를 수행할 수 있도록 설정합니다. SubscriptionState 생성이 완료되면, 해당 정보는 GrpcMuxImpl 내부에 존재하는 subscriptions_ 자료구조에 저장됩니다.&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;7. SubscriptionState를 통해 gRPC 통신을 수행을 요청합니다.&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;8. gRPCStream은 xDS의 Management Server(ex: istio)와 연결을 수행하여 DiscoveryRequest를 요청합니다. 이후 Management Server로부터 Discovery Response를 전달하면 이를 수신받습니다.&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;9. gRPCStream은 응답 메시지의 type_url을 기준으로 &amp;nbsp;subscriptions_ 자료구조에서 해당 type_url에 해당하는 Subscription을 찾아 응답 메시지를 전달합니다.&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;10. SubscriptionState는 6번 단계에서 매핑한 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;11. Watch는 생성당시 저장된 요청자의 Callback을 수행하여 처리를 위임합니다.&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;1228&quot; data-origin-height=&quot;601&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNKoHv/btrXJVGUaYy/FfKZUsFeOgOm3NwKcvyIO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNKoHv/btrXJVGUaYy/FfKZUsFeOgOm3NwKcvyIO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNKoHv/btrXJVGUaYy/FfKZUsFeOgOm3NwKcvyIO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNKoHv%2FbtrXJVGUaYy%2FFfKZUsFeOgOm3NwKcvyIO1%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;640&quot; height=&quot;313&quot; data-origin-width=&quot;1228&quot; data-origin-height=&quot;601&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;지금까지 gRPC Multiplexer에 대해서 살펴봤습니다. 이전 시나리오에서는 1개의 Subscription이 등록되고 gRPC 통신이 수행되는 과정에 대해서 살펴봤습니다. 실제로는 CDS, EDS, LDS 등 여러개의 Subscription이 존재하기 때문에 Subscription Factory에서 생성되는 Subscription은 다수가 될 수 있으며, 그에 따라 gRPC Mux 내부에는 Watch와 SubscriptionState 수가 다수가 될 수 있습니다. 마찬가지로 gRPC를 통해 수행되는 Mux 타입이 다수라면 해당 타입 또한 여러개가 될 수 있습니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Cluster 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Cluster Manager의 기능 중 하나인 gRPC Multiplexer와 Subscription 관리에 대해서 살펴봤습니다. 이번에는 Cluster Manager에서 가장 핵심이 되는 Cluster 관리에 대해서 살펴보겠습니다.&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;Cluster Manager는 이름 그대로 Cluster를 관리하는 컴포넌트이기 때문에 가장 중요한 역할은 Cluster의 생성과 삭제 수정등을 수행하는 것 입니다.&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;1200&quot; data-origin-height=&quot;396&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wIkfv/btrXGnymqbS/92LKxM8dXVmr7UsoRS3dMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wIkfv/btrXGnymqbS/92LKxM8dXVmr7UsoRS3dMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wIkfv/btrXGnymqbS/92LKxM8dXVmr7UsoRS3dMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwIkfv%2FbtrXGnymqbS%2F92LKxM8dXVmr7UsoRS3dMK%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;396&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;396&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;Cluster Manager는 기동 당시에 Cluster Manager Factory에 의해서 생성됨을 이전에 설명했습니다. 이때 생성 과정에서 주요하게 살펴볼 부분은 Cluster Manager 생성과 동시에 CDS 설정이 존재한다면, CDS를 생성하고 config.yaml에 존재하는 Static Resource는 Parsing 정보를 토대로 읽어들어 ClusterData를 생성하는 것입니다. 또한 이후 CDS를 통해 ClusterData 정보가 업데이트되면, 해당 정보를 Cluster Manager가 전달받아 업데이트를 수행하는 것 또한 Cluster Manager의 역할입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675390652512&quot; class=&quot;yaml&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static_resources:
  clusters:
  - name: local_service
    connect_timeout: 0.25s
    type: STRICT_DNS
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: local_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address: { address: webservice, port_value: 90 }         
    health_checks:
      timeout: 3s
      interval: 90s
      unhealthy_threshold: 5
      healthy_threshold: 5
      no_traffic_interval: 240s      
      http_health_check:
        path: &quot;/ping&quot;
        event_log_path: /var/log/envoy/health_check.log
        expected_statuses:
          start: 200
          end: 201
    outlier_detection:
      consecutive_5xx: 2
      base_ejection_time: 30s
      max_ejection_percent: 40
      interval: 20s
      success_rate_minimum_hosts: 5
      success_rate_request_volume: 10&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;해당 과정에 대해서 조금 더 알아보기 위해 예를 통해서 살펴보겠습니다. 가령 위와 같이 static cluster 정보가 존재한다고 가정해봅시다. 여기서 Cluster 정보를 살펴보면, Load Balancing, Health Check, Outlier Detection 정보가 포함되어있는 것을 확인할 수 있습니다. 따라서 이를 통해 유추하자면 Envoy 내부에서 Cluster를 관리할 때 그 안에는 Load Balancing, Health Check&amp;nbsp; 및 Outlier Detection을 수행하는 컴포넌트가 존재할 것이라는 것을 알 수 있습니다.&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;이번에는 Cluster Manager에서 Cluster를 만드는 과정에 대해서 살펴보겠습니다.&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;722&quot; data-origin-height=&quot;650&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cokp6e/btr3THCThla/7nBkyCC08IZBOKhSwtnaE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cokp6e/btr3THCThla/7nBkyCC08IZBOKhSwtnaE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cokp6e/btr3THCThla/7nBkyCC08IZBOKhSwtnaE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcokp6e%2Fbtr3THCThla%2F7nBkyCC08IZBOKhSwtnaE1%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;640&quot; height=&quot;576&quot; data-origin-width=&quot;722&quot; data-origin-height=&quot;650&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;먼저 살펴볼 것은 static resource에 존재하는 cluster 정보를 Cluster Manager에서 관리하는 방법입니다. 해당 과정은 Envoy 기동 과정에서 수행되는 작업으로 생성 과정을 간략하게 살펴보면 다음과 같습니다. 먼저 envoy 기동시에 config 파일을 파싱합니다. 이후 Cluster Manager의 Cluster 저장을 위해 ClusterData 구조로 만들어서 이를 active_clusters_ 라고 불리는 map에 Cluster 정보를 저장합니다. 그렇다면 active_clusters_는 어떻게 구성되어있을까요?&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;741&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDs8Id/btrXHUbqQvt/CWG0tQdLDGg2nkKZQFALd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDs8Id/btrXHUbqQvt/CWG0tQdLDGg2nkKZQFALd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDs8Id/btrXHUbqQvt/CWG0tQdLDGg2nkKZQFALd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDs8Id%2FbtrXHUbqQvt%2FCWG0tQdLDGg2nkKZQFALd0%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;480&quot; height=&quot;53&quot; data-origin-width=&quot;741&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;/figure&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;해당 자료구조는 위 그림과 같이 Cluster 이름과 Cluster 정보로 이루어진 Map입니다. Cluster가 생성된 이후에 active_clusters_에 ClusterData를 추가하며, Cluster 삭제 이벤트가 발생하면 이를 삭제하는 등 해당 자료구조를 통하여 Cluster Manager가 관리하고 있는 Cluster의 종류와 갯수 현행화를 수행합니다.&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;이때 Map의 Value로 저장되는 것은 ClusterDataPtr으로써 파싱된 결과를 ClusterData 데이터 구조로 만들고 이에 대한 포인터 값을 저장하는데, ClusterData가 보유한 내부 속성 중 중요한 속성 설명 및 생성 과정에 대해 조금 더 자세히 알아보겠습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qMYGx/btr0WfcaetD/EQUw2lJXkk3N9Fgn4qVYa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qMYGx/btr0WfcaetD/EQUw2lJXkk3N9Fgn4qVYa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qMYGx/btr0WfcaetD/EQUw2lJXkk3N9Fgn4qVYa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqMYGx%2Fbtr0WfcaetD%2FEQUw2lJXkk3N9Fgn4qVYa1%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;1504&quot; height=&quot;856&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;856&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;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;먼저 Parsing된 Cluster 정보를 토대로 ClusterData 인스턴스 생성을 위해 ProdClusterManagerFactory에게 처리를 위임합니다. 해당 Factory 내부에서는 다시 ClusterFactoryImplBase에게 Cluster 생성 주체를 위임합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cluster_factory_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1677557583331&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;std::pair&amp;lt;ClusterSharedPtr, ThreadAwareLoadBalancerPtr&amp;gt;
ClusterFactoryImplBase::create(Server::Configuration::ServerFactoryContext&amp;amp; server_context,
                               const envoy::config::cluster::v3::Cluster&amp;amp; cluster,
                               ClusterFactoryContext&amp;amp; context) {
  auto stats_scope = generateStatsScope(cluster, context.stats());
  std::unique_ptr&amp;lt;Server::Configuration::TransportSocketFactoryContextImpl&amp;gt;
      transport_factory_context =
          std::make_unique&amp;lt;Server::Configuration::TransportSocketFactoryContextImpl&amp;gt;(
              server_context, context.sslContextManager(), *stats_scope, context.clusterManager(),
              context.stats(), context.messageValidationVisitor());

  std::pair&amp;lt;ClusterImplBaseSharedPtr, ThreadAwareLoadBalancerPtr&amp;gt; new_cluster_pair =
      createClusterImpl(server_context, cluster, context, *transport_factory_context,
                        std::move(stats_scope));

  if (!cluster.health_checks().empty()) {
    if (cluster.health_checks().size() != 1) {
      throw EnvoyException(&quot;Multiple health checks not supported&quot;);
    } else {
      new_cluster_pair.first-&amp;gt;setHealthChecker(HealthCheckerFactory::create(
          cluster.health_checks()[0], *new_cluster_pair.first, context.runtime(),
          context.mainThreadDispatcher(), context.logManager(), context.messageValidationVisitor(),
          context.api()));
    }
  }

  new_cluster_pair.first-&amp;gt;setOutlierDetector(Outlier::DetectorImplFactory::createForCluster(
      *new_cluster_pair.first, cluster, context.mainThreadDispatcher(), context.runtime(),
      context.outlierEventLogger(), context.api().randomGenerator()));

  new_cluster_pair.first-&amp;gt;setTransportFactoryContext(std::move(transport_factory_context));
  return new_cluster_pair;
}&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;ClusterFactoryImplBase의 처리 과정을 살펴보기 위해 코드레벨로 확인해보겠습니다. ProdClusterManagerFactory로 부터 Cluster 생성 처리를 위임받은 ClusterFactoryImpl의 create 메소드내용을 살펴보면, Socket 관리, Cluster, HeathChecker, OutlierDetector를 생성하기 위해 각각의 Factory에게 처리를 위임하고 생성 결과를 전달받아 Cluster에 할당하는 것을 볼 수 있습니다. 참고로 여기서 생성되는 HealthCheker 및 OutlierDetector에 대한 자세한 설명은 조금 뒤에 다시 살펴보겠습니다.&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;위 코드에서 실제 Cluster 생성은 createClusterImpl 메소드내에서 이루어지며, 해당 메소드 내에서는 Proto 파일 정의에 따른 인스턴스 생성과 실제 Parsing 내용을 결합한 Cluster 인스턴스를 전달받습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1835&quot; data-origin-height=&quot;776&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qrK2A/btrX1O9Ouqy/r6avYiE0AUwOKTV4ViNmA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qrK2A/btrX1O9Ouqy/r6avYiE0AUwOKTV4ViNmA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qrK2A/btrX1O9Ouqy/r6avYiE0AUwOKTV4ViNmA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqrK2A%2FbtrX1O9Ouqy%2Fr6avYiE0AUwOKTV4ViNmA0%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;1835&quot; height=&quot;776&quot; data-origin-width=&quot;1835&quot; data-origin-height=&quot;776&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;Cluster 생성이 완료된 이후에는 Cluster 인스턴스와 내부에 Parsing된 정보를 토대로 생성된 HealthChecker, OutlierDetector, LoadBalancer가 존재할 것입니다. 이후에는 다음과 과정을 추가로 거칩니다.&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;1. healthChecker가 등록되어있다면, hostCheckCompleteCallback 함수를 등록시킵니다. 해당 함수는 host가 Health Check가 실패했을 때, 해당 정보를 Cluster Manager에게 전달하고, 결과적으로는 해당 Cluster가 Connection Pool에서 해제하도록 후속 작업을 수행하기 위한 용도로 사용됩니다.&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;2. outlierDector가 등록되어있다면, changeStateCallback 함수를 등록시킵니다. 해당 함수는 만약 host가 Outlier로 판정되고, ejection 상황이라면, 해당 정보를 Cluster Manager에게 전달하고, 결과적으로는 해당 Cluster가 Connection Pool에서 해제하도록 후속 작업을 수행하기 위한 용도로 사용됩니다.&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;3. Callback이 모두 등록이 완료되면, Cluster에 설정된 Load Balancer를 생성하기 위한 메타데이터를 등록합니다. 이때 등록되는 Load Balancer 타입은 lb_policy에 명시된 종류에 따라 다르게 생성되며, Round Robin, Least Request, Hash Ring과 같은 타입들을 지원합니다.&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;4. 생성된 Cluster 정보를 Cluster Manager가 관리하는 active_clusters_ 에 추가합니다.&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;위 네가지 과정을 거치게되면 ClusterData 설정은 마무리됩니다. 지금까지 static resource로 등록된 Cluster 처리 과정에 대해서 살펴봤습니다. 이번에는 CDS를 통해 dynamic으로 등록되는 resource 처리 과정에 대해서 살펴보겠습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAnMBA/btr3Qyz2qxn/OhypiHWgQZMIk5QYH9wiW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAnMBA/btr3Qyz2qxn/OhypiHWgQZMIk5QYH9wiW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAnMBA/btr3Qyz2qxn/OhypiHWgQZMIk5QYH9wiW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAnMBA%2Fbtr3Qyz2qxn%2FOhypiHWgQZMIk5QYH9wiW1%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;480&quot; height=&quot;503&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;759&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;Dynamic 설정의 경우에는 CDS를 통해 처리됨을 설명했습니다. 따라서 CDS 처리를 위해서는 Cluster Manager에서 관리하는 gRPC Multiplexer(ads_mux)와 Subscription Factory와의 상호작용이 필요합니다. 이를 토대로 CDS를 통한 동기화 과정을 살펴보면 다음과 같습니다.&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;1. CDS 등록을 위해 가장 먼저 Cluster Manager에 존재하는 Subscription Factory로부터 Subscription을 요청합니다.&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;2. Subscription Factory는 Subscription을 반환하기 CDS가 전달받기 희망하는 Resource 타입에 대한 데이터 요청 및 Callback 처리를 위해 Multiplexer에 이를 등록합니다.&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;3. CDS가 요청하는 Resource 타입 등록 및 해당 Subscription을 CDS에 반환합니다.&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;4. 외부에서 Cluster 변화(생성/삭제/수정)가 감지되어 Envoy에 통지되면 해당 내용이 Multiplexer를 지나 Callback을 통해 CDS로 전달될 것입니다. 이때 CDS 내부에서는 이를 처리하기 위해 CdsApiHelper에게 처리를 위임합니다.&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;5. CdsApiHelper에서는 해당 내용을 정제합니다. Cluster Manager에 위치한 active_clusters_에 해당 내용을 반영합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 기존 Cluster의 업데이트라면 해당 내용을 수정하고, 신규 추가라면 Cluster 정보를 active_clusters_에 추가할 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. Cluster Entry&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Static 방식과 Dynamic 방식을 통해서 Cluster Manager가 Cluster를 관리하기 위해 사용되는 active_clusters_에 Cluster 정보를 현행화하는 방식에 대해서 살펴봤습니다. 하지만 여기서 active_clusters_에서 값으로 관리되는 ClusterData는 Cluster의 역할 중 핵심 기능을 포함하고 있지 않습니다.&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;그렇다면 Cluster가 제공하는 핵심 기능은 무엇일까요?&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;1823&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btzBhZ/btr4vIvpEU5/uwP6c6h3DVuDtH0PW3pKKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btzBhZ/btr4vIvpEU5/uwP6c6h3DVuDtH0PW3pKKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btzBhZ/btr4vIvpEU5/uwP6c6h3DVuDtH0PW3pKKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtzBhZ%2Fbtr4vIvpEU5%2FuwP6c6h3DVuDtH0PW3pKKk%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;1823&quot; height=&quot;362&quot; data-origin-width=&quot;1823&quot; data-origin-height=&quot;362&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;Cluster 의 가장 중요한 기능 중 하나는 Client가 Cluster 내에 존재하는 여러 B/E host와의 연결을 시도할 때, 중간에서 Client의 연결과 B/E의 연결을 이어주는 Connection Pool 인터페이스 기능을 제공한다는 점입니다.&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; active_clusters_가 관리하는 ClusterData에서 이를 제공하지 않습니다. 따라서 Cluster Manager에서는 active_clusters_ 를 Cluster 변경이 생겼을 때(CDS) 관리하는 메타데이터 용도로써 내부적으로 관리하고 외부와의 Cluster 연결 등에는 별도 자료구조(thread_local_clusters_)와 ClusterData를 확장한 ClusterEntry를 통해서 Connection Pool 관리 기능과 Load Balancer 기능을 제공합니다.&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;따라서 이번에는 Static, Dynamic 방식을 통해서 active_clusters_에 등록된 이후 진행되는 후속 과정과 이를 통해서 생성되는 ClusterEntry 및 ClusterEntry를 관리하는 thread_local_clusters_ 에 대해서 살펴보겠습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1485&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhaoHl/btr34NhOi5F/hkkYe8DkczUOvKcCxt2um1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhaoHl/btr34NhOi5F/hkkYe8DkczUOvKcCxt2um1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhaoHl/btr34NhOi5F/hkkYe8DkczUOvKcCxt2um1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhaoHl%2Fbtr34NhOi5F%2FhkkYe8DkczUOvKcCxt2um1%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;457&quot; data-origin-width=&quot;1485&quot; data-origin-height=&quot;457&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;Cluster Manager를 구동하는 과정에서 수행하는 작업 중 하나는 Main 쓰레드에 존재하는 Dispatcher를 통해 TLS를 할당받는 것입니다. 이때 생성하는 Slot은 ThreadLocalClustManagerImpl 로써, 외부에 존재하는 모듈이나 내부에서 Cluster 데이터 동기화를 수행할 때 해당 Slot에 존재하는 인스턴스를 활용합니다.&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;그리고 Cluster Manager 내부에는 thread_local_clusters_라고 불리는 Map이 존재하는데, 해당 Map은 Key로써는 Cluster의 이름을 Value는 ClusterData를 기반으로 만들어진 ClusterEntry를 가지고 있습니다. 즉 이전에 설명했듯이 Connection Pool 기능과 실질적인 LoadBalancer 인스턴스를 지니고 있는 ClusterEntry를 해당 자료구조가 가지고 있으며, 외부 모듈에서는 thread_local_clusters_ 접근을 통해 Cluster Manager가 보유하고 있는 Cluster 정보에 대해 참조가 가능합니다.&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;그렇다면 Static, Dynamic 등록을 통해서 active_clusters_에 생성된 Cluster 정보를 기반으로 어떤 시점에 thread_local_clusters_를 만들어낼까요? ClusterEntry 생성과 thread_local_clusters_ 삽입 과정을 통해서 이해해보겠습니다. 먼저 Static Cluster 등록부터 살펴보겠습니다.&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;2425&quot; data-origin-height=&quot;794&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ6Hok/btr4mDsET64/sp8OJJizxafavYn9e3K8H0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ6Hok/btr4mDsET64/sp8OJJizxafavYn9e3K8H0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ6Hok/btr4mDsET64/sp8OJJizxafavYn9e3K8H0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ6Hok%2Fbtr4mDsET64%2Fsp8OJJizxafavYn9e3K8H0%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;2425&quot; height=&quot;794&quot; data-origin-width=&quot;2425&quot; data-origin-height=&quot;794&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;Static Resource가 모두 등록되면 해당 ClusterData는 모두 active_clusters_에 저장되어있을 것입니다. 이후 Cluster Manager 설정이 끝나게되면, active_clusters_를 순회하면서 초기화 작업을 진행합니다.&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;이때 수행하는 작업은 크게 4단계로 이루어져있습니다.&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;1. 개별 ClusterData를 순회하면서 Cluster에 멤버가 추가되거나 우선순위가 변경되었을 때 후속 작업을 수행하기 위한 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;2. 등록되어있는 Callback의 역할은 Cluster Manager가 여러개일 경우 개별 Cluster Manager에서 보유중인 local cluster를 업데이트 하는 것입니다. 따라서 Cluster Manager 동기화를 위해 TLS Slot에 저장된 ThreadLocalClusterManagerImpl을 참조하여 후속 작업 처리를 요청합니다.&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;3. Callback 등록이 완료되면, thread_local_clusters_에 Cluster를 생성하기 위해 TLS Slot에 저장된 ThreadLocalClusterManagerImpl를 참조하여 모든 Cluster Manager에게 Cluster 생성에 대한 내용을 통지하고&amp;nbsp; 이를 전달받은 Cluster Manager에서는 ClusterEntry를 생성하여 자신의 thread_local_clusters_에 저장합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1221&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCqSwD/btr4mCUQ2Tj/8hCTEjnHBcH4En8VYT7hC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCqSwD/btr4mCUQ2Tj/8hCTEjnHBcH4En8VYT7hC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCqSwD/btr4mCUQ2Tj/8hCTEjnHBcH4En8VYT7hC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCqSwD%2Fbtr4mCUQ2Tj%2F8hCTEjnHBcH4En8VYT7hC0%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;640&quot; height=&quot;314&quot; data-origin-width=&quot;1221&quot; data-origin-height=&quot;599&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;ClusterEntry는 ClusterData에 저장된 info 정보를 포함하고 있으며, 내부에는 클러스터 관리를 위한 여러 속성이 존재합니다. 그 중 몇개만 소개하자면, 먼저 Connection Pool에 접근할 수 있는 인터페이스 기능 제공을 포함합니다. 그리고 등록된 B/E의 host 및 우선순위를 관리하기 위한 PrioritySetIml이 있습니다. LoadBalancer는 해당 Cluster에 대해 외부에서 사용자 접속 요청이 들어왔을 때 실질적으로 LoadBalancer를 수행하기 위한 인스턴스가 매핑되며, AsyncClient를 통해서 Upstream과 연결을 수행합니다.&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;4. 만약 Cluster의 변경에 대하여 외부에서 통지를 받기위해 Callback을 등록했었다면, Cluster 변경에 대한 통지를 외부에게 알립니다.&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;위와 같은 과정을 거쳐 Static Resource로 등록한 자원들이 active_clusters_에 최초 저장이되고 이를 토대로 다시 전체 Cluster Manager에서 Cluster Entry를 만들고 이를 자신의 thread_local_clusters_에 저장함으로써 Cluster 관리가 이루어집니다. 또한 이후에는 외부에서 Cluster 접근을 요청할 때 thread_local_clusters_를 통해서 해당 Cluster 정보 참조가 가능해집니다.&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;1092&quot; data-origin-height=&quot;759&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buiUbo/btr4hlNACnH/AF6vBwqLK0jJGHbkyDUYc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buiUbo/btr4hlNACnH/AF6vBwqLK0jJGHbkyDUYc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buiUbo/btr4hlNACnH/AF6vBwqLK0jJGHbkyDUYc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuiUbo%2Fbtr4hlNACnH%2FAF6vBwqLK0jJGHbkyDUYc1%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;640&quot; height=&quot;445&quot; data-origin-width=&quot;1092&quot; data-origin-height=&quot;759&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;p data-ke-size=&quot;size16&quot;&gt;Dynamic Resource 등록의 경우 이전에 active_clusters_ 에 추가함을 설명했는데, 이 과정에서 Cluster 초기화를 후속 진행합니다. 이때 ClusterEntry가 생성되고 해당 데이터가 thread_local_clusters_에 추가되면서 Cluster Manager에서 Cluster 동기화가 이루어집니다.&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;5-2. Connection Pool 관리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Cluster Entry 내부에 있는 ConnectionPool 인스턴스를 통해서 Cluster Manager가 어떻게 Connection Pool을 관리하는지에 대해서 살펴보고자 합니다. 여기서 Connection Pool 관리는 Cluster Manager가 하며, 외부에서는 Cluster Entry를 통해 Cluster Manager에서 관리하는 Connection Pool 획득 및 해제등의 요청을 수행할 수 있습니다. 따라서 이 부분에 대해서 자세히 알아보도록 하겠습니다.&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;1953&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbMzTA/btr4xh4ZB71/4h4fR9prjOTSPGSpZVZkYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbMzTA/btr4xh4ZB71/4h4fR9prjOTSPGSpZVZkYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbMzTA/btr4xh4ZB71/4h4fR9prjOTSPGSpZVZkYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbMzTA%2Fbtr4xh4ZB71%2F4h4fR9prjOTSPGSpZVZkYK%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;1953&quot; height=&quot;410&quot; data-origin-width=&quot;1953&quot; data-origin-height=&quot;410&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;Cluster Manager 내부에는 Connection Pool을 관리하기 위한 map이 여러개 존재합니다. 그 중 host_http_conn_pool_map은 http 요청 관련 Connection Pool을 관리하며, 그 밖에 TCP 기반으로 Connection이 이루어지는 요청의 경우는 host_tcp_conn_pool_map을 통해 Connection Pool을 관리합니다. 본 포스팅에서는 Http 연결 요청에 대한 처리만을 다루고 있으므로 Cluster Manager에서 관리하는 host_http_conn_pool_map에 대해서만 설명하겠습니다.&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;host_http_conn_pool_map은 이름을 통해 알 수 있듯이 Map 자료구조입니다. 이는 HostPtr을 Key로 하며, Value는 Connection을 관리하기 위한 Container로 구성되어있습니다. 그리고 해당 Container 안에는 Connection Pool Map이 별도로 존재합니다.&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;따라서, 외부에서 Connection 연결을 시도하기 위해서는 먼저 host를 기반으로 host_http_conn_pool_map으로 부터 Container를 획득해야하며, 그 안에 존재하는 Connection Map을 통해 Connection 할당 및 해제를 수행할 수 있습니다.&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;그렇다면 Connection 할당 및 해제는 언제 어떻게 이루어질까요?&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;720&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y9H13/btr4G8ss71h/Bbr6XN6H1JYcarBBKp11UK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y9H13/btr4G8ss71h/Bbr6XN6H1JYcarBBKp11UK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y9H13/btr4G8ss71h/Bbr6XN6H1JYcarBBKp11UK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY9H13%2Fbtr4G8ss71h%2FBbr6XN6H1JYcarBBKp11UK%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;456&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;456&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;Envoy 관련 첫 포스팅 때, Envoy의 내부 흐름은 위와 같이 진행된다고 설명한 적이 있습니다. 여기서 Listener를 통해 Cluster에 접근하고 Load Balancing을 수행하는 과정에서 Cluster Manager를 통해 Endpoint 즉 host가 지정됩니다. 그리고 이 과정에서 Connection Pool로부터 Connection을 할당받아 요청을 전달할 수 있습니다. 정리하자면 Connection Pool로부터 Connection을 할당 받는 시점은 Load Balancing이 완료된 이후입니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;807&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3Skc6/btr5cDFJ1uc/Ge0nQbyBS68eL8QmlZGuuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3Skc6/btr5cDFJ1uc/Ge0nQbyBS68eL8QmlZGuuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3Skc6/btr5cDFJ1uc/Ge0nQbyBS68eL8QmlZGuuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3Skc6%2Fbtr5cDFJ1uc%2FGe0nQbyBS68eL8QmlZGuuK%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;1515&quot; height=&quot;807&quot; data-origin-width=&quot;1515&quot; data-origin-height=&quot;807&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;1. Listener를 거쳐 Router에 도달하게되면, Client의 요청이 어떤 Cluster에게 전달되어야하는지 이미 알고 있습니다. 따라서 Router에서는 Cluster Manager에게 자신이 접근하고자 하는 Cluster 정보를 요청합니다.&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;2. Cluster Manager는 자신이 보유하고 있는 thread_local_clusters_ 에서 Router가 요청한 Cluster 정보를 찾습니다.&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;cluster_manager_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679374564569&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ThreadLocalCluster* ClusterManagerImpl::getThreadLocalCluster(absl::string_view cluster) {
  ThreadLocalClusterManagerImpl&amp;amp; cluster_manager = *tls_;

  auto entry = cluster_manager.thread_local_clusters_.find(cluster);
  if (entry != cluster_manager.thread_local_clusters_.end()) {
    return entry-&amp;gt;second.get();
  } else {
    return nullptr;
  }
}&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;380&quot; data-origin-height=&quot;24&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XQ2nL/btr45mdm3GX/7okPDjznGrW9LiEaOYHpC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XQ2nL/btr45mdm3GX/7okPDjznGrW9LiEaOYHpC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XQ2nL/btr45mdm3GX/7okPDjznGrW9LiEaOYHpC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXQ2nL%2Fbtr45mdm3GX%2F7okPDjznGrW9LiEaOYHpC1%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;24&quot; data-origin-width=&quot;380&quot; data-origin-height=&quot;24&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;참고로 이때 thread_local_clusters_에 저장된 값은 ClusterEntry이지만, ClusterEntry는 ThreadLocalCluster를 상속받기 때문에 리턴 타입은 ThreadLocalCluster로 하여 ThreadLocalCluster에서 제공되는 메소드만 호출 가능합니다.&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;3. Cluster Manager로 부터 ThreadLocalCluster를 찾아서 Router로 반환합니다.&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;4. Router에서는 Cluster내에 존재하는 host를 통해 Connection 연결을 수행해야되기 때문에, 전달받은 ThreadLocalCluster를 통해 Connection Pool 할당을 요청합니다.&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;5. 이를 전달받은 ThreadLocalCluster(ClusterEntry)는 Cluster Manager에게 요청하여 host_http_conn_pool_map 에 할당 받은 Connection Pool이 존재하는지를 확인합니다. 만약 존재한다면 해당 Connection Pool Map에 있는 Container에 접근합니다. 반면 존재하지 않는다면 새로운 Container를 생성하여 해당 Map에 추가합니다.&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;7. Container 내부에는 Cluster 별로 Connection을 관리하는 Pool을 가지고 있습니다. 그리고 여기에서 Pool 존재여부를 최종적으로 확인하고, 해당 Pool을 반환합니다. 해당 과정을 코드로 살펴보면 다음과 같습니다.&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;conn_pool_map_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679375403383&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;template &amp;lt;typename KEY_TYPE, typename POOL_TYPE&amp;gt;
typename ConnPoolMap&amp;lt;KEY_TYPE, POOL_TYPE&amp;gt;::PoolOptRef
ConnPoolMap&amp;lt;KEY_TYPE, POOL_TYPE&amp;gt;::getPool(const KEY_TYPE&amp;amp; key, const PoolFactory&amp;amp; factory) {
  Common::AutoDebugRecursionChecker assert_not_in(recursion_checker_);

  auto pool_iter = active_pools_.find(key);
  if (pool_iter != active_pools_.end()) {
    return std::ref(*(pool_iter-&amp;gt;second));
  }
  ResourceLimit&amp;amp; connPoolResource = host_-&amp;gt;cluster().resourceManager(priority_).connectionPools();
  // We need a new pool. Check if we have room.
  if (!connPoolResource.canCreate()) {
    // We're full. Try to free up a pool. If we can't, bail out.
    if (!freeOnePool()) {
      host_-&amp;gt;cluster().stats().upstream_cx_pool_overflow_.inc();
      return absl::nullopt;
    }

    ...(중략)...
  }

  auto new_pool = factory();
  connPoolResource.inc();
  for (const auto&amp;amp; cb : cached_callbacks_) {
    new_pool-&amp;gt;addIdleCallback(cb);
  }

  auto inserted = active_pools_.emplace(key, std::move(new_pool));
  return std::ref(*inserted.first-&amp;gt;second);
}&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;active_pools_가 Container가 보유하고 있는 Pool을 의미합니다. 해당 과정을 살펴보면, 먼저 Container 내부에서 관리하는 active_pools_에서 해당 pool이 존재하는지를 찾습니다. 참고로 여기서 key 값은 protocol을 uint8_t로 캐스팅한 값입니다.&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;그리고 만약 값이 존재하지 않았을 때에는, 해당 Cluster가 Connection Pool을 생성이 가능한지 Resource Limit 설정 값을 살펴봅니다. 그리고 생성이 불가할 경우에는 위 코드와 같이 nullopt를 반환하는 것을 볼 수 있습니다.&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;8. 만약 Resource Limit 설정 값을 확인했을 경우 신규 Pool 생성이 가능한 경우에는 기존에 입력받은 factory 메소드로부터 신규로 Pool을 할당받고자 호출합니다.&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;cluster_manager_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679376113803&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Http::ConnectionPool::InstancePtr ProdClusterManagerFactory::allocateConnPool(
    Event::Dispatcher&amp;amp; dispatcher, HostConstSharedPtr host, ResourcePriority priority,
    std::vector&amp;lt;Http::Protocol&amp;gt;&amp;amp; protocols,
    const absl::optional&amp;lt;envoy::config::core::v3::AlternateProtocolsCacheOptions&amp;gt;&amp;amp;
        alternate_protocol_options,
    const Network::ConnectionSocket::OptionsSharedPtr&amp;amp; options,
    const Network::TransportSocketOptionsConstSharedPtr&amp;amp; transport_socket_options,
    TimeSource&amp;amp; source, ClusterConnectivityState&amp;amp; state, Http::PersistentQuicInfoPtr&amp;amp; quic_info) {

  Http::HttpServerPropertiesCacheSharedPtr alternate_protocols_cache;
  if (alternate_protocol_options.has_value()) {
    // If there is configuration for an alternate protocols cache, always create one.
    alternate_protocols_cache = alternate_protocols_cache_manager_-&amp;gt;getCache(
        alternate_protocol_options.value(), dispatcher);
  } else if (!alternate_protocol_options.has_value() &amp;amp;&amp;amp;
             (protocols.size() == 2 ||
              (protocols.size() == 1 &amp;amp;&amp;amp; protocols[0] == Http::Protocol::Http2)) &amp;amp;&amp;amp;
             Runtime::runtimeFeatureEnabled(
                 &quot;envoy.reloadable_features.allow_concurrency_for_alpn_pool&quot;)) {
    // If there is no configuration for an alternate protocols cache, still
    // create one if there's an HTTP/2 upstream (either explicitly, or for mixed
    // HTTP/1.1 and HTTP/2 pools) to track the max concurrent streams across
    // connections.
    envoy::config::core::v3::AlternateProtocolsCacheOptions default_options;
    default_options.set_name(host-&amp;gt;cluster().name());
    alternate_protocols_cache =
        alternate_protocols_cache_manager_-&amp;gt;getCache(default_options, dispatcher);
  }

  absl::optional&amp;lt;Http::HttpServerPropertiesCache::Origin&amp;gt; origin =
      getOrigin(transport_socket_options, host);
  if (protocols.size() == 3 &amp;amp;&amp;amp;
      context_.runtime().snapshot().featureEnabled(&quot;upstream.use_http3&quot;, 100)) {
    ASSERT(contains(protocols,
                    {Http::Protocol::Http11, Http::Protocol::Http2, Http::Protocol::Http3}));
    ASSERT(alternate_protocol_options.has_value());
    ASSERT(alternate_protocols_cache);
#ifdef ENVOY_ENABLE_QUIC
    Envoy::Http::ConnectivityGrid::ConnectivityOptions coptions{protocols};
    if (quic_info == nullptr) {
      quic_info = Quic::createPersistentQuicInfoForCluster(dispatcher, host-&amp;gt;cluster());
    }
    return std::make_unique&amp;lt;Http::ConnectivityGrid&amp;gt;(
        dispatcher, context_.api().randomGenerator(), host, priority, options,
        transport_socket_options, state, source, alternate_protocols_cache, coptions,
        quic_stat_names_, stats_, *quic_info);
#else
    (void)quic_info;
    // Should be blocked by configuration checking at an earlier point.
    PANIC(&quot;unexpected&quot;);
#endif
  }
  if (protocols.size() &amp;gt;= 2) {
    if (Runtime::runtimeFeatureEnabled(
            &quot;envoy.reloadable_features.allow_concurrency_for_alpn_pool&quot;) &amp;amp;&amp;amp;
        origin.has_value()) {
      ENVOY_BUG(origin.has_value(), &quot;Unable to determine origin for host &quot;);
      envoy::config::core::v3::AlternateProtocolsCacheOptions default_options;
      default_options.set_name(host-&amp;gt;cluster().name());
      alternate_protocols_cache =
          alternate_protocols_cache_manager_-&amp;gt;getCache(default_options, dispatcher);
    }

    ASSERT(contains(protocols, {Http::Protocol::Http11, Http::Protocol::Http2}));
    return std::make_unique&amp;lt;Http::HttpConnPoolImplMixed&amp;gt;(
        dispatcher, context_.api().randomGenerator(), host, priority, options,
        transport_socket_options, state, origin, alternate_protocols_cache);
  }
  if (protocols.size() == 1 &amp;amp;&amp;amp; protocols[0] == Http::Protocol::Http2 &amp;amp;&amp;amp;
      context_.runtime().snapshot().featureEnabled(&quot;upstream.use_http2&quot;, 100)) {
    return Http::Http2::allocateConnPool(dispatcher, context_.api().randomGenerator(), host,
                                         priority, options, transport_socket_options, state, origin,
                                         alternate_protocols_cache);
  }
  if (protocols.size() == 1 &amp;amp;&amp;amp; protocols[0] == Http::Protocol::Http3 &amp;amp;&amp;amp;
      context_.runtime().snapshot().featureEnabled(&quot;upstream.use_http3&quot;, 100)) {
#ifdef ENVOY_ENABLE_QUIC
    if (quic_info == nullptr) {
      quic_info = Quic::createPersistentQuicInfoForCluster(dispatcher, host-&amp;gt;cluster());
    }
    return Http::Http3::allocateConnPool(dispatcher, context_.api().randomGenerator(), host,
                                         priority, options, transport_socket_options, state,
                                         quic_stat_names_, {}, stats_, {}, *quic_info);
#else
    UNREFERENCED_PARAMETER(source);
    // Should be blocked by configuration checking at an earlier point.
    PANIC(&quot;unexpected&quot;);
#endif
  }
  ASSERT(protocols.size() == 1 &amp;amp;&amp;amp; protocols[0] == Http::Protocol::Http11);
  return Http::Http1::allocateConnPool(dispatcher, context_.api().randomGenerator(), host, priority,
                                       options, transport_socket_options, state);
}&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;여기서 할당된 factory 메소드는 ClusterManager에서 관리하는 allocateCoonPool이 최종적으로는 호출되며, Cluster Manager에서는 client가 요구하는 프로토콜이 무엇인지 확인한 다음에 해당 요청을 처리할 수 있는 Connection을 생성하여 반환합니다. 그리고 생성된 Pool을 Container가 보유하고 있는 active_pools_에 삽입합니다.&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;9. 생성된 Pool을 ThreadLocalCluster에 반환합니다.&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;10. Pool을 Router에게 반환합니다. 이후 해당 Router에서는 전달받은 Pool을 통해 Downstream과 Upstream과의 연결 작업을 요청할 수 있게됩니다.&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;위와 같이 10단계를 거치게되면, Router는 Connection Pool을 할당받아 요청을 처리할 수 있게되었음을 확인할 수 있습니다. 반면 Connection 해제는 Router로 전달되었던 Connection Pool을 통해 해제를 요청할 수 있습니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. ADS 관리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1675390652518&quot; class=&quot;dts&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dynamic_resources:
  lds_config:
    resource_api_version: V3
    ads: {}
  cds_config:
    resource_api_version: V3
    ads: {}
  ads_config:
    transport_api_version: V3
    api_type: GRPC
    grpc_services:
      envoy_grpc:
        cluster_name: envoy-control-xds&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;이전 포스팅에서 xDS API와 함께 ADS (Aggregate Discovery Service)에 대해서 설명했습니다. Cluster Manager의 또 다른 ADS를 생성하고 관리하는 것입니다. 즉 Cluster Manager에서는 xDS API를 위한 Subscription, gRPC Multiplxer 및 ADS 관리를 총체적으로 담당합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. Health Check 동작 원리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Static Resource 그리고 CDS를 통한 Cluster 추가에 대해서 살펴봤습니다. 이번에는 ClusterData를 생성하는 과정에서 Health Check, Outlier Detector가 어떻게 생성되고 동작하는지 살펴보겠습니다. 먼저 살펴볼 것은 Health Check 입니다.&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;1517&quot; data-origin-height=&quot;856&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buc8G3/btr1fPb8sJN/vRRCbG4rmZ3hawJ4yO0yS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buc8G3/btr1fPb8sJN/vRRCbG4rmZ3hawJ4yO0yS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buc8G3/btr1fPb8sJN/vRRCbG4rmZ3hawJ4yO0yS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbuc8G3%2Fbtr1fPb8sJN%2FvRRCbG4rmZ3hawJ4yO0yS0%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;1517&quot; height=&quot;856&quot; data-origin-width=&quot;1517&quot; data-origin-height=&quot;856&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;이전에 살펴봤듯이 ClusterData를 생성하는 과정에서 Outlier Detector, Health Chekcer, Load Balancer 등이 추가됨을 확인할 수 있습니다. 이때 Health Checker를 생성하는 역할은 HealthCheckerFactory가 담당하며, 이후 수행과정을 살펴보면 다음과 같습니다.&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;1278&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eaQRa8/btr0RKjre8M/UGjSJIOmnUQRaTRE49K1M1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eaQRa8/btr0RKjre8M/UGjSJIOmnUQRaTRE49K1M1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eaQRa8/btr0RKjre8M/UGjSJIOmnUQRaTRE49K1M1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeaQRa8%2Fbtr0RKjre8M%2FUGjSJIOmnUQRaTRE49K1M1%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;640&quot; height=&quot;221&quot; data-origin-width=&quot;1278&quot; data-origin-height=&quot;442&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;Config를 통해 전달받은 Cluster 내부에는 Http 방식 뿐만 아니라 Tcp, gRPC 혹은 Custom 방식의 Health Check를 지정할 수 있습니다. 따라서 HealthCheckerFactory에서는 사용자가 입력한 Config의 Heath Checker 방식을 확인하고 그에 걸맞는 Health Checker를 생성하도록 지정합니다. 본 포스팅에서는 Http 기반으로 생성했음을 가정하였음으로 ProdHttpHealthCheckerImpl을 생성할 것입니다.&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;1040&quot; data-origin-height=&quot;359&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm6kJl/btr4s3kjVgP/yflp7SPGUvHh2Wy6mrLZPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm6kJl/btr4s3kjVgP/yflp7SPGUvHh2Wy6mrLZPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm6kJl/btr4s3kjVgP/yflp7SPGUvHh2Wy6mrLZPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm6kJl%2Fbtr4s3kjVgP%2Fyflp7SPGUvHh2Wy6mrLZPk%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;640&quot; height=&quot;221&quot; data-origin-width=&quot;1040&quot; data-origin-height=&quot;359&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;생성된 Health Checker 인스턴스는 ClusterData의 HealthChecker에 바인딩되어 동작합니다.&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;1369&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhQys9/btr4sTWplGn/8KlrnpREctB2VP06rCYcFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhQys9/btr4sTWplGn/8KlrnpREctB2VP06rCYcFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhQys9/btr4sTWplGn/8KlrnpREctB2VP06rCYcFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhQys9%2Fbtr4sTWplGn%2F8KlrnpREctB2VP06rCYcFK%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;640&quot; height=&quot;202&quot; data-origin-width=&quot;1369&quot; data-origin-height=&quot;432&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;cluster_manager_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679027498373&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  if (new_cluster-&amp;gt;healthChecker() != nullptr) {
    new_cluster-&amp;gt;healthChecker()-&amp;gt;addHostCheckCompleteCb(
        [this](HostSharedPtr host, HealthTransition changed_state) {
          if (changed_state == HealthTransition::Changed &amp;amp;&amp;amp;
              host-&amp;gt;healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)) {
            postThreadLocalHealthFailure(host);
          }
        });
  }&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;Health Checker가 매핑되고 나면 Cluster Manager에서는 Health Checker가 정상적으로 매핑되어있는지를 확인합니다. 그리고 인스턴스가 존재할 경우에는 Health Checker에서 비정상 Health 대상으로 판단한 host에 대해서 ejection 수행을 위한 Callback을 등록합니다.&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;이때 postThreadLocalHealthFailure는 TLS를 통해서 Health가 실패했음을 전달하며, 이를 수신받은 Cluster Manager에서는 해당 host에 연결된 Connection Pool을 해제하는 작업을 수행합니다.&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;2301&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG36Ct/btr4gVB8xY7/BbR0WPVHoTjBGbGaOUqkNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG36Ct/btr4gVB8xY7/BbR0WPVHoTjBGbGaOUqkNK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG36Ct/btr4gVB8xY7/BbR0WPVHoTjBGbGaOUqkNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG36Ct%2Fbtr4gVB8xY7%2FBbR0WPVHoTjBGbGaOUqkNK%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;2301&quot; height=&quot;501&quot; data-origin-width=&quot;2301&quot; data-origin-height=&quot;501&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수행 과정을 살펴보면 위 그림과 같습니다.&lt;/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;1. Health Checker에서 주기적으로 Health Check를 수행하다가 이상이 발생하면, 내부 종료 작업을 먼저 거친 뒤에 등록된 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;2. Cluster Manager에서는 기존에 등록한 Callback 함수를 수행합니다. 이때 내부적으로 ThreadLocalClusterManagerImpl을 참조하여 해당 Host 이상 여부를 전체로 전파합니다.&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;3. Dispatcher를 통해 해당 내역을 전달받은 Cluster Manager에서는 Connection을 해제하기 위하여 Connection Pool에서 해당 host 내부에 존재하는 Connection들을 순차적으로 종료하고 해당 Pool 또한 Close 시킵니다.&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;해당 과정이 정상적으로 완료되면, Health Check에 따른 연결된 Client 및 host 또한 정상적으로 종료되는 것을 이해할 수 있습니다. 그렇다면 Health Check는 어떻게 이루어질까요? Health Check Factory로부터 만들어진 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;ProdHttpHealthCheckerImpl&lt;span&gt; 인스턴스의 구조와 수행 과정을 살펴보면서 조금 더 자세하게 알아보겠습니다.&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;&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;1834&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDzQ1j/btr1IrayYRu/kThap4HVnMgR4zzDbT2PwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDzQ1j/btr1IrayYRu/kThap4HVnMgR4zzDbT2PwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDzQ1j/btr1IrayYRu/kThap4HVnMgR4zzDbT2PwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDzQ1j%2Fbtr1IrayYRu%2FkThap4HVnMgR4zzDbT2PwK%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;1834&quot; height=&quot;694&quot; data-origin-width=&quot;1834&quot; data-origin-height=&quot;694&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;1. ProdHttpHealthCheckerImpl 생성자 내부에서 Cluster&amp;nbsp; 데이터변경이 발생했을 때 처리를 위한 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;2. 해당 Callback은 호출될 때 추가되는 host 정보와 삭제되는 host 정보를 같이 전달받는데, 이를 토대로 추가되는 host 정보를 active_sessions_ 에 Session을 만들어 추가합니다. 반대로 삭제되는 host 정보 또한 active_sessions_ 에서 삭제합니다.&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;3. 이때 추가되는 Session에는 Health Check를 정기적으로 수행하기 위해서 Dispatcher에 존재하는 libevent로 부터 Timer를 할당받습니다. 이때 생성되는 timer는 총 2개로 Health Check를 주기적으로 수행하기 위한 interval_timer_와 timeout을 판별하기 위한 timeout_timer_ 를 생성합니다.&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;여기서 Health Check는 Session 내에 생성되는 Timer에 의해 이루어지므로 Session 생성 과정을 보다 자세하게 살펴보겠습니다.&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;1831&quot; data-origin-height=&quot;1695&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/on3W7/btr1Qw333Sp/2x2zZf3P3ekZGm9KomILA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/on3W7/btr1Qw333Sp/2x2zZf3P3ekZGm9KomILA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/on3W7/btr1Qw333Sp/2x2zZf3P3ekZGm9KomILA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fon3W7%2Fbtr1Qw333Sp%2F2x2zZf3P3ekZGm9KomILA1%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;1831&quot; height=&quot;1695&quot; data-origin-width=&quot;1831&quot; data-origin-height=&quot;1695&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;p data-ke-size=&quot;size16&quot;&gt;onInterval은 HealthCheck를 위해 주기적으로 호출되는 메소드로써, 아래와 같은 4가지 단계를 거칩니다.&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;1. client_가 존재하는지를 살펴보고 client_가 존재하지 않는다면 최초 실행임을 의미하므로 Connection을 생성합니다. 이때 생성된 Connection은 CodecClient를 통해 Wrapping 되는데, CodecClient는 HTTP Codec 타입에 따라서 생성되는 ClientConnectionImpl 인스턴스 타입입니다.&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;2. Stream을 생성합니다.&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;3. Health Check 관련 Header 설정을 진행합니다. 이때 method 및 path 등은 Health Check 설정으로 입력된 값을 따릅니다.&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;4. Upstream 설정 및 Health Check 처리를 위한 encoding 처리를 담당합니다.&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;위 과정이 마무리되면, 주기에 맞추어 Health Check 요청을 HTTP로 전달할 것입니다. 그렇다면 HTTP 요청에 대한 응답은 어떻게 처리될까요?&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;751&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nY2wc/btr1CLaTcle/I5LeYWecxoXP5WLWu58qF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nY2wc/btr1CLaTcle/I5LeYWecxoXP5WLWu58qF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nY2wc/btr1CLaTcle/I5LeYWecxoXP5WLWu58qF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnY2wc%2Fbtr1CLaTcle%2FI5LeYWecxoXP5WLWu58qF0%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;751&quot; height=&quot;100&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;100&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;HttpActiveHealthCheckSession의 구조를 보면, 위와 같이 여러 Class가 상속되어있음을 확인할 수 있습니다. 그 중 요청 응답 처리는 ResponseDecoder 내부 정의된 I/F에 의해서 호출됩니다. 참고로 해당 과정에서 호출되는 메소드는 decodeHeaders와 decodeData 입니다. 따라서 HttpActiveHealthCheckSession 내부에 정의된 하기 2개의 메소드가 호출됩니다.&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;health_checker_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1677820797167&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::decodeHeaders(
    Http::ResponseHeaderMapPtr&amp;amp;&amp;amp; headers, bool end_stream) {
  ASSERT(!response_headers_);
  response_headers_ = std::move(headers);
  if (end_stream) {
    onResponseComplete();
  }
}

void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::decodeData(Buffer::Instance&amp;amp; data,
                                                                     bool end_stream) {
  if (parent_.response_buffer_size_ != 0) {
    if (!parent_.receive_bytes_.empty() &amp;amp;&amp;amp;
        response_body_-&amp;gt;length() &amp;lt; parent_.response_buffer_size_) {
      response_body_-&amp;gt;move(data, parent_.response_buffer_size_ - response_body_-&amp;gt;length());
    }
  } else {
    if (!parent_.receive_bytes_.empty()) {
      response_body_-&amp;gt;move(data, data.length());
    }
  }

  if (end_stream) {
    onResponseComplete();
  }
}&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;위 메소드에서 살펴볼 것은 data가 전달이 완료되지 않았을 경우에는 response_body 데이터를 지속 수신받는 것을 알 수 있고 stream이 종료되었을 경우에는 onResponseComplete() 메소드가 호출됨을 알 수 있습니다. 즉 Health Check를 결정하는 요인은 onResponseComplete() 메소드 내부에서 이루어짐을 짐작할 수 있습니다.&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;health_checker_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1677821237737&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onResponseComplete() {
  request_in_flight_ = false;

  switch (healthCheckResult()) {
  case HealthCheckResult::Succeeded:
    handleSuccess(false);
    break;
  case HealthCheckResult::Degraded:
    handleSuccess(true);
    break;
  case HealthCheckResult::Failed:
    handleFailure(envoy::data::core::v3::ACTIVE, /*retriable=*/false);
    break;
  case HealthCheckResult::Retriable:
    handleFailure(envoy::data::core::v3::ACTIVE, /*retriable=*/true);
    break;
  }

  if (shouldClose()) {
    client_-&amp;gt;close();
  }

  response_headers_.reset();
  response_body_-&amp;gt;drain(response_body_-&amp;gt;length());
}&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;onResponseComplete() 메소드 내부를 살펴보면, healthCheck에 대한 결과를 기점으로 Switch로 분기하여 각기 다른 결과를 호출하는 것을 볼 수 있습니다. 만약 Failure가 발생한다면, 해당 timer를 disabled 시키고 종료처리할 것이며, shouldClose() 호출로 인해 client 연결 또한 종료됩니다.&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;만약 정상적으로 처리되었다면, 지속적으로 Health Check를 주기적으로 반복하여 정상여부 확인을 반복합니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Outlier Detection 동작 원리&lt;/h2&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;1517&quot; data-origin-height=&quot;869&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nj7gH/btr4fUpACZ3/PbhntoKOdyn6KFte238G81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nj7gH/btr4fUpACZ3/PbhntoKOdyn6KFte238G81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nj7gH/btr4fUpACZ3/PbhntoKOdyn6KFte238G81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fnj7gH%2Fbtr4fUpACZ3%2FPbhntoKOdyn6KFte238G81%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;1517&quot; height=&quot;869&quot; data-origin-width=&quot;1517&quot; data-origin-height=&quot;869&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;지금까지 Health Checker의 생성 및 동작과정에 대해서 살펴봤다면, 이번에는 Outlier Detector가 생성되는 과정과 동작 방법에 대해 살펴보겠습니다.&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;996&quot; data-origin-height=&quot;359&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZMyUU/btr4hP11g5i/P4geHGbsUb8S88XKsbsjsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZMyUU/btr4hP11g5i/P4geHGbsUb8S88XKsbsjsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZMyUU/btr4hP11g5i/P4geHGbsUb8S88XKsbsjsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZMyUU%2Fbtr4hP11g5i%2FP4geHGbsUb8S88XKsbsjsK%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;640&quot; height=&quot;231&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;359&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;Outlier Detector는 ClusterData 생성 당시에 DetectorImplFactory에 의해서 생성됩니다. 이때 생성되는 Outlier Detector의 인스턴스는 DetectorImpl이며 해당 인스턴스가 ClusterData의 OutlierDetector에 바인딩되어 동작합니다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;outlier_detection_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679022047694&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DetectorSharedPtr DetectorImplFactory::createForCluster(
    Cluster&amp;amp; cluster, const envoy::config::cluster::v3::Cluster&amp;amp; cluster_config,
    Event::Dispatcher&amp;amp; dispatcher, Runtime::Loader&amp;amp; runtime, EventLoggerSharedPtr event_logger,
    Random::RandomGenerator&amp;amp; random) {
  if (cluster_config.has_outlier_detection()) {

    return DetectorImpl::create(cluster, cluster_config.outlier_detection(), dispatcher, runtime,
                                dispatcher.timeSource(), std::move(event_logger), random);
  } else {
    return nullptr;
  }
}&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;다만 위 코드와 같이 모든 ClusterData에서 OutlierDetector를 생성하는 것은 아니며, Static 혹은 Dynamic Resource 내에 outlier_detection 설정이 활성화되어있는 경우에만 생성됩니다. 만약 해당 설정이 비활성화 되어있다면, OutlierDetector는 nullptr이 매핑됩니다.&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;1369&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XRzhL/btr4igyFv0H/yOsiqAE83L3BiBM6s0oPw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XRzhL/btr4igyFv0H/yOsiqAE83L3BiBM6s0oPw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XRzhL/btr4igyFv0H/yOsiqAE83L3BiBM6s0oPw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXRzhL%2Fbtr4igyFv0H%2FyOsiqAE83L3BiBM6s0oPw0%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;640&quot; height=&quot;202&quot; data-origin-width=&quot;1369&quot; data-origin-height=&quot;432&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;cluster_manager_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679026883711&quot; class=&quot;xl&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  if (new_cluster-&amp;gt;outlierDetector() != nullptr) {
    new_cluster-&amp;gt;outlierDetector()-&amp;gt;addChangedStateCb([this](HostSharedPtr host) {
      if (host-&amp;gt;healthFlagGet(Host::HealthFlag::FAILED_OUTLIER_CHECK)) {
        ENVOY_LOG_EVENT(debug, &quot;outlier_detection_ejection&quot;,
                        &quot;host {} in cluster {} was ejected by the outlier detector&quot;,
                        host-&amp;gt;address()-&amp;gt;asStringView(), host-&amp;gt;cluster().name());
        postThreadLocalHealthFailure(host);
      }
    });
  }&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;OutlierDetector가 매핑되고 나면 Cluster Manager에서는 Outlier Detector가 정상적으로 매핑되어있는지를 확인합니다. 그리고 인스턴스가 존재할 경우에는 Outlier Detector에서 Ejection 대상으로 판단한 host에 대해서 ejection 수행을 위한 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;이때 postThreadLocalHealthFailure는 TLS를 통해서 Health가 실패했음을 전달하며, 이를 수신받은 Cluster Manager에서는 해당 host에 연결된 Connection Pool을 해제하는 작업을 수행합니다. 해당 작업은 Health Check를 수행하면서 실패했을 때 Connection 해제하는 과정과 완벽하게 동일합니다.&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;지금까지 Outlier Detector 설정 관련해서 살펴봤습니다. 이번에는 실제 Outlier Detector가 수행하는 기능에 대해서 조금 더 자세하게 살펴보기 위해 Outlier로 등록되는 DetectorImpl 구조 및 동작 원리에 대해서 살펴보겠습니다.&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;1602&quot; data-origin-height=&quot;986&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pE9aM/btr4wMv0gIG/014rGu7UPbopwFo6q0CnQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pE9aM/btr4wMv0gIG/014rGu7UPbopwFo6q0CnQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pE9aM/btr4wMv0gIG/014rGu7UPbopwFo6q0CnQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpE9aM%2Fbtr4wMv0gIG%2F014rGu7UPbopwFo6q0CnQ0%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;1602&quot; height=&quot;986&quot; data-origin-width=&quot;1602&quot; data-origin-height=&quot;986&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;이를 위해 DetectorImpl을 생성 과정에 대해서 상세하게 짚어보겠습니다.&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;1. DetectorImpl 내부에서도 정기적으로 Cluster 내부에 존재하는 host들의 상태를 살펴보고 Outlier Detection을 지정한 임계치를 넘어가는 host를 점검하기 위해 Dispatcher로 부터 Timer를 할당받아 interval_timer_에 지정하는 작업을 합니다.&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;2. 그 다음은 외부에서 Cluster 내부 host가 추가되거나 삭제되는 등의 작업이 발생했을 때 Outlier Detector에서도 이를 감지하고 설정을 변경해야되기 때문에 ClusterEntry 내부에 존재하는 PriortySet에 Callback을 추가합니다. 해당 Callback 등록을 통해 실질적으로 Cluster 내부 host 변경이 발생했을 경우 이를 Detector가 통지받아 후속 작업을 수행할 수 있습니다. Callback을 전달받으면, 내부에서는 hosts를 관리하는 host_monitors_ Map 자료구조에 해당 hosts의 변경사항을 기록합니다. 이때 host 별로 DetectorHostMonitorImpl 인스턴스를 생성하여 Outlier Detection을 위한 기본 속성을 저장하는데, 이는 잠시 후 다시 살펴보겠습니다.&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;3. DetectorImpl 생성이 완료되면, ClusterData OutlierDetector에 매핑합니다.&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;4. ClusterData를 active_clusters_에 추가합니다.&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;5. ClusterManager에서 해당 OutlierDetector에게 Callback을 등록합니다. 이는 Outlier Detection 판정이 된 host에 대해서 Client 연결 종료 등의 후속작업을 처리하기 위함입니다.&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;위와 같은 과정을 거치고나면, Outlier Detector는 본격적으로 본인의 역할을 수행할 수 있습니다. 이번에는 초기화가 완료된 이후 후속 작업 진행 절차에 대해서 살펴보겠습니다.&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;outlier_detection_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679043718087&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void DetectorImpl::armIntervalTimer() {
  interval_timer_-&amp;gt;enableTimer(std::chrono::milliseconds(
      runtime_.snapshot().getInteger(IntervalMsRuntime, config_.intervalMs())));
}&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;먼저 초기화 작업이 끝나고나서 가장 먼저 수행하는 작업은 timer를 활성화 시키는 것입니다. 이는 위 메소드를 호출하여 실행되며, interval_timer_의 수행 주기는 위와 같이 config에 지정된 값을 활용하는 것을 볼 수 있습니다. 그렇다면, interval_timer_를 활성화 시켰을 때 어떤 작업을 수행할까요?&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;outlier_detection_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679043397336&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DetectorImpl::DetectorImpl(const Cluster&amp;amp; cluster,
                           const envoy::config::cluster::v3::OutlierDetection&amp;amp; config,
                           Event::Dispatcher&amp;amp; dispatcher, Runtime::Loader&amp;amp; runtime,
                           TimeSource&amp;amp; time_source, EventLoggerSharedPtr event_logger,
                           Random::RandomGenerator&amp;amp; random)
    : config_(config), dispatcher_(dispatcher), runtime_(runtime), time_source_(time_source),
      stats_(generateStats(cluster.info()-&amp;gt;statsScope())),
      interval_timer_(dispatcher.createTimer([this]() -&amp;gt; void { onIntervalTimer(); })),
      event_logger_(event_logger), random_generator_(random) {
  // Insert success rate initial numbers for each type of SR detector
  external_origin_sr_num_ = {-1, -1};
  local_origin_sr_num_ = {-1, -1};
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 위와 같이 해당 DetectorImpl의 생성자 코드를 살펴보면 힌트를 얻을 수 있습니다. 내용을 살펴보면 dispatcher로부터 timer를 생성받고나서 해당 timer가 호출해야되는 callback이 지정된 것을 확인할 수 있습니다. 이는&amp;nbsp; 위와 같이 onIntervalTimer() 메소드가 지정되어있습니다. 따라서 주기적으로 onIntervalTimer() 메소드가 호출됨으로써 Outlier Detection 작업을 수행함을 알 수 있습니다.&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;outlier_detection_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679043541502&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void DetectorImpl::onIntervalTimer() {
  MonotonicTime now = time_source_.monotonicTime();

  for (auto host : host_monitors_) {
    checkHostForUneject(host.first, host.second, now);

    // Need to update the writer bucket to keep the data valid.
    host.second-&amp;gt;updateCurrentSuccessRateBucket();
    // Refresh host success rate stat for the /clusters endpoint. If there is a new valid value, it
    // will get updated in processSuccessRateEjections().
    host.second-&amp;gt;successRate(DetectorHostMonitor::SuccessRateMonitorType::LocalOrigin, -1);
    host.second-&amp;gt;successRate(DetectorHostMonitor::SuccessRateMonitorType::ExternalOrigin, -1);
  }

  processSuccessRateEjections(DetectorHostMonitor::SuccessRateMonitorType::ExternalOrigin);
  processSuccessRateEjections(DetectorHostMonitor::SuccessRateMonitorType::LocalOrigin);

  armIntervalTimer();
}&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;위 코드와 같이 host_monitors_에 매핑된 전체 host들에 대해서 순회하면서 ejection 여부를 판단합니다. 만약 ejection이 결정되었다면, 해당 host_monitors_에 저장된 Monitor 정보를 초기화하고 ejection을 수행합니다. 또한 외부로부터 등록된 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;&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;975&quot; data-origin-height=&quot;789&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmImNL/btr41vbBl2F/dRKUDKOeH9BGec9GIq4taK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmImNL/btr41vbBl2F/dRKUDKOeH9BGec9GIq4taK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmImNL/btr41vbBl2F/dRKUDKOeH9BGec9GIq4taK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmImNL%2Fbtr41vbBl2F%2FdRKUDKOeH9BGec9GIq4taK%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;640&quot; height=&quot;518&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;789&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;참고로 host_monitors_에 매핑된 인스턴스는 DetectorHostMonitorImpl 인스턴스이며, 해당 인스턴스 내부에는 Outlier Detection을 판별하기 위한 여러가지 내부 속성 프로퍼티 값들이 존재합니다. 해당 프로퍼티 값은 Client가 Cluster Manager로부터 Connection Pool을 받아 Upstream host에 Http 요청을 완료한 이후에 전달받은 응답값을 토대로 Router가 갱신을 수행합니다.&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;따라서 Outlier Detector 내부에서 onIntervalTimer()가 수행될 때 해당 Monitor 인스턴스 내부 속성 값을 기준으로 ejection 여부를 판별할 수 있습니다.&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;outlier_detection_impl.cc&lt;/p&gt;
&lt;pre id=&quot;code_1679044232523&quot; class=&quot;roboconf&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void DetectorImpl::armIntervalTimer() {
  interval_timer_-&amp;gt;enableTimer(std::chrono::milliseconds(
      runtime_.snapshot().getInteger(IntervalMsRuntime, config_.intervalMs())));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 host_monitors_ 순회가 모두 끝나고 나면 다시 timer를 활성화 시켜 다음 outlier detection 처리를 반복 수행함으로써 지속적인 Outlier Detection 확인이 가능합니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 Cluster Manager의 기능에 대해 일부 살펴봤습니다. Cluster Manager의 가장 중요한 것은 Cluster 관리입니다. 다만 그 외에도 ads 관리 및 subscription factory 등을 담당하는 것을 살펴봤습니다.&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;다음 포스팅에서는 Cluster에 지정되는 Load Balancer 종류에 대해 개념적으로 살펴보도록 하겠습니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <category>cluster manager</category>
      <category>Envoy</category>
      <category>envoy architecture</category>
      <category>envoy 아키텍처</category>
      <category>Istio</category>
      <category>istio architecture</category>
      <category>istio 아키텍처</category>
      <category>서비스메시</category>
      <category>이스티오</category>
      <category>인보이</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/216</guid>
      <comments>https://cla9.tistory.com/216#entry216comment</comments>
      <pubDate>Thu, 18 May 2023 11:44:49 +0900</pubDate>
    </item>
    <item>
      <title>2. [envoy-internals] 쓰레딩 모델과 데이터 공유</title>
      <link>https://cla9.tistory.com/212</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서는 envoy를 이해하는데 아주 기초적인 컴포넌트 종류에 대한 설명을 진행했습니다. 이번 포스팅에서는 envoy 내부 여러 쓰레드가 존재하는데, 쓰레드간에 데이터는 어떻게 이루어지는지 그리고 쓰레딩 모델은 어떻게 구성되어있는지에 대해서 살펴보겠습니다. 본격적으로 진행하기 앞서 envoy는 멀티 쓰레딩 모델이지만 내부적으로 Lock을 통해 데이터 동기화를 수행하지 않습니다. 어떻게 Lock 없이 데이터 동기화가 가능할까요? envoy 쓰레딩 모델을 학습함으로써&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Threading Model&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Envoy 내부에는 전체를 관장하는 Main 쓰레드와 Network 요청과 라우팅을 처리하기 위한 여러 Worker 쓰레드가 존재합니다. 또한 그 외에도 파일 동기화를 위해 별도 File Flush 쓰레드가 별도로 존재합니다.&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;정리하자면 envoy 내부에는 Main, Worker, File Flush 쓰레드 총 3가지로 구성되었다고 볼 수 있습니다.&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;1202&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYVXad/btrPx1pnNuZ/qxSfJskekUOi6KTXVgoa31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYVXad/btrPx1pnNuZ/qxSfJskekUOi6KTXVgoa31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYVXad/btrPx1pnNuZ/qxSfJskekUOi6KTXVgoa31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYVXad%2FbtrPx1pnNuZ%2FqxSfJskekUOi6KTXVgoa31%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;1202&quot; height=&quot;248&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;248&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;여기서 Main 쓰레드는 envoy 내부의 xDS 정보를 관리하고, Admin Server, Health Check 등 Envoy 프로세스가 수행하는데 있어 중추적인 데이터를 관리하고 부가적인 Admin 기능을 제공합니다. 따라서 Envoy를 기동하는데 있어 주요 Metadata 정보는 Main 쓰레드에서 생성되며, 필요 시 다른 쓰레드로 해당 정보를 공유합니다.&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;그렇다면 envoy 내부에서 Main 쓰레드와 다수로 생성되는 Worker 쓰레드간의 데이터 공유가 필요할 경우에는 어떻게 해야할까요?&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;975&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0adHJ/btrPzCICqVl/brKboKHCYAAKN2PUN2Dxvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0adHJ/btrPzCICqVl/brKboKHCYAAKN2PUN2Dxvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0adHJ/btrPzCICqVl/brKboKHCYAAKN2PUN2Dxvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0adHJ%2FbtrPzCICqVl%2FbrKboKHCYAAKN2PUN2Dxvk%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;640&quot; height=&quot;163&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;248&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;첫 번째로 생각해볼 수 있는 방법은 쓰레드간 공유 Data를 생성한 다음에 Main 쓰레드에서 데이터 변경이 필요한 경우 Worker 쓰레드가 해당 공유 Data 접근이 모두 끝나는 시점에 Exclusive Lock을 걸어 Worker 쓰레드의 접근을 일시적으로 차단한 다음 데이터를 변경하여 쓰레드 안정성을 확보하는 방법입니다. 하지만 위와 같은 방식은 동시성이 떨어지는 단점이 존재합니다.&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;1116&quot; data-origin-height=&quot;433&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nGFiF/btrPyGSoPn9/6Ckv2szxRlVNVshfImVikK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nGFiF/btrPyGSoPn9/6Ckv2szxRlVNVshfImVikK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nGFiF/btrPyGSoPn9/6Ckv2szxRlVNVshfImVikK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnGFiF%2FbtrPyGSoPn9%2F6Ckv2szxRlVNVshfImVikK%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;640&quot; height=&quot;248&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;433&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;두 번째 방법은 모든 쓰레드에 걸쳐 동일한 데이터를 가지고 있는 상태에서 Data 변경이 발생하면, Main 쓰레드에서 데이터를 변경한 다음 모든 Worker 쓰레드에 수정된 Data를 전달하는 것입니다. 이 경우에는 특정 Worker 쓰레드가 바라보고 있는 원본 Data에 대해 Main 쓰레드에서 수정을 가해도 Worker 쓰레드가 바라보는 원본 Data는 해당 시점에 변경되지 않기 때문에 쓰레드 안정성을 확보할 수 있습니다.&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;위와 같이 설계된 디자인에서는 쓰레드 외부간에는 데이터가 공유되지 않지만, 내부에서는 해당 데이터를 전역적으로 공유해서 사용할 수 있습니다. 이러한 기법을 Thread Local이라고 부릅니다.&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;1582&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwT9S2/btrPAKe58yK/Dl0DSTfMhIn1cUUOabf651/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwT9S2/btrPAKe58yK/Dl0DSTfMhIn1cUUOabf651/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwT9S2/btrPAKe58yK/Dl0DSTfMhIn1cUUOabf651/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwT9S2%2FbtrPAKe58yK%2FDl0DSTfMhIn1cUUOabf651%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;1582&quot; height=&quot;405&quot; data-origin-width=&quot;1582&quot; data-origin-height=&quot;405&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;다만 위 예시에서는 1개의 데이터에 대한 동기화 과정을 표현했는데, envoy 내부에는 Main과 Worker 쓰레드 간의 xDS 업데이트를 위해 사용되는 데이터 항목이 많습니다. 따라서 실제로는 위 그림과 같이 하나의 데이터 인스턴스가 아니라 여러개의 Vector 자료 구조로 개별 쓰레드 별로 관리되어야 할 것입니다.&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;위 그림과 같이 데이터를 관리하면, Lock 없이 쓰레드 안정성을 다소 확보하는 것을 확인할 수 있었습니다. 그렇다면 여기에는 비효율이 없을까요?&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;size16&quot;&gt;따라서 envoy에서는 이를 해결 하기 위해서 Shared Pointer와 자체 TLS(Thread Local Storage)를 구축하였습니다. 이와 관련해는 조금 뒤에 더 자세히 다루어보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Distpatcher&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Envoy의 TLS(Thread Local Storage)를 살펴보기 위해서는 내부의 중추적인 역할을 수행하는 Dispatcher에 대해서 알아야합니다. 따라서 TLS를 다루기 이전에 Dispatcher에 대해서 알아보고 넘어가겠습니다.&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;이전에 Threading Model을 통해서 메인 쓰레드와 Worker 쓰레드 사이에 데이터 공유가 이루어졌음을 확인했습니다. 그렇다면 Worker 쓰레드 입장에서는 메인 쓰레드가 보낸 데이터를 어떻게 확인하고 후속 작업을 처리할까요?&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;775&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tNPh2/btrPQNY3tOS/gh1RNHVtQfVAfTWHsjJSik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tNPh2/btrPQNY3tOS/gh1RNHVtQfVAfTWHsjJSik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tNPh2/btrPQNY3tOS/gh1RNHVtQfVAfTWHsjJSik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtNPh2%2FbtrPQNY3tOS%2Fgh1RNHVtQfVAfTWHsjJSik%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;480&quot; height=&quot;61&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;99&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;envoy에서는 Dispatcher가 중간 매개체 역할을 해줍니다. 여기서 Dispatcher는 Main 쓰레드에서 보낸 데이터를 수신받으면, 이를 감지하고 Worker 쓰레드에게 통지하여 후속 작업이 진행될 수 있도록 가교 역할을 수행합니다.&amp;nbsp;&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;1175&quot; data-origin-height=&quot;373&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2G7Ej/btrPUnMtF6z/eTLsRKBZJYqoI2yIi4Qi0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2G7Ej/btrPUnMtF6z/eTLsRKBZJYqoI2yIi4Qi0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2G7Ej/btrPUnMtF6z/eTLsRKBZJYqoI2yIi4Qi0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2G7Ej%2FbtrPUnMtF6z%2FeTLsRKBZJYqoI2yIi4Qi0K%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;1175&quot; height=&quot;373&quot; data-origin-width=&quot;1175&quot; data-origin-height=&quot;373&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;해당 과정을 조금 더 자세히 살펴보면 위 그림과 같습니다.&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;envoy에서 Main 쓰레드를 포함하여 모든 쓰레드에는 Dispatcher가 존재합니다. 해당 Dispatcher는 자체적인 event loop를 가지고 있어서 별도의 life cycle이 존재합니다.&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;이 경우 가령 위 그림과 같이 Main 쓰레드에 Event가 발행할 경우 Main 쓰레드는 개별 Worker 쓰레드에 존재하는 Dispatcher에게 Event의 Callback 내용을 전달합니다. Worker 쓰레드에는 전달된 Event를 내부 Buffer에 저장하며, 저장된 Buffer에서 하나씩 Event를 추출하여 작업을 수행합니다.&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;그렇다면 Dispatcher의 내부는 어떻게 구성되어있을까요?&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;256&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bucACs/btrPRCiBOrF/19GalLyuL0INqUqRxwJ3cK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bucACs/btrPRCiBOrF/19GalLyuL0INqUqRxwJ3cK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bucACs/btrPRCiBOrF/19GalLyuL0INqUqRxwJ3cK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbucACs%2FbtrPRCiBOrF%2F19GalLyuL0INqUqRxwJ3cK%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;256&quot; height=&quot;316&quot; data-origin-width=&quot;256&quot; data-origin-height=&quot;316&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;envoy Dispatcher 내부에는 자체적인 event loop가 있다고 했는데, 해당 기능은 자체 구현하지 않고 C언어의 라이브러리인 libevent를 사용합니다. 다만 libevent가 C언어로 구성되어있기 때문에 메모리 관리를 위해 libevent에서 제공하는 자료구조를 unique_ptr 스마트 포인터로 감싸서 자동으로 관리할 수 있도록 Wrapping 하였습니다. 따라서 Dispatcher의 핵심은 libevent라고 볼 수 있습니다.&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;libevent 라이브러리는 다양한 기능을 제공하는데, 핵심 기능은 event가 발생되었을 때 이를 감지하고 알림을 전달해주는 역할을 수행합니다. libevent 공식 홈페이지를 보면 제공하는 기능에 대해서 보다 상세하게 소개되어있으며, 이에 대해서는 아래 공식 홈페이지를 참조하시기 바랍니다.&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;a href=&quot;https://libevent.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://libevent.org/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1667187074809&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;libevent&quot; data-og-description=&quot;The libevent API provides a mechanism to execute a callback function when a specific event occurs on a file descriptor or after a timeout has been reached. Furthermore, libevent also support callbacks due to signals or regular timeouts. libevent is meant t&quot; data-og-host=&quot;libevent.org&quot; data-og-source-url=&quot;https://libevent.org/&quot; data-og-url=&quot;https://libevent.org/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bIzCDQ/hyQpw0LJrs/jKArxu0Qkk2YaYD0IBivJK/img.jpg?width=271&amp;amp;height=209&amp;amp;face=0_0_271_209&quot;&gt;&lt;a href=&quot;https://libevent.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://libevent.org/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bIzCDQ/hyQpw0LJrs/jKArxu0Qkk2YaYD0IBivJK/img.jpg?width=271&amp;amp;height=209&amp;amp;face=0_0_271_209');&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;libevent&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;The libevent API provides a mechanism to execute a callback function when a specific event occurs on a file descriptor or after a timeout has been reached. Furthermore, libevent also support callbacks due to signals or regular timeouts. libevent is meant t&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;libevent.org&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;지금까지 Dispatcher를 통한 Event 전달 과정과 해당 기능을 수행하는 libevent에 대해서 알아봤습니다. 이번에는 Dispatcher에서 Event를 전달하는 부분을 코드를 살펴보면서, 조금 더 수행 과정에 대해서 들여다보겠습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwbdxx/btrP19Z8ihV/cITuIYHJgj9YK1Cpzmu1xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwbdxx/btrP19Z8ihV/cITuIYHJgj9YK1Cpzmu1xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwbdxx/btrP19Z8ihV/cITuIYHJgj9YK1Cpzmu1xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbwbdxx%2FbtrP19Z8ihV%2FcITuIYHJgj9YK1Cpzmu1xK%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;480&quot; height=&quot;332&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;547&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;p data-ke-size=&quot;size16&quot;&gt;먼저 쓰레드간에 데이터 공유 시, 쓰레드 별로 작업 부하가 다를 수 있기 때문에 전달된 Event가 즉시 처리될 수 없습니다. 따라서 Dispatcher 내부에는 Buffer를 위해 위와 같이 post_callbacks_ 라는 자료 구조에 Event Callback을 저장합니다.&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;564&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FNLCO/btrPT1JqNlk/VF4vlaN5TjECjtNDZL1Ef0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FNLCO/btrPT1JqNlk/VF4vlaN5TjECjtNDZL1Ef0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FNLCO/btrPT1JqNlk/VF4vlaN5TjECjtNDZL1Ef0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFNLCO%2FbtrPT1JqNlk%2FVF4vlaN5TjECjtNDZL1Ef0%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;564&quot; height=&quot;321&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;321&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;위 코드를 통해서 방금 전 설명한 내용처럼 Dispatcher의 post를 통해서 Callback Event가 전달되면, 바로 처리하지 않고 내부에 존재하는 post_callbacks_ 자료구조에 해당 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;그렇다면, 해당 callback은 언제 실행될까요?&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;945&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mBfgb/btrPUnFfySy/YfAjpokqs0ZkQhPRkk2TP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mBfgb/btrPUnFfySy/YfAjpokqs0ZkQhPRkk2TP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mBfgb/btrPUnFfySy/YfAjpokqs0ZkQhPRkk2TP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmBfgb%2FbtrPUnFfySy%2FYfAjpokqs0ZkQhPRkk2TP0%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;780&quot; height=&quot;451&quot; data-origin-width=&quot;945&quot; data-origin-height=&quot;547&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;위 코드는 Dispatcher의 생성자 부분입니다. 여기서 Dispatcher를 구성하기 위해 다양한 속성들이 Member Initializer에 의해서 매핑되고 있지만, Event 전달 관점에서 주목할 부분은 빨간색으로 밑줄 친 영역입니다.&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;Dispatcher 내부에는 Scheduler가 존재하는데, post callback을 처리하기 위해서 별도 Scheduler를 통해 수행할 수 있습니다. 이때 위와 같이 runPostCallbacks()라는 메소드를 호출하는 것을 확인할 수 있습니다.&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;966&quot; data-origin-height=&quot;673&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lgFjE/btrP0hdps6u/tgdu9CUG5ughzxLyIEml3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lgFjE/btrP0hdps6u/tgdu9CUG5ughzxLyIEml3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lgFjE/btrP0hdps6u/tgdu9CUG5ughzxLyIEml3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlgFjE%2FbtrP0hdps6u%2Ftgdu9CUG5ughzxLyIEml3K%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;780&quot; height=&quot;543&quot; data-origin-width=&quot;966&quot; data-origin-height=&quot;673&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;해당 메소드의 역할을 살펴보면, post_callbacks_에 저장된 내용이 하나도 없을 때까지 Event 내용을 하나씩 꺼내서 실행시키는 역할을 수행합니다. 이후 모든 작업이 완료되면, 향후 Post 메소드를 통해 Event가 유입될 때까지 작업을 진행하지 않습니다.&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;지금까지 Dispatcher 내부 동작 과정에 대해서 살펴봤습니다. 이번에는 Dispatcher가 취급하는 Event 종류에 대해서 알아보겠습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;195&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9hoHW/btrP0GRcFTy/ZuNhcN7einvB7URWdcOI3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9hoHW/btrP0GRcFTy/ZuNhcN7einvB7URWdcOI3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9hoHW/btrP0GRcFTy/ZuNhcN7einvB7URWdcOI3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9hoHW%2FbtrP0GRcFTy%2FZuNhcN7einvB7URWdcOI3K%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;195&quot; data-origin-width=&quot;286&quot; data-origin-height=&quot;195&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;Dispatcher를 통해 전달되는 Event는 모두 위 클래스를 상속받습니다. 해당 클래스 내용을 잠시 살펴보면, event 타입의 프로퍼티를 기본적으로 가지고 있으며, 해당 클래스가 소멸할 때 event를 release 하도록 되어있습니다.&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;1025&quot; data-origin-height=&quot;405&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clU8xS/btrP2oJwTn2/sGpP4q1e6Adv32kzKj884k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clU8xS/btrP2oJwTn2/sGpP4q1e6Adv32kzKj884k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clU8xS/btrP2oJwTn2/sGpP4q1e6Adv32kzKj884k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclU8xS%2FbtrP2oJwTn2%2FsGpP4q1e6Adv32kzKj884k%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;1025&quot; height=&quot;405&quot; data-origin-width=&quot;1025&quot; data-origin-height=&quot;405&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;해당 event를 상속하는 Event 타입은 위 그림과 같이 크게 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;첫 번째는 Signal Event입니다. 이는 생성 즉시 Event를 발행할 때 사용되는 Event 타입입니다.&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;두 번째는 Timer Event입니다. 해당 Event는 내부 속성에 Timer가 존재하여, 지정된 시간이 지난 이후 Event를 발행할 수 있으며, Timer를 활성화하거나 비 활성화 등을 통해서 Timer를 조정할 수 있습니다.&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;세 번째는 File Event입니다. 해당 Event 타입은 File에 읽기 혹은 쓰기등의 내부 Event가 발생했을 때, 해당 Event를 전달하는 것을 목적으로 합니다. 또한 Linux의 경우 Socket은 File로 취급됩니다. 따라서 Socket을 쓰거나 읽을 때, 해당 Event를 감지하여 전달할 수 있습니다.&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;envoy에서는 위 세가지 Event 타입을 폭넓게 활용하여 여러 쓰레드간에 데이터 공유에 활용합니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. TLS(Thread Local Storage)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Threading Model을 설명하면서, 쓰레드마다 동일한 데이터를 보관할 경우 메모리 낭비로 인해 비효율이 발생함을 설명하고 이를 위해서 envoy에서는 TLS를 사용한다고 언급했습니다. envoy의 TLS에 대해서 알아보면서, envoy가 어떻게 메모리 낭비를 줄이기 위해 최적화 기법을 사용했는지를 살펴보겠습니다.&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;646&quot; data-origin-height=&quot;451&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/20pAE/btrP2a0SwjD/UyXH1nQJkk5evvEXrKTKi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/20pAE/btrP2a0SwjD/UyXH1nQJkk5evvEXrKTKi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/20pAE/btrP2a0SwjD/UyXH1nQJkk5evvEXrKTKi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F20pAE%2FbtrP2a0SwjD%2FUyXH1nQJkk5evvEXrKTKi0%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;265&quot; data-origin-width=&quot;646&quot; data-origin-height=&quot;451&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;TLS는 말 그대로 Thread Local 객체들을 저장하는 저장소이므로 가장 먼저 해야할 일은 TLS에 저장되는 타입에 대한 정의가 필요합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ejTsW3/btrP7TvQLCv/yweVIWrVIbTXKCKBhLcNck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ejTsW3/btrP7TvQLCv/yweVIWrVIbTXKCKBhLcNck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ejTsW3/btrP7TvQLCv/yweVIWrVIbTXKCKBhLcNck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FejTsW3%2FbtrP7TvQLCv%2FyweVIWrVIbTXKCKBhLcNck%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;828&quot; height=&quot;120&quot; data-origin-width=&quot;828&quot; data-origin-height=&quot;120&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;그에 따라 위와 같이 TLS에 저장하려는 데이터 타입에는 envoy에서 정의한 ThreadLocalObject 타입을 필수적으로 상속 받아야합니다. 그렇다면 ThradLocalObject 클래스는 어떻게 정의되어있을까요?&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;727&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZ5jk9/btrP3l2b3in/cPX0aYInkZfsmm048VHHqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZ5jk9/btrP3l2b3in/cPX0aYInkZfsmm048VHHqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZ5jk9/btrP3l2b3in/cPX0aYInkZfsmm048VHHqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZ5jk9%2FbtrP3l2b3in%2FcPX0aYInkZfsmm048VHHqk%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;727&quot; height=&quot;271&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;271&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;ThreadLocalObject 클래스를 살펴보면, 그 안에 어떠한 속성도 정의되어있지 않으며 asType 메소드가 존재하여 지정된 타입으로 캐스팅하여 반환하는 기능만을 제공하는 것을 확인할 수 있습니다.&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;또한 위 코드에서 주목할 부분은 ThreadLocalObject를 Shared Pointer로 감싼 타입을 새로 정의했다는 것입니다.&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;498&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdaTLG/btrP3jXzCuj/BfcxtE6MiAWsKBLL687X3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdaTLG/btrP3jXzCuj/BfcxtE6MiAWsKBLL687X3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdaTLG/btrP3jXzCuj/BfcxtE6MiAWsKBLL687X3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdaTLG%2FbtrP3jXzCuj%2FBfcxtE6MiAWsKBLL687X3K%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;498&quot; height=&quot;120&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;120&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;위 코드는 각 쓰레드별로 저장되는 TLS 구조의 모습입니다. 해당 구조체에는 각 쓰레드별로 구동되는 Dispatcher 정보를 저장할 수 있는 속성과, 이전 코드에서 확인한 ThreadLocalObject를 Shared Pointer로 감싼 타입을 Vector Container로 가지고 있는 것을 확인할 수 있습니다.&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;그렇다면 위와 같이 Shared Pointer 형태로 데이터를 관리하면 어떤 이점이 있을까요?&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;1240&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FOhsp/btrP37hOKKe/mCLnCWsYhSnudH5c0rKfy0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FOhsp/btrP37hOKKe/mCLnCWsYhSnudH5c0rKfy0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FOhsp/btrP37hOKKe/mCLnCWsYhSnudH5c0rKfy0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFOhsp%2FbtrP37hOKKe%2FmCLnCWsYhSnudH5c0rKfy0%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;640&quot; height=&quot;236&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;457&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;첫 번째로 얻을 수 있는 이점은 데이터 자체를 저장하는 것이 아니라 Pointer를 지니고 있으므로 데이터 중복을 줄일 수 있으며, 메모리 주소 값을 저장하므로 비용이 많이 절감됩니다.&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;두 번째는 Shared Pointer로 데이터를 가르키는 것이기 때문에 메모리 관리를 자동으로 수행할 수 있습니다.&amp;nbsp;&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;1240&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oSsLX/btrP6cWIAMn/LFGKlAPc9QOTKb409F2H7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oSsLX/btrP6cWIAMn/LFGKlAPc9QOTKb409F2H7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oSsLX/btrP6cWIAMn/LFGKlAPc9QOTKb409F2H7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoSsLX%2FbtrP6cWIAMn%2FLFGKlAPc9QOTKb409F2H7K%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;640&quot; height=&quot;236&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;457&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;가령 위와 같이 Shared Pointer로 가르키는 오브젝트에 대한 링크가 모두 해제되었을 경우 참조 카운트가 0이므로 자동으로 메모리를 해제할 수 있습니다.&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;그렇다면 위와 같이 Shared Pointer를 사용하게 되면 모든 문제가 해결될까요? 정답은 그렇지 않습니다. 위와 같이 구성하게 되면 결국 처음에 야기되었던 문제점이 다시 발생합니다. 그 이유는 결국 공유 데이터를 가르키는 것이기 때문에 결론적으로는 쓰레드 안정성에 대한 이슈가 다시 불거지기 때문입니다.&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;따라서 envoy에서는 기존에 살펴본 Dispatcher와 각 쓰레드별로 Shared Pointer를 지정하도록 구성함으로써 이러한 문제를 해결하고자 했습니다. 이와 관련해서는 다음 시나리오를 통해서 확인해보겠습니다.&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;1564&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2xlEX/btrP4vvXVXt/EJRZfXJEMbN2mXyWNDORe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2xlEX/btrP4vvXVXt/EJRZfXJEMbN2mXyWNDORe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2xlEX/btrP4vvXVXt/EJRZfXJEMbN2mXyWNDORe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2xlEX%2FbtrP4vvXVXt%2FEJRZfXJEMbN2mXyWNDORe0%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;1564&quot; height=&quot;459&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;459&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;위와 같이 두 개의 쓰레드에서 서로 동일한 Thread Local Object를 가르키고 있다고 가정해봅시다. 이러한 상황에서 Thread Local 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;&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;1564&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pvbHw/btrP4VHWxrW/eZkKn4Z7fNwD9UlyJ9tjeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pvbHw/btrP4VHWxrW/eZkKn4Z7fNwD9UlyJ9tjeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pvbHw/btrP4VHWxrW/eZkKn4Z7fNwD9UlyJ9tjeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpvbHw%2FbtrP4VHWxrW%2FeZkKn4Z7fNwD9UlyJ9tjeK%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;1564&quot; height=&quot;629&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;629&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;1. 변경을 수행해야하는 쓰레드에서 Thread Local Object를 새로 만들고 기존 Thread Local Object를 가르키던 포인터를 신규 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;2. 변경을 수행한 쓰레드에서 다른 쓰레드에게 변경 내용을 전파합니다. 따라서 해당 쓰레드의 Dispatcher에게 post 요청을 통해 변경 Event를 전달합니다.&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;3. Dispatcher는 전달된 Event 내역을 자신의 buffer에 저장하였다가 쓰레드에게 데이터 변경 내용을 전달합니다.&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;1564&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SAmRR/btrP6dBmysb/kcy1mBm5Fn6UbS9RVzHtF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SAmRR/btrP6dBmysb/kcy1mBm5Fn6UbS9RVzHtF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SAmRR/btrP6dBmysb/kcy1mBm5Fn6UbS9RVzHtF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSAmRR%2FbtrP6dBmysb%2Fkcy1mBm5Fn6UbS9RVzHtF0%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;1564&quot; height=&quot;459&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;459&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;4. Event를 전달받은 쓰레드에서는 Event를 통해 전달된 Callback을 수행하면서 자신이 가르키고 있는 Thread Local Object를 신규로 변경합니다. 이 과정을 통해 모든 쓰레드에서 새로운 Thread Local Object로 변경이 완료되면, 기존의 Thread Local Object의 참조 카운트는 0이 되므로 삭제할 수 있습니다.&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;위와 같이 4가지 과정을 수행하면, 쓰레드 안정성을 확보할 수 있습니다. 그 이유는 첫 번째 단계를 살펴보면, 두 개의 쓰레드가 공유 데이터를 바라보고 있다가 특정 쓰레드에서 데이터 변경이 수행되었을 경우 다른 쓰레드에 영향을 미치지 않기 때문입니다. 이후 다른 쓰레드의 event loop 수행 과정에서 변경 내용을 통지받으면 그제서야 변경된 데이터를 바라볼 수 있도록 적용됩니다.&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;이는 데이터 변경이 발생되었을 때 동일 시점에 모든 쓰레드에서 공유 데이터를 바라볼 수 있는 Strict Consistency는 포기하더라도 Eventually Consistency를 적용함으로써 쓰레드 안정성, 메모리 효율성과 성능을 얻는 것이 이점이 크기 때문입니다.&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;지금까지 TLS에 대해서 기본적인 동작 과정에 대해서 개략적으로 살펴봤습니다. 이번에는 조금 더 구체적으로 Thread Local Data를 활용해서 Envoy내 쓰레드 끼리 어떻게 동기화를 수행하는지에 대해서 살펴보겠습니다.&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;style3&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-1 Envoy TLS 동작 과정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Envoy 내부에는 TLS를 관장하기 위해 InstanceImpl 인스턴스가 존재합니다. 이를 위해 해당 클래스 내부에는 TLS와 쓰레드간 상호작용을 위한 여러 메소드와 속성이 포함되어있습니다. 코드를 토대로 해당 클래스에 대해서 자세하게 살펴보겠습니다.&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;920&quot; data-origin-height=&quot;1454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpm7Sh/btrP9e1aqHi/fYnVkIUW24zlNM0RMbczwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpm7Sh/btrP9e1aqHi/fYnVkIUW24zlNM0RMbczwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpm7Sh/btrP9e1aqHi/fYnVkIUW24zlNM0RMbczwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdpm7Sh%2FbtrP9e1aqHi%2FfYnVkIUW24zlNM0RMbczwK%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;920&quot; height=&quot;1454&quot; data-origin-width=&quot;920&quot; data-origin-height=&quot;1454&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;위 코드에서 먼저 눈에 띄는 것은 TLS를 위한 자료구조인 ThreadLocalData 구조체가 해당 클래스내에 포함된 것을 확인할 수 있습니다. 또한 ThreadLocalData는 C++의 thread_local 키워드가 붙어있어 쓰레드내 전역적으로 공유되는 데이터임을 알 수 있습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYnLW0/btrP35Zx2uS/w967jcWhmrwOMtBxuZu62k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYnLW0/btrP35Zx2uS/w967jcWhmrwOMtBxuZu62k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYnLW0/btrP35Zx2uS/w967jcWhmrwOMtBxuZu62k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYnLW0%2FbtrP35Zx2uS%2Fw967jcWhmrwOMtBxuZu62k%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;480&quot; height=&quot;131&quot; data-origin-width=&quot;984&quot; data-origin-height=&quot;268&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;그 다음 눈 여겨 볼 점은 dispatcher를 저장하는 점과 registered_threads_ 속성을 통해 해당 쓰레드와 연결되는 쓰레드들을 관리한다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void InstanceImpl::registerThread(Event::Dispatcher&amp;amp; dispatcher, bool main_thread) {
  ASSERT_IS_MAIN_OR_TEST_THREAD();
  ASSERT(!shutdown_);

  if (main_thread) {
    main_thread_dispatcher_ = &amp;amp;dispatcher;
    thread_local_data_.dispatcher_ = &amp;amp;dispatcher;
  } else {
    ASSERT(!containsReference(registered_threads_, dispatcher));
    registered_threads_.push_back(dispatcher);
    dispatcher.post([&amp;amp;dispatcher] { thread_local_data_.dispatcher_ = &amp;amp;dispatcher; });
  }
}&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;실제 Thread 등록은 Worker 쓰레드가 생성되는 시점에 registerThread 메소드를 호출하여 InstanceImpl에 등록됩니다. 이때 호출되는 코드를 살펴보면, 등록되는 Thread가 Main 쓰레드인 경우와 그렇지 않은 경우가 분리되어서 저장되는 것을 확인할 수 있습니다.&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;그밖에 InstanceImpl 코드를 더 살펴보면,&amp;nbsp; 그 전에는 보지 못했던 Slot 구조체와 이를 저장하는 slots_ Vector Container가 존재하는 것을 알 수 있습니다. Slot은 무엇이고 어떤 역할을 수행할까요?&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;1414&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkFWz5/btrP8Ydkh9p/bvpebuZ3w00BBo26n0K9D0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkFWz5/btrP8Ydkh9p/bvpebuZ3w00BBo26n0K9D0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkFWz5/btrP8Ydkh9p/bvpebuZ3w00BBo26n0K9D0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkFWz5%2FbtrP8Ydkh9p%2FbvpebuZ3w00BBo26n0K9D0%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;1414&quot; height=&quot;385&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;385&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;InstainceImpl은 ThreadLocalData를&amp;nbsp; thread_local 속성으로 가지고 있지만, 사용자가 ThreadLocalData에 데이터를 할당하고자 할때 직접 할당하는 것을 방지하도록 설계되어있습니다.&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;대신 Client는 ThreadLocalData에 대한 추가,수정,삭제 작업을 희망할 경우 InstanceImpl로 부터 Slot을 할당 받아야합니다. 여기서 Slot은 ThreadLocalData에 작업 수행 및 ThreadLocalData를 공유하는 모든 쓰레드에게 명령을 요청하기 위한 일종의 interface라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;SlotPtr InstanceImpl::allocateSlot() {
  ASSERT_IS_MAIN_OR_TEST_THREAD();
  ASSERT(!shutdown_);

  if (free_slot_indexes_.empty()) {
    SlotPtr slot = std::make_unique&amp;lt;SlotImpl&amp;gt;(*this, slots_.size());
    slots_.push_back(slot.get());
    return slot;
  }
  const uint32_t idx = free_slot_indexes_.front();
  free_slot_indexes_.pop_front();
  ASSERT(idx &amp;lt; slots_.size());
  SlotPtr slot = std::make_unique&amp;lt;SlotImpl&amp;gt;(*this, idx);
  slots_[idx] = slot.get();
  return slot;
}&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;Slot을 할당받기 위해서는 Client는 InstanceImpl에게 allocateSlot() 메소드를 호출할 수 있습니다. 이때 내부적으로는 비어있는 Slot을 관리하는 free_slot_indexes가 존재하는데, free한 Slot이 비었을 경우 Slot을 추가로 생성해서 반환하는 것을 볼 수 있습니다. 반면 비어있는 Slot이 존재한다면, 해당 Slot의 index를 기반으로 Slot을 반환합니다.&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;1348&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/euuK91/btrP33HrRw7/nDwn637ly3Fbgx3SD0k2m0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/euuK91/btrP33HrRw7/nDwn637ly3Fbgx3SD0k2m0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/euuK91/btrP33HrRw7/nDwn637ly3Fbgx3SD0k2m0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeuuK91%2FbtrP33HrRw7%2FnDwn637ly3Fbgx3SD0k2m0%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;280&quot; data-origin-width=&quot;1348&quot; data-origin-height=&quot;280&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;위 코드에서 주목할 부부은 Slot을 생성할 때, 두 번째 인자에 비어있는 Slot의 index를 지정한다는 점입니다. 따라서 Client 입장에서 전달받은 Slot에서는 Instance 내부에 존재하는 slots_&amp;nbsp; list에서 몇 번째 index에 Slot이 생성되었는지 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;void InstanceImpl::SlotImpl::set(InitializeCb cb) {
  ASSERT_IS_MAIN_OR_TEST_THREAD();
  ASSERT(!parent_.shutdown_);

  for (Event::Dispatcher&amp;amp; dispatcher : parent_.registered_threads_) {
    // See the header file comments for still_alive_guard_ for why we capture index_.
    dispatcher.post(wrapCallback(
        [index = index_, cb, &amp;amp;dispatcher]() -&amp;gt; void { setThreadLocal(index, cb(dispatcher)); }));
  }

  // Handle main thread.
  setThreadLocal(index_, cb(*parent_.main_thread_dispatcher_));
}&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;Client는 Slot을 할당 받으면, Thread Local에 저장하기 위한 Callback Event를 등록할 수 있습니다. 이때 위 코드와 같이 set 메소드를 통해 저장할 수 있으며, 코드 내용을 자세히 살펴보면, 등록된 Thread를 모두 순회하면서 dispatcher에게 post 메소드를 호출하여, 각자 쓰레드가 보유한 setThreadLocal을 호출하도록 요구하는 것을 볼 수 있습니다.&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;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;void InstanceImpl::setThreadLocal(uint32_t index, ThreadLocalObjectSharedPtr object) {
  if (thread_local_data_.data_.size() &amp;lt;= index) {
    thread_local_data_.data_.resize(index + 1);
  }

  thread_local_data_.data_[index] = object;
}&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;setThreadLocal 코드는 위와 같습니다. 해당 코드 내용을 보면 ThreadLocalData 구조체 내에있는 data_에 접근하여 Slot에 지정된 index에 해당하는 위치에 Callback을 저장하는 것을 볼 수 있습니다.&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;1517&quot; data-origin-height=&quot;317&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BDwsW/btrP9jIj8YS/s4Rd1bxcQw9jFRKTmU1mA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BDwsW/btrP9jIj8YS/s4Rd1bxcQw9jFRKTmU1mA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BDwsW/btrP9jIj8YS/s4Rd1bxcQw9jFRKTmU1mA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBDwsW%2FbtrP9jIj8YS%2Fs4Rd1bxcQw9jFRKTmU1mA1%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;1517&quot; height=&quot;317&quot; data-origin-width=&quot;1517&quot; data-origin-height=&quot;317&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;Client에서 수행할 수 있는 두 번째 요청은 runOnAllThreads 입니다. InstanceImpl로 부터 전달받은 Slot에는 index 값과 더불어 InstanceImpl에 대한 parent_ 속성에 저장되어있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;void InstanceImpl::SlotImpl::runOnAllThreads(const UpdateCb&amp;amp; cb) {
  parent_.runOnAllThreads(dataCallback(cb));
}&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;따라서 Client 입장에서 전달받은 Slot에 runOnAllThreads 명령을 요청하면, 내부적으로는 InstanceImpl에게 runOnAllThreads를 실행시키도록 위임합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;void InstanceImpl::runOnAllThreads(Event::PostCb cb) {
  ASSERT_IS_MAIN_OR_TEST_THREAD();
  ASSERT(!shutdown_);

  for (Event::Dispatcher&amp;amp; dispatcher : registered_threads_) {
    dispatcher.post(cb);
  }

  // Handle main thread.
  cb();
}&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;요청을 전달받은 InstanceImpl에서는 이전과 마찬가지로 registered_threads_에 등록된 dispatcher 들을 순회하면서 사용자가 전달한 Callback을 전달하고 Main 쓰레드 내에서 해당 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;지금까지 InstanceImpl을 통해 쓰레드를 등록, Thread Local Object 할당 및 Dispatcher를 활용한 데이터 공유 과정에 대해서 살펴봤습니다. 이번에는 Envoy가 종료될 때 TLS 초기화가 어떻게 이루어지는지 살펴보겠습니다.&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;pre class=&quot;isbl&quot;&gt;&lt;code&gt;void InstanceImpl::shutdownGlobalThreading() {
  ASSERT_IS_MAIN_OR_TEST_THREAD();
  ASSERT(!shutdown_);
  shutdown_ = true;
}
&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;Envoy가 종료될 때, InstanceImpl에 suhtdownGlobalThreading() 메소드를 호출하여 종료를 요청합니다. 다만 해당 메소드의 역할은 InstanceImpl 내부에 존재하는 shutdown_ bool 값을 true로 변경하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot;&gt;&lt;code&gt;void InstanceImpl::shutdownThread() {
  ASSERT(shutdown_);
  for (auto it = thread_local_data_.data_.rbegin(); it != thread_local_data_.data_.rend(); ++it) {
    it-&amp;gt;reset();
  }
  thread_local_data_.data_.clear();
}&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;이후 개별적인 Worker 쓰레드는 shutdownThread() 메소드를 호출함으로써 쓰레드 별로 종료 과정이 이루어집니다. 이때 위 코드에서 주목할 부분은 ThreadLocalData 내부 데이터를 종료할 때, 뒤에서부터 reset 과정이 진행되는 점입니다.&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;그 이유는 ThreadLocalData를 구성할 때 가장 먼저 적재되는 데이터는 ClusterManager에서 생성되는 ThreadLocalObject이고 해당 데이터들은 종료될 때까지 수정이 이루어지지 않는 데이터이면서, 여러 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 부터 종료를 시작해서 가장 높은 Object를 나중에 종료시킴으로써 의존성이 강한 Object가 먼저 삭제되어 발생될 수 있는 문제를 예방합니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Envoy 쓰레드 모델과, TLS에 대해서 살펴봤습니다. TLS를 이해하기 위해서는 Dispatcher의 기능 중 일부에 이해해야하기 때문에 이 부분에 대해서도 다루어봤습니다.&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;또한 코드를 살펴보면서 Envoy에서 Lock을 사용하지 않고도 공유 데이터를 사용하기 위해 많은 고민이 반영되었음을 확인할 수 있었습니다.&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;Envoy의 쓰레딩 모델과 TLS를 이해하는 것은 istio를 이해하는데 있어서도 중요합니다. Envoy 내부에서 xDS API 업데이트가 발생되었을 때 내부적으로 TLS를 통해서 데이터 변경을 전파하고 반영하기 때문입니다. 그러한 측면에서 이번 포스팅 내용은 어렵지만 중요하다고 볼 수 있습니다.&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <category>Envoy</category>
      <category>envoy architecture</category>
      <category>envoy dispatcher</category>
      <category>envoy threading model</category>
      <category>envoy TLS</category>
      <category>envoy 아키텍처</category>
      <category>Istio</category>
      <category>istio envoy</category>
      <category>istio 아키텍처</category>
      <category>ThreadLocalStorage</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/212</guid>
      <comments>https://cla9.tistory.com/212#entry212comment</comments>
      <pubDate>Wed, 17 May 2023 14:09:32 +0900</pubDate>
    </item>
    <item>
      <title>1. [envoy-internals] 아키텍처 Overview</title>
      <link>https://cla9.tistory.com/191</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 서론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;istio를 처음 사용하면, 이전과 달리 설치가 및 설정 방법이 편해져서 사용하기 쉽습니다. 그럼에도 불구하고 istio를 운영하는 것은 여전히 어려운 일입니다. 그 이유는 istio에 문제가 생겼을 때 이를 해결하기 위해서는 istio 내부 아키텍처에 대한 이해가 필수적이기 때문입니다.&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;istio는 엄밀히 말해서 envoy proxy를 관리하는 컨트롤러라고 볼 수 있습니다. 따라서 istio 뿐만 아니라 envoy proxy의 내부 구조에 대해서도 잘 알고 있어야합니다. 이번 포스팅부터 진행되는 envoy internals 시리즈는 envoy 내부 구조와 흐름에 대해서 살펴보면서, 트러블 슈팅을 위한 인사이트를 갖는 것을 목적으로 하고 있습니다. 다만 모든 내용을 다루지는 않으며, istio를 이해하는데 있어 필수적인 부분에 대해서만 살펴보겠습니다. 이번 포스팅은 envoy 관련 첫번째 포스팅으로 envoy 구조와 내부 컴포넌트 동작 원리에 대해서 알아보도록 하겠습니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Envoy 컴포넌트&lt;/h2&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;1165&quot; data-origin-height=&quot;219&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pDFg7/btrJBGDUgRc/jXLWIujejdKQKFaGr9PURk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pDFg7/btrJBGDUgRc/jXLWIujejdKQKFaGr9PURk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pDFg7/btrJBGDUgRc/jXLWIujejdKQKFaGr9PURk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpDFg7%2FbtrJBGDUgRc%2FjXLWIujejdKQKFaGr9PURk%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;1165&quot; height=&quot;219&quot; data-origin-width=&quot;1165&quot; data-origin-height=&quot;219&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;Envoy는 Proxy 프로그램으로 사용자와 Service 중간에서 Proxy 역할을 수행합니다. 이때 Client로부터 전달받는 트래픽은 Downstream, 전달받은 요청을 Service로 전달하는 트래픽을 Upstream이라고 부릅니다. 즉 Envoy를 기준으로 상위 서비스 전달은 Upstream Envoy로 흘러 들어오는 스트림은 Downstream입니다.&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;그렇다면 Downstream을 통해 전달되는 트래픽이 Envoy의 어떤 과정을 거쳐서 Upstream으로 Service에 전달될까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Envoy의 주요 컴포넌트에 대해서 먼저 알아보겠습니다.&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;1672&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4JAvu/btrJCoXygSw/jH5dgxlabp2LZ72Hzd5QFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4JAvu/btrJCoXygSw/jH5dgxlabp2LZ72Hzd5QFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4JAvu/btrJCoXygSw/jH5dgxlabp2LZ72Hzd5QFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4JAvu%2FbtrJCoXygSw%2FjH5dgxlabp2LZ72Hzd5QFk%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;1672&quot; height=&quot;446&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;446&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;Envoy 내부에서 가장 핵심이 되는 컴포넌트는 위 그림과 같습니다. 도식화된 그림을 살펴보면 여러개 Listener 그리고 요청을 전달하기 위한 Route 과정 그리고 해당 Route 과정을 통해서 Cluster에 전달되고 Cluster는 Load Balancing 정책에 따라서 자신이 보유하고 있는 Endpoint 중 하나를 선정합니다. 결과적으로는 해당 Endpoint에 매칭되는 Service에 트래픽이 전달됩니다.&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;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;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1 Endpoint&lt;/h3&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;1326&quot; data-origin-height=&quot;730&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEkF8D/btrJCnjLyk2/xCgq7lzXYkj5Zx5top7fM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEkF8D/btrJCnjLyk2/xCgq7lzXYkj5Zx5top7fM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEkF8D/btrJCnjLyk2/xCgq7lzXYkj5Zx5top7fM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEkF8D%2FbtrJCnjLyk2%2FxCgq7lzXYkj5Zx5top7fM0%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;640&quot; height=&quot;352&quot; data-origin-width=&quot;1326&quot; data-origin-height=&quot;730&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;먼저 살펴볼 것은 Endpoint입니다. Endpoint는 위 그림과 같이 Proxy를 통해 연결해야하는 최종 목적지 주소와 Port 번호를 의미합니다. 위와 같이 address는 IP일 수도 있고 혹은 도메인 네임일 수도 있습니다. 그 밖에 health check를 위한 설정 및 Load Balancing을 수행할 때 가중치 와 우선순위 등을 설정할 수 있습니다. 자세한 내용은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/endpoint/v3/endpoint_components.proto&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Envoy 공식 문서&lt;/a&gt;&lt;/u&gt;&lt;/span&gt;를 통해서 확인하시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2 Cluster&lt;/h3&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;977&quot; data-origin-height=&quot;381&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc3xoD/btrJCmSHpFD/NkRk6OBwRQfoFgqoRAjZd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc3xoD/btrJCmSHpFD/NkRk6OBwRQfoFgqoRAjZd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc3xoD/btrJCmSHpFD/NkRk6OBwRQfoFgqoRAjZd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc3xoD%2FbtrJCmSHpFD%2FNkRk6OBwRQfoFgqoRAjZd1%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;977&quot; height=&quot;381&quot; data-origin-width=&quot;977&quot; data-origin-height=&quot;381&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;Service는 가용성 혹은 성능 향상의 목적으로 여러 서버에 동일한 Service를 배포합니다. 따라서 단일 Endpoint를 통해 여러개 Service를 관리할 수 있는 논리적인 집합 단위가 필요합니다.&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;Envoy Proxy에서는 Cluster 컴포넌트를 통해 Endpoint들을 그룹핑하여 관리할 수 있습니다. 해당 컴포넌트를 통해서 Endpoint 중에서 어디로 트래픽을 보낼지 결정하는 Load Balancing을 결정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1660448802441&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  clusters:
  - name: some_service
    connect_timeout: 0.25s
    type: STATIC
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: some_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 1234&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;위 설정은 Envoy proxy 공식 문서의 설정 예시입니다. Cluster 하위에 Endpoint 목록을 지정하도록 되어 있는 것을 확인할 수 있습니다. 달리 말하면 Cluster와 Endpoint 간에는 의존성이 존재함을 확인할 수 있습니다. 위 설정에는 Endpoint 지정외에도 다양한 설정을 지정할 수 있습니다. 가령 Circuit Breaker 설정 HTTP Connection과 DNS refresh와 Resolving 정책 등의 부가적인 옵션을 해당 컴포넌트를 통해서 설정할 수 있습니다. 자세한 옵션 설정은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#envoy-v3-api-field-config-cluster-v3-cluster-cluster-type&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Envoy 공식 문서&lt;/a&gt;&lt;/u&gt;&lt;/span&gt;를 통해서 확인하시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3 Listener&lt;/h3&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;478&quot; data-origin-height=&quot;311&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqYXnh/btrJCmd8dIq/YD2rB12LGzAf2GBxMIdYf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqYXnh/btrJCmd8dIq/YD2rB12LGzAf2GBxMIdYf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqYXnh/btrJCmd8dIq/YD2rB12LGzAf2GBxMIdYf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqYXnh%2FbtrJCmd8dIq%2FYD2rB12LGzAf2GBxMIdYf0%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;320&quot; height=&quot;208&quot; data-origin-width=&quot;478&quot; data-origin-height=&quot;311&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;Listener는 Envoy Proxy가 어떤 address의 어떤 port로 접속하는 요청에 대해서 Proxy 처리를 수행할 것인지를 설정합니다. 가령 위와같이 지정했다면, 해당 Envoy Proxy가 위치한 서버로 접속하는 80 Port 요청에 대해서 Envoy Proxy가 트래픽을 전달받고 후속 작업을 처리하게됩니다. 즉 Listener는 Envoy Proxy로 흐름을 전달하는 문지기 역할이라고 볼 수 있습니다.&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;1899&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1jWZy/btrJC4rvkDt/ZxJJ8kDdxNfKHwK1t9zKg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1jWZy/btrJC4rvkDt/ZxJJ8kDdxNfKHwK1t9zKg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1jWZy/btrJC4rvkDt/ZxJJ8kDdxNfKHwK1t9zKg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1jWZy%2FbtrJC4rvkDt%2FZxJJ8kDdxNfKHwK1t9zKg0%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;1899&quot; height=&quot;558&quot; data-origin-width=&quot;1899&quot; data-origin-height=&quot;558&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;Listener를 통해 트래픽이 전달되었다면, 이를 Cluster에 전달하기 위해서는 여러 내부 과정을 거쳐야합니다. Listener에는 Listener Filters와 Filter Chains(Network Filters)를 지니고 있습니다. 따라서 실제 트래픽이 전달되었을 때 Listener 내부에 위치한 Filter들을 통과하면서 사용자 요청을 분석하고 어떤 Cluster로 전달해야할지 등을 결정합니다.&amp;nbsp;그렇다면 Listener Filter와 Filter Chains는 무엇이 다를까요?&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;Listener Filters는 Connection에 대한 Metadata를 조작하거나 추가하는 데 사용되며, 변경된 정보를 토대로 Filter Chains에 존재하는 무수한 Filter 중에서 해당 요청을 처리하는데 적합한 Filter를 선정하는데 사용됩니다. 즉 실제 사용자 요청을 처리하는 것은 FIlter Chains에 존재하는 Filter이지만 해당 Filter를 사용하기 위해서 사전에 보조적인 작업을 담당하는 것이 Listener FIlter라고 볼 수 있습니다.&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;참고로 Envoy에서 제공하는 Listener Filter 목록은 위와 같으며, 개별 Filter의 역할은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 271px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 23.9535%; height: 20px; text-align: center;&quot;&gt;Filter&lt;/td&gt;
&lt;td style=&quot;width: 76.0465%; height: 20px; text-align: center;&quot;&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 97px;&quot;&gt;
&lt;td style=&quot;width: 23.9535%; height: 97px; text-align: center;&quot;&gt;HTTP Inspector&lt;/td&gt;
&lt;td style=&quot;width: 76.0465%; height: 97px;&quot;&gt;Application에서 전달한 Traffic을 분석하여 해당 네트워크 요청이 HTTP인지 확인합니다. 또한 HTTP 요청이 맞다면, HTTP 1.1 요청인지 혹은 HTTP 2 요청인지를 분석합니다. &lt;br /&gt;&lt;br /&gt;이를 토대로 추후 네트워크 요청에 적합한 Filter Chain을&amp;nbsp; 찾는데 사용됩니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;width: 23.9535%; height: 60px; text-align: center;&quot;&gt;Original Destination&lt;/td&gt;
&lt;td style=&quot;width: 76.0465%; height: 60px;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #404040;&quot;&gt;IpTable에 의해서 redirect된 소켓의 원래 목적지 값 주소를 알기 위해 SO_ORIGINAL_DST 값을 읽는 역할을 수행합니다.&amp;nbsp; 해당 값은 Envoy 처리 이후 Connection Local address로 설정하는데 사용됩니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;width: 23.9535%; height: 60px; text-align: center;&quot;&gt;Original Source&lt;/td&gt;
&lt;td style=&quot;width: 76.0465%; height: 60px;&quot;&gt;Client가 Envoy의 주소로 Downstream 연결을 시도하면,&amp;nbsp; Envoy는 Upstream과 통신을 위해서는&amp;nbsp; Source IP를 Envoy의 주소로 변경이 필요합니다. 따라서 해당 과정을 통해 연결의 목적지 주소를 Source 주소로 복제하는데 사용합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 23.9535%; height: 17px; text-align: center;&quot;&gt;Proxy Protocol&lt;/td&gt;
&lt;td style=&quot;width: 76.0465%; height: 17px;&quot;&gt;해당 Listener Filter는 HA Proxy Protocol을 지원하기 위한 역할을 수행합니다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 23.9535%; height: 17px; text-align: center;&quot;&gt;TLS Inspector&lt;/td&gt;
&lt;td style=&quot;width: 76.0465%; height: 17px;&quot;&gt;HTTP Inspector와 유사하게 해당 요청이 TLS 요청인지를 확인합니다. 이를 토대로 추후 네트워크 요청에 적합한 Filter Chain을 찾는데 사용됩니다.&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;Listener Filter를 통해서 Connection 요청 Metadata를 조작하거나 어떤 요청인지를 분석하고난 이후에는 Filter Chains(Network Filters)에 위치한 FIlter들을 통과하면서 사용자의 요청에 적합한 Filter를 찾아서 수행합니다.&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;1782&quot; data-origin-height=&quot;917&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KFUDg/btrP3mrRKeC/O024EecN0Vfd8VSSrKDRxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KFUDg/btrP3mrRKeC/O024EecN0Vfd8VSSrKDRxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KFUDg/btrP3mrRKeC/O024EecN0Vfd8VSSrKDRxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKFUDg%2FbtrP3mrRKeC%2FO024EecN0Vfd8VSSrKDRxk%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;1782&quot; height=&quot;917&quot; data-origin-width=&quot;1782&quot; data-origin-height=&quot;917&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;기본적으로 제공하는 FIlter Chains 목록은 위와 같습니다. 해당 항목들은 L3/L4 Filter 기능을 담당하며, 사용자 요청에 적합한 Filter를 찾아서 처리하고 목적지로 전달하는데 사용합니다. 만약 위 Filters 중 사용자 요청을 처리할 수 없는 경우에는 Envoy에서 제공하는 Default Chain이 사용되며, 만약 Default Chain 설정을 하지 않았다면 해당 Connection은 종료됩니다. 참고로 envoy proxy 기동시 위와 같이 모든 Filter가 등록되는 것은 아니며 순서 또한 다를 수 있습니다. 위 Filter 목록 중 필요한 Filter만 선별적으로 등록 가능합니다. 이에 대해서는 추후 살펴보겠습니다.&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;개별 FIlter의 기능 설명은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/network_filters/network_filters&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;공식 문서&lt;/a&gt;&lt;/u&gt;&lt;/span&gt;를 참고하기 바라며, 여기서는 HTTP connection manager에 대해 중점으로 다루어 보고자 합니다.&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;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-3-1 HTTP Connection manager&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Envoy에서 HTTP를 담당하는 Network Level의 Filter로써 Filter Chain의 가장 마지막에 위치합니다. 해당 Filter의 주요 기능은 raw byte를 HTTP 메시지 convert를 담당하며, 내부적으로 HTTP L7 Filter들이 존재하여 부가적인 작업을 수행합니다. 따라서 HTTP 요청이 전달되었을 때, Listener Filters 이후 Filter Chain을 수행하는 과정에서 HTTP Connection manager가 요청을 처리할 경우 내부적으로 sub filters를 적용하여 부가작업을 수행합니다.&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;HTTP Connection 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;b&gt;1) HTTP header 조작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- 여러가지 Security 이유로 인해 Envoy를 통해서 특정 header를 삭제하거나 값을 변경할 수 있습니다. 가령 use_remote_address 옵션을 true로 변경했을 경우 connection manager는 실제 전달되는 remote address를 x-forwared-for http header에 사용합니다. 그밖에 다양한 header에 대해서 조작이 가능합니다.&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;b&gt;2) Retry 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- HTTP 요청에 대해서 내부적으로 연결이 실패했을 때 얼만큼 Retry할 것인지를 설정할 수 있습니다.&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;3) Redirect&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 요청 서비스에서 Redirect 응답이 왔을 때 Proxy 내부에서 해당 3xx 응답을 기반으로 Redirect를 수행할 수 있습니다.&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;4) Timeout&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- HTTP 요청에 대해서 응답이 지정 시간동안 없을 경우 요청을 취소할 수 있습니다.&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;위와 같은 기능 외 HTTP Connection manager에는 L7 Filters들이 있어서 해당 Filters를 통해 부가적인 작업을 수행할 수 있다고 말했습니다. 여기에는 다음과 같은 Filter들이 해당됩니다.&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;263&quot; data-origin-height=&quot;1265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ekYHWU/btrJB3tkzWk/nBvMgKxSeGKbrnxdYFrBxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ekYHWU/btrJB3tkzWk/nBvMgKxSeGKbrnxdYFrBxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ekYHWU/btrJB3tkzWk/nBvMgKxSeGKbrnxdYFrBxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FekYHWU%2FbtrJB3tkzWk%2FnBvMgKxSeGKbrnxdYFrBxk%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;263&quot; height=&quot;1265&quot; data-origin-width=&quot;263&quot; data-origin-height=&quot;1265&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;각 Filter에 대한 설명은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.envoyproxy.io/docs/envoy/v1.23.0/configuration/http/http_filters/http_filters&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Envoy 공식 문서&lt;/a&gt;&lt;/u&gt;&lt;/span&gt;를 참고하시기 바라며, 참고로 위 Filter 중에 Router Filter의 경우는 사용자가 지정한 router 규칙에 일치하는 URL로 접근하였을 경우 지정된 Cluster로 Forwarding을 담당하는 Filter입니다.&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;1383&quot; data-origin-height=&quot;487&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dn5Ln6/btrJBeaqnvT/wHiPmqgFq7R6oon37TkCzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dn5Ln6/btrJBeaqnvT/wHiPmqgFq7R6oon37TkCzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dn5Ln6/btrJBeaqnvT/wHiPmqgFq7R6oon37TkCzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdn5Ln6%2FbtrJBeaqnvT%2FwHiPmqgFq7R6oon37TkCzK%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;1383&quot; height=&quot;487&quot; data-origin-width=&quot;1383&quot; data-origin-height=&quot;487&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 지금까지 설명을 바탕으로 HTTP 요청에 대한 Envoy Proxy 내부의 네트워크 흐름을 다시 한번 짚어보겠습니다.&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;1) Listener에 구성된 address와 port에 상응하는 요청이 들어오면, Listener 내부적으로 Listener Filters로 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2) Listener Filters를 순회하면서 Connection Metadata를 조작하고 이후 Network Filters로 전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3) Filter Chains(Network Filters)를 순회하면서 사용자 요청을 처리하는데 적합한 Filter를 찾고 해당 Filter가 요청을 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(※ 위 예제에서는 HTTP 요청이 들어왔음을 가정했으므로 HTTP Connection Manager가 이를 담당합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4) HTTP Connection Manager 내부에 있는 Sub Filters를 순회합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5) L7 필터 마지막에 위치한 Router Filter는 사용자의 Routing Path 요청과 적합한 Cluster를 찾고 해당 Cluster에게 트래픽을 Forwarding 하는 역할을 수행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6) Cluster는 내부에 설정된 로드밸런싱 정책등을 고려하여 적합한 Endpoint를 선정하며, 해당 Endpoint에 매칭되는 Service로 트래픽이 전달됩니다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-4 Envoy 컴포넌트 등록 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Envoy 내부에 존재하는 컴포넌트를 기반으로 Envoy 네트워크 흐름에 대해서 살펴봤습니다. 그렇다면 Cluster, Listener, Endpoint 등은 어떻게 등록할 수 있을까요?&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;Envoy에서는 2가지 방식으로 컴포넌트 정보를 등록하거나 수정할 수 있습니다. 첫 번째 방식은 Static 방식이고 두 번째 방식은 Dynamic 방식입니다.&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;style2&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-4-1 Static 등록 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Static 방식은 말 그대로 Envoy 기동 시점에 사용자가 지정한 Config 파일 정보를 기반으로 내부 컴포넌트를 등록하는 방식입니다. 따라서 Envoy가 이해할 수 있는 형태로 내부 컴포넌트 설정을 기술해야합니다. Envoy에서는 YAML 형태로 이를 지정할 수 있으며, 아래 예제를 통해서 간단히 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1660540887488&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address: { address: 127.0.0.1, port_value: 10000 }
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          &quot;@type&quot;: type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          codec_type: AUTO
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: [&quot;*&quot;]
              routes:
              - match: { prefix: &quot;/&quot; }
                route: { cluster: some_service }
          http_filters:
          - name: envoy.filters.http.router
            typed_config:
              &quot;@type&quot;: type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
  - name: some_service
    connect_timeout: 0.25s
    type: STATIC
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: some_service
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: 127.0.0.1
                port_value: 1234&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;해당 설정은 Envoy 공식 문서에 존재하는 예제입니다. 지금까지 살펴본 Envoy 컴포넌트에 대한 개념을 잘 숙지했다면, 위 예제의 큰 구조를 쉽게 파악할 수 있습니다.&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;먼저 Listener를 보면 listener_0이라는 이름으로 1개의 Listener가 등록되어있고, 내부에는 filter_chains 항목을 통해 Network Filter인 HTTP Connection Manager를 등록하는 것을 확인할 수 있습니다. 또한 Routes 설정을 통해 /으로 들어오는 모든 prefix에 대해서 some_service라는 Cluster로 전달하도록 지정했음을 확인할 수 있습니다. 이를 토대로 Listener로 들어오는 모든 HTTP 설정에 대해서 some_service Cluster로 트래픽이 전달되도록 Router Filter가 지정될 것임을 알 수 있습니다.&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;Clusters 항목을 보면, 이전에 Route에서 지정한 some_service 이름으로 등록되어있음을 확인할 수 있고 내부적으로 endpoint를 등록하여 Cluster로 트래픽이 전달되었을 때 내부적으로 어떤 endpoint로 전달할 수 있는지를 알 수 있습니다. 추가적으로 ROUND_ROBIN 정책을 적용하여 트래픽 부하를 고르게 분산하도록 지정되어있음을 확인할 수 있습니다.&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;style2&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-4-2 Dynamic 등록 방식&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Static 등록 방식은 Config 파일에 직접 기술하여 해당 정보를 토대로 envoy proxy를 구성하는 방법입니다. 하지만 Listener, Endpoint와 Cluster 정보가 수시로 바뀌는 상황에서는 Static 등록 방식을 사용하기에 적합하지 않습니다. 그 이유는 변경할 때마다 해당 Config 파일 수정이 필요하며, envoy proxy 또한 reload 해야하기 때문입니다.&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;따라서 envoy proxy에서는 Static 등록 방식 이외에 Dynamic 방식을 제공하여 envoy proxy 기동 중에도 내부 컴포넌트 설정을 변경할 수 있도록 인터페이스를 제공하였습니다. 이를 xDS API라고 부르며, FIle 동기화, REST 혹은 gRPC&amp;nbsp; 방식이 존재합니다. 참고로 아직 살펴보지는 않았지만 istio에서는 gRPC 통신을 이용하여 envoy proxy의 정보를 동적으로 변경합니다.&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;그렇다면 xDS는 어떠한 종류가 있으며, 어떤 컴포넌트와 매칭되어 변경을 수행할 수 있을까요? 이에 대해서 간단히 알아보도록 하겠습니다.&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;1080&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccjw6r/btrJ7NQQGqW/XtcPySDAJU4wD9i1uVX4L0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccjw6r/btrJ7NQQGqW/XtcPySDAJU4wD9i1uVX4L0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccjw6r/btrJ7NQQGqW/XtcPySDAJU4wD9i1uVX4L0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fccjw6r%2FbtrJ7NQQGqW%2FXtcPySDAJU4wD9i1uVX4L0%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;1080&quot; height=&quot;296&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;296&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;이전에 살펴봤던 주요 컴포넌트 Listener, Cluster, Endpoint를 갱신할 수 있는 Discovery Service가 제공됩니다. 따라서 envoy proxy에서 요구하는 spec에 맞게 gRPC 혹은 REST 호출을 보내면 개별 컴포넌트에 해당하는 Discovery Service를 통해서 내부 컴포넌트 설정을 변경할 수 있습니다.&amp;nbsp;&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;1080&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TN223/btrJ7mTBpmM/Gqrrz3q2U4YLFoXv8bK9Jk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TN223/btrJ7mTBpmM/Gqrrz3q2U4YLFoXv8bK9Jk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TN223/btrJ7mTBpmM/Gqrrz3q2U4YLFoXv8bK9Jk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTN223%2FbtrJ7mTBpmM%2FGqrrz3q2U4YLFoXv8bK9Jk%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;1080&quot; height=&quot;440&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;440&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;참고로 istio에서는 위와 같이 중앙에 envoy를 관리하는 Management Server가 있으며, Envoy의 xDS를 통해서 중앙에서 Configuration을 등록과 수정등을 자유롭게 할 수 있습니다.&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;위와 같은 Discovery Service외에도 VHDS, SRDS, LDS, SDS, RTDS, ECDS등 다양한 Discovery Service가 존재하며, 이는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/operations/dynamic_configuration&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;envoy 공식 문서&lt;/a&gt;&lt;/u&gt;&lt;/span&gt;를 통해서 참고하시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ADS&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 xDS 관련하여 또 하나 살펴볼 주요 내용 중 하나인 ADS(Aggregated xDS)에 대해서 살펴보겠습니다. 먼저 ADS는 무엇이고 왜 사용할까요?&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;Envoy 내부 동작 구조에 대해서 자세히 살펴보지 않았지만, 우선 가볍게 짚고 넘어가자면 Envoy 내부의 쓰레딩 모델은 기본적으로 Lock 없이 데이터를 주고 받으며, 데이터 동기화는 Eventually Consistency를 전제로 설계되어있습니다. 즉 이말은 데이터를 전달한다고 해서 바로 반영하는 것은 아니고 완벽한 동기화가 일시에 이루어지지 않음을 의미합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1067&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEcsOG/btrKhlFb1Bi/EemgYnlkHnvG2wdHuFSazK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEcsOG/btrKhlFb1Bi/EemgYnlkHnvG2wdHuFSazK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEcsOG/btrKhlFb1Bi/EemgYnlkHnvG2wdHuFSazK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEcsOG%2FbtrKhlFb1Bi%2FEemgYnlkHnvG2wdHuFSazK%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;1067&quot; height=&quot;698&quot; data-origin-width=&quot;1067&quot; data-origin-height=&quot;698&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;이전에 살펴봤듯이 Endpoint와 Cluster는 의존 관계를 맺고 있음을 확인했습니다. 즉 Cluster는 Endpoint의 논리적 집합이었음을 이전 내용을 통해 확인했습니다.&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;그리고 이러한 상황에서 만약 위와 같이 Endpoint와 Cluster가 추가되어서 이를 갱신하는 상황이 발생한다고 가정했을 때, EDS를 통한 갱신이 CDS보다 먼저 이루어진다면 어떻게 될까요?&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;Envoy 입장에서는 EDS를 통해서 전달된 Endpoint의 대상이 Cluster-A라고 전달받았지만, 아직 CDS를 통해 Cluster 정보를 전달받지 못한 상황이므로 일정 기간 동안에는 Cluster 정보에 대한 동기화가 진행되지 않는 이상현상이 발생됩니다.&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;따라서 이러한 이슈를 해결하고자 등장한 것이 Aggregated Discovery Service입니다.&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;603&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5fSVr/btrKkhpdhAZ/h85kSPGXWFiySvkT6kiF8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5fSVr/btrKkhpdhAZ/h85kSPGXWFiySvkT6kiF8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5fSVr/btrKkhpdhAZ/h85kSPGXWFiySvkT6kiF8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5fSVr%2FbtrKkhpdhAZ%2Fh85kSPGXWFiySvkT6kiF8k%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;603&quot; height=&quot;133&quot; data-origin-width=&quot;603&quot; data-origin-height=&quot;133&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;ADS는 Single gRPC 스트림으로 구성된 서비스로써 envoy에 전달되는 resource의 순서를 적용하려는 사용자를 위해 집계된 xDS를 단일 gRPC 스트림으로 전달할 수 있습니다. 따라서 위의 경우 ADS와 CDS의 의존 관계에 있을 때에도 이를 집계한 결과를 단일 스트림 형태로 envoy 전달하기 때문에 일관성을 달성할 수 있는 특징을 지니고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1661144597666&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  &quot;dynamic_resources&quot;: {
    &quot;lds_config&quot;: {
      &quot;ads&quot;: {},
      &quot;initial_fetch_timeout&quot;: &quot;0s&quot;,
      &quot;resource_api_version&quot;: &quot;V3&quot;
    },
    &quot;cds_config&quot;: {
      &quot;ads&quot;: {},
      &quot;initial_fetch_timeout&quot;: &quot;0s&quot;,
      &quot;resource_api_version&quot;: &quot;V3&quot;
    },
    &quot;ads_config&quot;: {
      &quot;api_type&quot;: &quot;GRPC&quot;,
      &quot;set_node_on_first_message_only&quot;: true,
      &quot;transport_api_version&quot;: &quot;V3&quot;,
      &quot;grpc_services&quot;: [
        {
          &quot;envoy_grpc&quot;: {
            &quot;cluster_name&quot;: &quot;xds-grpc&quot;
          }
        }
      ]
    }
  },&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dynamic_resources는 위와 같이 기존 Config yaml에 위와 같이 설정할 수 있습니다. 예를들어 위와 같이 설정을 지정할 수 있습니다. 내용을 살펴보면 lds와 cds는 ads를 통해서 해당 내용을 전달받을 수 있으며, ads는 xds-grpc 클러스터를 통해서 해당 정보를 가져오도록 지정되어 있습니다.&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;참고로 위 설정은 istio를 통해 배포된 Pod에 속한 envoy proxy의 설정 파일 중 일부를 발췌한 내용이며, 해당 설정에 대한 자세한 내역은 차후 포스팅을 통해 다루어보겠습니다.&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. xDS API&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;xDS API는 istio와 연계하여 Service Discovery를 수행하는데 있어 주요하게 사용됩니다. 따라서 xDS 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;먼저 xDS 지원 방식 부터 살펴보겠습니다. xDS 지원 방식은 총 3가지(File 동기화, HTTP, gRPC)입니다.&amp;nbsp;&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;900&quot; data-origin-height=&quot;296&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMStnV/btrODheWTX9/DKMiKqeoMhhupoZ7A014O0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMStnV/btrODheWTX9/DKMiKqeoMhhupoZ7A014O0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMStnV/btrODheWTX9/DKMiKqeoMhhupoZ7A014O0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMStnV%2FbtrODheWTX9%2FDKMiKqeoMhhupoZ7A014O0%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;480&quot; height=&quot;158&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;296&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;File 동기화 방식은 위와 같이 envoy에서 File의 상태를 관찰하고 설정이 적용된 File에서 변경이 일어났을 경우 envoy에서 이를 인지하여 내부 컴포넌트의 설정을 동기화하는 방식입니다.&amp;nbsp;&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;519&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kGQVR/btrOGvk0mO1/lEtySMAeMxOJ8BLkiDZjH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kGQVR/btrOGvk0mO1/lEtySMAeMxOJ8BLkiDZjH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kGQVR/btrOGvk0mO1/lEtySMAeMxOJ8BLkiDZjH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkGQVR%2FbtrOGvk0mO1%2FlEtySMAeMxOJ8BLkiDZjH1%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;519&quot; height=&quot;125&quot; data-origin-width=&quot;519&quot; data-origin-height=&quot;125&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;반면 gRPC와 HTTP 방식은 config 정보를 전달하는 Management Server가 중앙에 존재합니다. 따라서 envoy에서는 Management Server에 요청하여 config 정보를 전달받고 전달받은 정보를 토대로 자신의 내부 컴포넌트 설정을 동기화합니다.&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;HTTP 방식은 주기적인 polling을 통해서 Management Server로부터 변경된 데이터 항목을 전달받아 갱신합니다. 반면 gRPC는 bidirectional streaming 통신을 통해 데이터를 주고 받는 차이점이 존재합니다. 해당 통신 방법에 대해서 궁금하신 분은 제 블로그 아래 내용을 참고 부탁드립니다.&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;a href=&quot;https://cla9.tistory.com/177?category=993774&quot;&gt;https://cla9.tistory.com/177?category=993774&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure id=&quot;og_1665797899113&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;3. gRPC는 왜 빠를까? (통신 방식) - 2&quot; data-og-description=&quot;서론 지난 포스팅에서는 gRPC에서 사용되는 protobuf와 REST 통신에서 사용되는 JSON 크기와 Serialization/Deserialization 관점에서 성능을 비교해봤습니다. 이번에는 gRPC에서 제공하는 통신 방법에 대해서 &quot; data-og-host=&quot;cla9.tistory.com&quot; data-og-source-url=&quot;https://cla9.tistory.com/177?category=993774&quot; data-og-url=&quot;https://cla9.tistory.com/177&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dfF1Wb/hyQaB8OvA3/LU8Yq3gRIHkpQ4L35UUBO0/img.png?width=294&amp;amp;height=306&amp;amp;face=0_0_294_306,https://scrap.kakaocdn.net/dn/bd3Zs9/hyQazC9TAA/9JiQ9DDS6tTZReLmTl6po0/img.png?width=294&amp;amp;height=306&amp;amp;face=0_0_294_306,https://scrap.kakaocdn.net/dn/buAnXV/hyQaCfzX20/y58luzOLh4R7FPOjIIY2w0/img.png?width=763&amp;amp;height=1535&amp;amp;face=0_0_763_1535&quot;&gt;&lt;a href=&quot;https://cla9.tistory.com/177?category=993774&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://cla9.tistory.com/177?category=993774&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dfF1Wb/hyQaB8OvA3/LU8Yq3gRIHkpQ4L35UUBO0/img.png?width=294&amp;amp;height=306&amp;amp;face=0_0_294_306,https://scrap.kakaocdn.net/dn/bd3Zs9/hyQazC9TAA/9JiQ9DDS6tTZReLmTl6po0/img.png?width=294&amp;amp;height=306&amp;amp;face=0_0_294_306,https://scrap.kakaocdn.net/dn/buAnXV/hyQaCfzX20/y58luzOLh4R7FPOjIIY2w0/img.png?width=763&amp;amp;height=1535&amp;amp;face=0_0_763_1535');&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;3. gRPC는 왜 빠를까? (통신 방식) - 2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서론 지난 포스팅에서는 gRPC에서 사용되는 protobuf와 REST 통신에서 사용되는 JSON 크기와 Serialization/Deserialization 관점에서 성능을 비교해봤습니다. 이번에는 gRPC에서 제공하는 통신 방법에 대해서&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;cla9.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;envoy에서 널리 사용하는 방식은 gRPC 방식이며, istio 또한 gRPC 방식을 통해 xDS 정보를 전달받습니다. 따라서 본 포스팅에서는 gRPC 방식에 보다 초점을 맞추어 살펴보겠습니다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1 xDS gRPC&amp;nbsp;&lt;/h4&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;526&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dVXVHz/btrOSbk7YHN/CCspzzwKuvYJjyJ8fvKMh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dVXVHz/btrOSbk7YHN/CCspzzwKuvYJjyJ8fvKMh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dVXVHz/btrOSbk7YHN/CCspzzwKuvYJjyJ8fvKMh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVXVHz%2FbtrOSbk7YHN%2FCCspzzwKuvYJjyJ8fvKMh0%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;526&quot; height=&quot;232&quot; data-origin-width=&quot;526&quot; data-origin-height=&quot;232&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;앞서 envoy에서 gRPC를 활용한 xDS 방식은 envoy와 config를 전달하는 Management Server 사이에 bidirectional streaming 통신을 사용한다고 설명했습니다. 따라서 해당 방식은 Connection이 끊기지 않고 지속 연결된 상태라고 봐도 무방합니다.&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;이러한 상황에서 처음 Management Server에 envoy가 연결되면, 위와 같이 Discovery Request를 Management Server에 전달합니다. 그러면 Management Server에서는 envoy가 요청하는 Config에 대해서 전체 목록을 전달하게되고, envoy는 해당 설정을 전달받아 Config 업데이트를 수행합니다.&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;673&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCbUd1/btrOCq40dK8/Z374fBionLeEW5ca3zTUG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCbUd1/btrOCq40dK8/Z374fBionLeEW5ca3zTUG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCbUd1/btrOCq40dK8/Z374fBionLeEW5ca3zTUG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCbUd1%2FbtrOCq40dK8%2FZ374fBionLeEW5ca3zTUG0%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;673&quot; height=&quot;354&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;354&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;이후 Config 업데이트가 완료되면, envoy는 이전에 Management Server로부터 전달받은 Config 항목에 대한 응답을 전달합니다. 만약 Config 내용을 정상적으로 업데이트를 수행했을 경우에는 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;ACK&lt;/span&gt;&lt;/b&gt;를 응답하고 그렇지 않을 경우에는 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;NACK&lt;/span&gt;&lt;/b&gt;를 응답합니다. (※&amp;nbsp; ACK와 NACK의 구조와 동작방식에 대해서는 &lt;a href=&quot;https://www.envoyproxy.io/docs/envoy/latest/api-docs/xds_protocol#ack&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;envoy 공식 문서&lt;/u&gt;&lt;/span&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;이때 응답 메시지는 별개의 포맷을 활용하지 않고 다음 Discovery Request를 전달할 때, 응답을 포함하여 전달됩니다. 해당 Discovery Request 메시지는 Config 업데이트 완료 이후 Management Server에 Config가 변경되었을 때, 동기화된 Config 내역을 다시 전달받기 위해 요청하는 메시지입니다. 따라서 Management Server에 Config가 변경되거나 새로운 Resource가 추가되었을 경우 envoy에게 Discovery Response를 전달함으로써 동기화 작업이 이루어집니다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-2 SotW(State of the world), Delta xDS&lt;/h4&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;673&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cTSiF3/btrORYtiBWn/xtkXk8stzfPdfKFOZKftgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cTSiF3/btrORYtiBWn/xtkXk8stzfPdfKFOZKftgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cTSiF3/btrORYtiBWn/xtkXk8stzfPdfKFOZKftgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcTSiF3%2FbtrORYtiBWn%2FxtkXk8stzfPdfKFOZKftgk%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;673&quot; height=&quot;354&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;354&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;지금까지 envoy gRPC 동작 과정에 대해서 가볍게 살펴봤는데, 이번에는 Management Server에서 Discovery Response를 응답 메시지를 전달하는데 있어서 선택할 수 있는 2가지 방법에 대해서 살펴보겠습니다.&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;첫 번째 방법은 SotW(State of the world) 방식입니다.&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;706&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZbazL/btrOCZTq8Cd/3fsv4nhoh5OaatDWL9N3nk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZbazL/btrOCZTq8Cd/3fsv4nhoh5OaatDWL9N3nk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZbazL/btrOCZTq8Cd/3fsv4nhoh5OaatDWL9N3nk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZbazL%2FbtrOCZTq8Cd%2F3fsv4nhoh5OaatDWL9N3nk%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;706&quot; height=&quot;280&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;280&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;가령 envoy가 Cluster 정보를 동기화 하기 위해 xDS로 연결되었으며 이미 한차례 Cluster 동기화되었다고 가정해봅시다. 이때 SotW 방식은 동기화된 Cluster 정보 중 하나가 변경되었을 경우에 전체 Cluster 정보를 전달하는 방식을 의미합니다. 해당 방식은 구현이 간단한 반면에 전체 데이터 중 일부만 변경되었음에도 불구하고 전체 정보를 전달하기 때문에 많은 Network overhead가 발생할 수 있습니다.&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;181&quot; data-origin-height=&quot;105&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beTVxu/btrORQvtIEE/zrbGE7WbeB8Bd2yQm8mJxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beTVxu/btrORQvtIEE/zrbGE7WbeB8Bd2yQm8mJxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beTVxu/btrORQvtIEE/zrbGE7WbeB8Bd2yQm8mJxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeTVxu%2FbtrORQvtIEE%2FzrbGE7WbeB8Bd2yQm8mJxk%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;181&quot; height=&quot;105&quot; data-origin-width=&quot;181&quot; data-origin-height=&quot;105&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;참고로 이때 DiscoveryResponse 응답 포맷은 위와같으며, 여기서 resources를 통해서 전체 resource 정보가 전달됩니다.&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;두 번째 방법은 Delta 방식입니다.&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;706&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/75LDd/btrOUM5UrCQ/nddZvwV5oKwwYGaHPyuy70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/75LDd/btrOUM5UrCQ/nddZvwV5oKwwYGaHPyuy70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/75LDd/btrOUM5UrCQ/nddZvwV5oKwwYGaHPyuy70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F75LDd%2FbtrOUM5UrCQ%2FnddZvwV5oKwwYGaHPyuy70%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;706&quot; height=&quot;280&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;280&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;해당 방식은 변경된 Config 정보만을 선별하여 전체를 전달하는 것이 아니라 변경분 (Delta 혹은 incremental)만 전달하는 방식을 의미합니다.&amp;nbsp;&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;224&quot; data-origin-height=&quot;127&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p9fZy/btrOR7wu1Ar/hmzBtH0BIzE0mcXQWsWW91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p9fZy/btrOR7wu1Ar/hmzBtH0BIzE0mcXQWsWW91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p9fZy/btrOR7wu1Ar/hmzBtH0BIzE0mcXQWsWW91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp9fZy%2FbtrOR7wu1Ar%2FhmzBtH0BIzE0mcXQWsWW91%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;127&quot; data-origin-width=&quot;224&quot; data-origin-height=&quot;127&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;이때 Delta Discovery Response 응답 포맷은 위와 같으며, resources 항목은 변경된 항목만 추가됩니다. 또한 기존에 존재하는 Resource가 삭제되었을 경우에는 removed_resources를 통해서 이를 envoy에게 전달합니다.&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;지금까지 SotW, Delta 두 가지 방식에 대해서 살펴봤습니다. 그렇다면 두 방식은 어떻게 지정할 수 있을까요?&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;1779&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9P2g6/btrOS2OLtIM/zqz51BzZWRslyfUNcIhUbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9P2g6/btrOS2OLtIM/zqz51BzZWRslyfUNcIhUbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9P2g6/btrOS2OLtIM/zqz51BzZWRslyfUNcIhUbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9P2g6%2FbtrOS2OLtIM%2Fzqz51BzZWRslyfUNcIhUbK%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;1779&quot; height=&quot;936&quot; data-origin-width=&quot;1779&quot; data-origin-height=&quot;936&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;위 설정과 같이 envoy configuration에서 api_type을 지정할 때, GRPC로 지정 혹은 DELTA_GRPC로 지정하면 입력 값에 따라서 동작 방식이 결정됩니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;877&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S8O3G/btrOEhr4EzC/llaPYhCHW5ZkH0fOYwYRd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S8O3G/btrOEhr4EzC/llaPYhCHW5ZkH0fOYwYRd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S8O3G/btrOEhr4EzC/llaPYhCHW5ZkH0fOYwYRd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS8O3G%2FbtrOEhr4EzC%2FllaPYhCHW5ZkH0fOYwYRd0%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;877&quot; height=&quot;198&quot; data-origin-width=&quot;877&quot; data-origin-height=&quot;198&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;지금까지 gRPC 방식에서 Config 정보를 전달하는 두 가지 방법에 대해서 살펴봤습니다. 그렇다면 istio에서는 어떤 방식을 기본적으로 사용할까요? istio에서는 기본적으로는 SotW 방식을 사용하고 있으며, 사이드카 컨테이너를 주입할 때 사용자가 지정한 ISTIO_DELTA_XDS 값을 통해서 Delta 방식으로 변경할 수 있습니다. 하지만 현재는 값을 변경한다고 할지라도 실제 데이터를 전달할 때 Delta 값만을 전달하지 않습니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Envoy 내부 주요 컴포넌트 및 설정 방식에 대해 살펴봤습니다. istio는 envoy를 얼만큼 잘 이해하고 있느냐에 따라서 istio에 대한 전문성을 확보한다고 생각합니다. 또한 istio가 문제가 생겼을 때 트러블 슈팅할 때는 envoy 설정에 대한 이해를 기반으로 진행되어야합니다. 따라서 이번 포스팅의 내용은 간략하지만 중요한 부분을 다루고 있습니다.&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;다음 포스팅에서는 Envoy 내부 구조에 대해서 조금 더 자세히 알아보도록 하겠습니다.&lt;/p&gt;</description>
      <category>MSA/Istio</category>
      <category>Envoy</category>
      <category>envoy cluster</category>
      <category>envoy listener</category>
      <category>envoy router</category>
      <category>envoy 구조</category>
      <category>envoy 내부</category>
      <category>envoy 아키텍처</category>
      <category>Istio</category>
      <category>istio 구조</category>
      <category>istio 아키텍처</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/191</guid>
      <comments>https://cla9.tistory.com/191#entry191comment</comments>
      <pubDate>Wed, 17 May 2023 11:26:22 +0900</pubDate>
    </item>
    <item>
      <title>4. kotlin 환경에서 gRPC 설정하기</title>
      <link>https://cla9.tistory.com/178</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 gRPC에 대한 소개 및 해당 기술이 가진 이점에 대해서 살펴봤습니다. 이번 포스팅에서는 kotlin 환경에서 gRPC 관련 기본 설정 셋업하는 방법에 대해서 다루어보도록 하겠습니다. 프로젝트 설정은 gradle 기반의 기본 Kotlin 빈 프로젝트는 생성되었음을 가정하고 진행하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. gradle 설정 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle.kts&lt;/p&gt;
&lt;pre id=&quot;code_1646889107581&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import com.google.protobuf.gradle.*

plugins {
    ...(중략)...
    id(&quot;com.google.protobuf&quot;) version &quot;0.8.13&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 설정할 것은 protobuf 관련 plugin을 설정하는 것입니다. 위 내용을 gradle.kts 파일 plugins 항목에 추가합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle.kts&lt;/p&gt;
&lt;pre id=&quot;code_1646889207467&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...(중략)...

val grpcVersion = &quot;3.19.4&quot;
val grpcKotlinVersion = &quot;1.2.1&quot;
val grpcProtoVersion = &quot;1.44.1&quot;

dependencies{
    implementation(&quot;io.grpc:grpc-kotlin-stub:$grpcKotlinVersion&quot;)
    implementation(&quot;io.grpc:grpc-protobuf:$grpcProtoVersion&quot;)
    implementation(&quot;com.google.protobuf:protobuf-kotlin:$grpcVersion&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 다음에는 protobuf 관리와 stub을 자동으로 생성해주는 라이브러리 의존성을 위와같이 추가합니다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle.kts&lt;/p&gt;
&lt;pre id=&quot;code_1646889304409&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...(중략)...
sourceSets{
    getByName(&quot;main&quot;){
        java {
            srcDirs(
                &quot;build/generated/source/proto/main/java&quot;,
                &quot;build/generated/source/proto/main/kotlin&quot;
            )
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 내용은 build 이후에 Stub 클래스가 생성되는 directory에 대해서 target으로 추가하기 위한 설정입니다. 해당 설정을 통해 소스 내에서 Stub 클래스 참조가 가능합니다.&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;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;build.gradle.kts&lt;/p&gt;
&lt;pre id=&quot;code_1646889401442&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...(중략)...

protobuf {
    protoc {
        artifact = &quot;com.google.protobuf:protoc:$grpcVersion&quot;
    }
    plugins {
        id(&quot;grpc&quot;) {
            artifact = &quot;io.grpc:protoc-gen-grpc-java:$grpcProtoVersion&quot;
        }
        id(&quot;grpckt&quot;) {
            artifact = &quot;io.grpc:protoc-gen-grpc-kotlin:$grpcKotlinVersion:jdk7@jar&quot;
        }
    }
    generateProtoTasks {
        all().forEach {
            it.plugins {
                id(&quot;grpc&quot;)
                id(&quot;grpckt&quot;)
            }
            it.builtins {
                id(&quot;kotlin&quot;)
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 설정할 내용은 build 시점에 protobuf를 생성하기 위한 task를 추가하는 작업입니다. 해당 설정을 통해 Java Stub 파일과 Kotlin Stub파일을 생성할 수 있습니다.&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;기본 설정이 모두 끝났으면 gradle refresh를 통해서 설정을 마무리합니다.&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 임시 Protobuf 생성 테스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정이 완료되었으면, Protobuf를 만들어보고 정상적으로 Stub 클래스가 생성되는지 확인해보도록 하겠습니다.&amp;nbsp;&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;200&quot; data-origin-height=&quot;147&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3TyiZ/btrvDjuPIAB/tC1dtyu0GdesZjck7KMCWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3TyiZ/btrvDjuPIAB/tC1dtyu0GdesZjck7KMCWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3TyiZ/btrvDjuPIAB/tC1dtyu0GdesZjck7KMCWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3TyiZ%2FbtrvDjuPIAB%2FtC1dtyu0GdesZjck7KMCWK%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;147&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;147&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;먼저 main 디렉토리 하위에 proto 디렉토리를 생성합니다.&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;190&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IAFEq/btrvCnq1659/9eHkGLn8g3nB3kF2ZjngaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IAFEq/btrvCnq1659/9eHkGLn8g3nB3kF2ZjngaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IAFEq/btrvCnq1659/9eHkGLn8g3nB3kF2ZjngaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIAFEq%2FbtrvCnq1659%2F9eHkGLn8g3nB3kF2ZjngaK%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;190&quot; height=&quot;126&quot; data-origin-width=&quot;190&quot; data-origin-height=&quot;126&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;생성된 proto 디렉토리 하위에 test.proto를 생성합니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;248&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kyv1n/btrvwJ2bAvJ/GuRKcD0zrrxuemhGUdOa4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kyv1n/btrvwJ2bAvJ/GuRKcD0zrrxuemhGUdOa4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kyv1n/btrvwJ2bAvJ/GuRKcD0zrrxuemhGUdOa4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fkyv1n%2FbtrvwJ2bAvJ%2FGuRKcD0zrrxuemhGUdOa4K%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;248&quot; height=&quot;158&quot; data-origin-width=&quot;248&quot; data-origin-height=&quot;158&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;간단한 테스트를 위해 위 내용을 기입합니다.&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;306&quot; data-origin-height=&quot;275&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cl3Ay3/btrvwI92mex/C6Gq6LTuxtGCcTJOcb7wFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cl3Ay3/btrvwI92mex/C6Gq6LTuxtGCcTJOcb7wFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cl3Ay3/btrvwI92mex/C6Gq6LTuxtGCcTJOcb7wFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcl3Ay3%2FbtrvwI92mex%2FC6Gq6LTuxtGCcTJOcb7wFk%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;306&quot; height=&quot;275&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;275&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;gradle 탭에서 build 버튼을 클릭합니다.&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;306&quot; data-origin-height=&quot;279&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VncB1/btrvDkAyqX5/TzSkkt2z0IUUoO6JuPWyA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VncB1/btrvDkAyqX5/TzSkkt2z0IUUoO6JuPWyA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VncB1/btrvDkAyqX5/TzSkkt2z0IUUoO6JuPWyA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVncB1%2FbtrvDkAyqX5%2FTzSkkt2z0IUUoO6JuPWyA1%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;306&quot; height=&quot;279&quot; data-origin-width=&quot;306&quot; data-origin-height=&quot;279&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;build가 정상적으로 완료되면, 위 그림과 같이 build 폴더가 생깁니다. 이를 확인해봅시다.&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;396&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pBChE/btrvvuRImSm/ikxrO0oiN5Sa9vrOQRyrjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pBChE/btrvvuRImSm/ikxrO0oiN5Sa9vrOQRyrjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pBChE/btrvvuRImSm/ikxrO0oiN5Sa9vrOQRyrjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpBChE%2FbtrvvuRImSm%2FikxrO0oiN5Sa9vrOQRyrjk%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;383&quot; data-origin-width=&quot;396&quot; data-origin-height=&quot;383&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;Stub 클래스가 정상 생성되었는지 확인을 위해 build &amp;gt; generated &amp;gt; source &amp;gt; proto &amp;gt; main 하위에 java와 kotlin 패키지를 열어봅니다. 만약 정상적으로 build가 완료되었으면, 위 그림과 같이 Test Stub 클래스가 생성된 것을 확인할 수 있습니다.&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;332&quot; data-origin-height=&quot;165&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/llJpf/btrvy2OFoqB/XypJ1rsrdCC8DZsmCE7g10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/llJpf/btrvy2OFoqB/XypJ1rsrdCC8DZsmCE7g10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/llJpf/btrvy2OFoqB/XypJ1rsrdCC8DZsmCE7g10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FllJpf%2Fbtrvy2OFoqB%2FXypJ1rsrdCC8DZsmCE7g10%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;165&quot; data-origin-width=&quot;332&quot; data-origin-height=&quot;165&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;생성된 Stub 클래스를 프로그램내에서 정상 사용할 수 있는지 여부를 테스트하기 위해 위와 같이 별도 main 함수를 만들어 생성 가능 여부를 확인해봅니다.&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;만약 클래스 참조가 불가하다면, build.gradle.kts 파일에 sourceSets 내 경로가 일치하는지 확인 후 수정합니다.&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;지금까지 과정이 모두 정상적이라면, proto 파일을 만들고 이를 build하고 Stub 클래스를 생성 후 프로그램 참조하는 모든 과정을 가볍게 훑어볼 수 있었습니다. 테스트를 위해 사용되었던 test.proto 파일은 더 이상 필요하지 않으므로 제거해도 좋습니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅은 Kotlin 기반에서 gRPC 설정 하는 방법에 대해서 알아봤습니다. 다음 포스팅부터는 본격적으로 protobuf 사용법에 대해서 알아보겠습니다.&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>MSA/gRPC</category>
      <category>GRPC</category>
      <category>gRPC intellij</category>
      <category>gRPC kotlin</category>
      <category>gRPC kotlin 설정</category>
      <category>gRPC 구조</category>
      <category>gRPC 프로젝트 생성</category>
      <category>protobuf</category>
      <category>protocol buffer</category>
      <category>stub</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/178</guid>
      <comments>https://cla9.tistory.com/178#entry178comment</comments>
      <pubDate>Thu, 10 Mar 2022 19:45:54 +0900</pubDate>
    </item>
    <item>
      <title>3. gRPC는 왜 빠를까? (통신 방식) - 2</title>
      <link>https://cla9.tistory.com/177</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅에서는 gRPC에서 사용되는 protobuf와 REST 통신에서 사용되는 JSON 크기와 Serialization/Deserialization 관점에서 성능을 비교해봤습니다. 이번에는 gRPC에서 제공하는 통신 방법에 대해서 살펴보고 REST 단건 통신과 비교하여 송/수신 시간을 비교해보겠습니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. gRPC 통신 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 HTTP 2.0을 기반으로 구성되어있기 때문에 Multiplexing으로 연결을 구성할 수 있습니다. 따라서 단일 Connection으로 순서의 상관없이 여러 응답을 전달받을 수 있는 Streaming 처리가 가능합니다. gRPC는 총 4가지의 통신 방법을 지원하며 그 중 3가지 방식은 Streaming 처리 방식입니다. 지금부터 하나씩 살펴보겠습니다.&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Unary&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 방식은 Unary 통신 방식입니다.&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;973&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl2gts/btrvuP8651f/KSEcpGmKroJDisljKgxPl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl2gts/btrvuP8651f/KSEcpGmKroJDisljKgxPl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl2gts/btrvuP8651f/KSEcpGmKroJDisljKgxPl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbl2gts%2FbtrvuP8651f%2FKSEcpGmKroJDisljKgxPl0%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;480&quot; height=&quot;124&quot; data-origin-width=&quot;973&quot; data-origin-height=&quot;251&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;이는 가장 단순한 서비스 형태로써 클라이언트가 단일 요청 메시지를 보내고 서버는 이에 단일 응답을 내려보내주는 방식입니다. 일반적으로 사용하는 REST API를 통해 주고 받는 Stateless 방식과 동일하다고 볼 수 있으며, 개념적으로 이해하기 쉽습니다.&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;그렇다면 gRPC의 Unary 통신과 REST의 성능을 비교해보면 어떤차이를 보일까요? 테스트 시나리오를 기반으로 두 통신방법을 비교해보록 하겠습니다.&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;1175&quot; data-origin-height=&quot;363&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q1Rnc/btrvvC2Bdae/lR0IBQfYXt4JZtw8zRkdM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q1Rnc/btrvvC2Bdae/lR0IBQfYXt4JZtw8zRkdM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q1Rnc/btrvvC2Bdae/lR0IBQfYXt4JZtw8zRkdM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq1Rnc%2FbtrvvC2Bdae%2FlR0IBQfYXt4JZtw8zRkdM0%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;640&quot; height=&quot;198&quot; data-origin-width=&quot;1175&quot; data-origin-height=&quot;363&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;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: 100%;&quot;&gt;1. 사용자를 등록하는 서비스가 있다고 가정한다.&lt;br /&gt;2. 10, 100 등 10만까지 10의 거듭 제곱 형태로 delay없이 요청 횟수를 늘리면서 REST와 gRPC의 응답 총 시간을 구한다.&lt;br /&gt;3. &lt;span style=&quot;background-color: #ffffff; color: #000000;&quot;&gt;테스트 시작전 warm up을 위해 50회의 요청 수행 후 테스트를 진행한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 시나리오를 기반으로 Unary 통신을 구현해보겠습니다.&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;pre id=&quot;code_1646728144472&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

import &quot;google/protobuf/empty.proto&quot;;

option java_multiple_files = true;
option java_package = &quot;grpc.polar.penguin&quot;;

message Address{
    string city = 1;
    string zip_code = 2;
}

message Person{
    string name = 2;
    int32 age = 3;
    repeated string hobbies = 4;
    optional Address address = 5;
}

service PersonService {
    rpc register(Person) returns (google.protobuf.Empty);
}&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;Protobuf는 위와 같이 디자인했습니다. message 포맷은 이전 포스팅에서 설계 내용과 동일합니다. 여기서 새로 추가된 항목은 service 부분입니다. 추가된 내용을 살펴보면 인자로 Person 타입을 입력받고 반환 값은 없으므로 Empty를 지정하였습니다.&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;이번 포스팅 내용은 통신 방법에 대한 설명이므로 syntax 설명은 향후 다른 포스팅 내용으로 다루겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646728193401&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PersonGrpcService : PersonServiceGrpcKt.PersonServiceCoroutineImplBase() {
    override suspend fun register(request: Person): Empty {
        //TODO : request 처리
        return Empty.getDefaultInstance()
    }
}&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;Proto 파일 디자인 후 Build하면 Stub 클래스가 자동 생성됩니다. 위 코드는 gRPC 서비스 처리를 구현하기 위해 &lt;span&gt;Stub 클래스인&lt;/span&gt;&amp;nbsp;PersonServiceCoroutineImplBase을 상속받아 구현한 코드입니다. 테스트 시나리오에서는 전달받은 Person 객체를 따로 저장하거나 처리하지 않고 Empty 객체를 반환하도록 구현하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646749940092&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    val server = ServerBuilder.forPort(6565)
        .addService(PersonGrpcService())
        .build()

    server.start()
    server.awaitTermination()
}&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;Server 기동 시에 Service를 등록 시켜서 Client의 요청이 들어왔을 경우에 해당 Service로 Routing 하도록 설정합니다. 이후 Server를 기동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646728214592&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    val channel = ManagedChannelBuilder.forAddress(&quot;localhost&quot;, 6565)
        .usePlaintext()
        .build()

    val stub = PersonServiceGrpc.newBlockingStub(channel)

    execute(stub, 50) //warm up phase

    val base = 10.0
    val dec = DecimalFormat(&quot;#,###&quot;)
    for (exponent in 1..5) {
        val iterCount = base.pow(exponent).toInt()
        val time = measureTimeMillis {
            execute(stub, iterCount)
            println(&quot;count : ${dec.format(iterCount)}&quot;)
        }
        println(&quot;elapsed time $time ms&quot;)
        println(&quot;------------------------------------&quot;)
    }
    
    channel.shutdown()
}&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;Unary 테스트를 위한 client 코드는 위와같습니다. Server를 localhost의 6565 포트에서 기동중이므로 해당 요청에 대한 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;이후 proto 파일 Build 과정에서 생성된 PersonServiceGrpc 내에 있는 BlockingStub 객체를 생성 해서 해당 Channel에 Binding 합니다. Channel에 Binding 한 다음에는 Stub 객체의 메소드를 호출하면 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;stub 객체까지 만들고 나면, 10 ~ 10만번까지 10의 거듭제곱 형태로 늘려가면서 gRPC Unary 통신을 수행 후 총 수행 시간을 출력합니다.&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;위 코드에서 실질적으로 gRPC를 호출하는 부분은 execute 함수입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646749857406&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun execute(stub: PersonServiceGrpc.PersonServiceBlockingStub, count: Int) {
    repeat(IntRange(1, count).count()) {
        stub.register(
            person {
                name = &quot;kevin&quot;
                age = (1..50).random()
                address = address {
                    city = &quot;seoul&quot;
                    zipCode = &quot;123456&quot;
                }
                hobbies.addAll(listOf(&quot;foot ball&quot;, &quot;basket ball&quot;))
            }
        )
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;execute 함수를 살펴보면 위와 같이 iteration count를 인자로 전달받고 그 횟수만큼 gRPC 요청을 보내는 것을 확인할 수 있습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AeuWR/btrvtccmaGk/YaFdEd740t9ruVkEGlgPS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AeuWR/btrvtccmaGk/YaFdEd740t9ruVkEGlgPS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AeuWR/btrvtccmaGk/YaFdEd740t9ruVkEGlgPS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAeuWR%2FbtrvtccmaGk%2FYaFdEd740t9ruVkEGlgPS1%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;294&quot; height=&quot;306&quot; data-origin-width=&quot;294&quot; data-origin-height=&quot;306&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;프로그램을 실행하면 위와 같이 Unary 요청 수행 결과를 확인할 수 있습니다.&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;이번에는 REST 통신을 통해 같은 횟수를 반복했을 때 Unary 통신과 비교하여 총 수행시간이 얼만큼의 차이가 있는지를 비교해보도록 하겠습니다. 이때 Unary 테스트 또한 단일 Channel에서 Blocking 방식으로 수행시간을 측정하였으므로 REST 통신 또한 같은 방법으로 테스트를 진행하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646728234710&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class PersonDto(
    val name : String,
    val age : Int,
    val hobbies : List&amp;lt;String&amp;gt;? = null,
    val address : AddressDto? = null
)

data class AddressDto(
    val city : String,
    val zipCode : String
)&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;JSON으로 입력받을 DTO를 위와 같이 디자인합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646728250518&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
class PersonController(private val service: PersonService) {
    @PostMapping(&quot;/person&quot;)
    suspend fun register(@RequestBody person : PersonDto) {
        //TODO : request 처리
    }
}&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;REST Controller 코드는 위와 같습니다. gRPC 서비스 코드에서도 인자를 전달받아 아무런 처리를 하지 않았기 때문에 마찬가지로 요청만 전달받고 아무 처리를 수행하지 않도록 구성하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646728286600&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
class RegisterTest : CommandLineRunner {
    override fun run(vararg args: String?) {
        val client = WebClient.builder()
            .build()

        execute(client, 50) // warm up phase

        val base = 10.0
        val dec = DecimalFormat(&quot;#,###&quot;)
        for (exponent in 1..5) {
            val iterCount = base.pow(exponent).toInt()
            val time = measureTimeMillis {
                execute(client, iterCount)
                println(&quot;count : ${dec.format(iterCount)}&quot;)
            }
            println(&quot;elapsed time $time ms&quot;)
            println(&quot;------------------------------------&quot;)
        }
    }

    private fun execute(client: WebClient, count: Int) {
        repeat(IntRange(1, count).count()) {
            client.post().uri(&quot;localhost:8080/person&quot;)
                .bodyValue(
                    PersonDto(
                        name = &quot;kevin&quot;,
                        age = (1..50).random(),
                        address = AddressDto(city = &quot;seoul&quot;, zipCode = &quot;123456&quot;),
                        hobbies = listOf(&quot;foot ball&quot;, &quot;basket ball&quot;)
                    )
                )
                .retrieve()
                .bodyToMono(Void::class.java)
                .block()
        }
    }
}&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;Client 수행 프로그램은 위와 같습니다. gRPC 테스트 코드와 크게 다르지 않으며, 차이점이 있다면 Stub 객체를 사용한 것이 아닌 Webclient를 사용한 부분입니다.&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;297&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cYGbt0/btrvqjiTQKr/zJCKdkCrg71jDKr6EJtTfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cYGbt0/btrvqjiTQKr/zJCKdkCrg71jDKr6EJtTfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cYGbt0/btrvqjiTQKr/zJCKdkCrg71jDKr6EJtTfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcYGbt0%2FbtrvqjiTQKr%2FzJCKdkCrg71jDKr6EJtTfK%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;297&quot; height=&quot;312&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;312&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;Client 코드를 수행하면 위와 같은 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;횟수&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;REST&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;gRPC(Unary)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;성능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;23 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;14 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;1.64배&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;100&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;165 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;101 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;1.63배&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;1,000&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;1,000 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;694 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;1.44배&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;10,000&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;4,109 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;2,132 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;1.92배&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;100,000&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;41,491 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;13,768 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;3.01배&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;결과를 살펴보면, Iteration 횟수가 증가할 수록 그 차이가 벌어지는 것을 확인할 수 있습니다. 격차가 벌어진 이유는 다양한 이유가 있지만 Protobuf의 Serialization &amp;amp; Deserialization이 가장 큰 영향을 미치지 않았을까 생각합니다.&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;이번에는 네트워크 패킷을 통해서 REST와 gRPC의 통신 과정을 비교 해보겠습니다. 비교를 위해서 사용자 등록을 5회만 수행 후 종료한 내용을 확인해보도록 하겠습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z2QgE/btrvu12rAaM/jMZB0pmNdWd3KFkVAoeqPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z2QgE/btrvu12rAaM/jMZB0pmNdWd3KFkVAoeqPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z2QgE/btrvu12rAaM/jMZB0pmNdWd3KFkVAoeqPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz2QgE%2Fbtrvu12rAaM%2FjMZB0pmNdWd3KFkVAoeqPk%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;777&quot; height=&quot;629&quot; data-origin-width=&quot;777&quot; data-origin-height=&quot;629&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;REST 통신을 5회 수행하였을 때, 네트워크 흐름을 표시하면 위 그림과 같습니다. 자세히보면 REST 통신은 HTTP 1.1을 사용한 것을 알 수 있고 SYN, ACK와 FIN, ACK가 매 요청마다 보이지 않는 것으로 보아 Connection을 매번 요청하지 않았음을 확인할 수 있습니다.&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;763&quot; data-origin-height=&quot;1535&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WyXms/btrvocK0zfS/ShYTGqdN0c7Hwe6IGJzKSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WyXms/btrvocK0zfS/ShYTGqdN0c7Hwe6IGJzKSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WyXms/btrvocK0zfS/ShYTGqdN0c7Hwe6IGJzKSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWyXms%2FbtrvocK0zfS%2FShYTGqdN0c7Hwe6IGJzKSk%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;763&quot; height=&quot;1535&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;1535&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;이번에는 gRPC Unary 통신 결과입니다. REST에서는 HTTP 1.1 방식이었던 것과 달리 예상대로 HTTP 2.0으로 통신을 수행한 것을 확인할 수 있습니다.&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;gRPC에서 Unary 통신은 HTTP 2.0 Stream으로 데이터를 전송합니다. 따라서 위 패킷 내용을 살펴보면, Stream 통신에 있어서 필요한 데이터 흐름을 파악할 수 있습니다.&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;가령 WINDOW_UPDATE를 통해서 Client가 수신할 수 있는 Byte 수를 Server에 알려줘 해당 정보를 기반으로 Flow control이 가능하도록 사전 설정하는 것을 확인할 수 있습니다. 또한 PING 패킷의 경우는 연결된 Channel 에서 사용중인 Connection liveness를 체크합니다. 만약 PING 단계에서 정상 응답을 수신 받지 못하면, Connection을 끊습니다. 이후 Connection 재생성을 통해 다시 연결할 수 있습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;21&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsmLCJ/btrvwjPPnvl/R0oU0OB0gY2IQOMOVRKUkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsmLCJ/btrvwjPPnvl/R0oU0OB0gY2IQOMOVRKUkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsmLCJ/btrvwjPPnvl/R0oU0OB0gY2IQOMOVRKUkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsmLCJ%2FbtrvwjPPnvl%2FR0oU0OB0gY2IQOMOVRKUkK%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;655&quot; height=&quot;21&quot; data-origin-width=&quot;655&quot; data-origin-height=&quot;21&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;619&quot; data-origin-height=&quot;659&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v2YlS/btrvyll6DMB/vPA9ZoXw5I7gObR4Bxiutk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v2YlS/btrvyll6DMB/vPA9ZoXw5I7gObR4Bxiutk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v2YlS/btrvyll6DMB/vPA9ZoXw5I7gObR4Bxiutk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv2YlS%2Fbtrvyll6DMB%2FvPA9ZoXw5I7gObR4Bxiutk%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;480&quot; height=&quot;511&quot; data-origin-width=&quot;619&quot; data-origin-height=&quot;659&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림은 요청 패킷을 구조화한 모습입니다.&lt;/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;Header를 살펴보면, Header의 길이 그리고 Header의 종류 flag가 보입니다. 그리고 Stream ID가 표시된 것을 볼 수 있는데, 이는 HTTP Stream 내에서 사용되는 Stream 메시지 별 Unique ID 입니다. Client에서 보내는 메시지는 Stream ID가 홀수개로 증가합니다.&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;Header에는 그 밖에 요청 Path 정보 및 Schema, Content-type이 표시됩니다. 내부적으로 요청은 POST로 요청되는 것을 확인할 수 있습니다.&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;Data 영역에는 실제 전달되는 데이터와 Flag등을 전달합니다. Unary 통신의 경우 gRPC Stream 요청은 아니므로 Flag에는 End Stream으로 지정된 것을 확인할 수 있습니다.&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;497&quot; data-origin-height=&quot;21&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7ppdj/btrvAbwx1Cr/8QEiLWnb4MKcMkShx4jRe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7ppdj/btrvAbwx1Cr/8QEiLWnb4MKcMkShx4jRe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7ppdj/btrvAbwx1Cr/8QEiLWnb4MKcMkShx4jRe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7ppdj%2FbtrvAbwx1Cr%2F8QEiLWnb4MKcMkShx4jRe1%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;497&quot; height=&quot;21&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;21&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;1866&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/catyQj/btrvvDnyvUj/zQkEk8wwxZWr2ojbv3fAw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/catyQj/btrvvDnyvUj/zQkEk8wwxZWr2ojbv3fAw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/catyQj/btrvvDnyvUj/zQkEk8wwxZWr2ojbv3fAw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcatyQj%2FbtrvvDnyvUj%2FzQkEk8wwxZWr2ojbv3fAw0%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;1866&quot; height=&quot;415&quot; data-origin-width=&quot;1866&quot; data-origin-height=&quot;415&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;응답 패킷은 크게 3가지 부분으로 이루어져있습니다. 첫번째는 요청에 대한 응답헤더이고, 두 번째는 응답에 대한 데이터 마지막으로는 trailer 헤더로 구성되어있습니다.&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;그렇다면 위 5개의 데이터 전송 흐름에서 gRPC 패킷은 어떤 특징을 지니고 있을까요?&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;1106&quot; data-origin-height=&quot;127&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L47dY/btrvvalyhhN/8mAIpitQEhEAhmAT8zWH20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L47dY/btrvvalyhhN/8mAIpitQEhEAhmAT8zWH20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L47dY/btrvvalyhhN/8mAIpitQEhEAhmAT8zWH20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL47dY%2FbtrvvalyhhN%2F8mAIpitQEhEAhmAT8zWH20%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;1106&quot; height=&quot;127&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;127&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;요청 패킷을 살펴보면, Header 길이가 최초 메시지를 보낼 때보다 크기가 줄어든 것을 확인할 수 있습니다. 또한 Stream ID는 홀수 번호로 순차 증가한 것을 확인할 수 있습니다.&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;1106&quot; data-origin-height=&quot;127&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oLhEL/btrvy3yKHWD/NIVx1Xd1ZE65EL2guzeeDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oLhEL/btrvy3yKHWD/NIVx1Xd1ZE65EL2guzeeDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oLhEL/btrvy3yKHWD/NIVx1Xd1ZE65EL2guzeeDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoLhEL%2Fbtrvy3yKHWD%2FNIVx1Xd1ZE65EL2guzeeDK%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;1106&quot; height=&quot;127&quot; data-origin-width=&quot;1106&quot; data-origin-height=&quot;127&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;마찬가지로 응답 패킷을 살펴보면, 최초 응답 헤더에 비해 이후 응답 메시지의 Header 크기가 줄어든 것을 확인할 수 있습니다.&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;위와 같이 gRPC는 기반에 HTTP 2.0을 기반으로 하여 메시지 전송간 데이터 Payload가 줄어드는 장점이 존재하기 때문에 이전 REST 방식에 통신에 있어서 조금 더 빠른 결과를 나타낼 수 있습니다.&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Streaming&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 Streaming 처리 방법에 대해서 살펴보도록 하겠습니다. Stream은 데이터를 한번만 전송하는 것이 아니라 연속적인 흐름으로 전달하는 것을 의미합니다.&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;gRPC에서는 총 3가지 종류의 Streaming이 존재합니다.&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;h4 data-ke-size=&quot;size20&quot;&gt;1) Client Stream&lt;/h4&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;1056&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTJN90/btrvwi4vrIw/ZR6ebhUJRMP6X6fEVjB7kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTJN90/btrvwi4vrIw/ZR6ebhUJRMP6X6fEVjB7kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTJN90/btrvwi4vrIw/ZR6ebhUJRMP6X6fEVjB7kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTJN90%2Fbtrvwi4vrIw%2FZR6ebhUJRMP6X6fEVjB7kK%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;640&quot; height=&quot;161&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;266&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;Client는 Stream 형태로 전달하고 Client의 요청이 끝나면 Server에서 한번에 응답을 내려주는 경우는 Client Stream이라고 부릅니다.&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;2) Server Stream&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9yzni/btrvvDnzFtz/bdxlGsx3871pbKz6nXCp0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9yzni/btrvvDnzFtz/bdxlGsx3871pbKz6nXCp0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9yzni/btrvvDnzFtz/bdxlGsx3871pbKz6nXCp0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9yzni%2FbtrvvDnzFtz%2FbdxlGsx3871pbKz6nXCp0k%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;640&quot; height=&quot;161&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;266&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;Client의 요청은 한번만 전달하고 Server에서 응답은 여러 번에 걸쳐 전송하는 경우는 Server Stream이라고 부릅니다.&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;3) Bidirectional Stream&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;266&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLF5rw/btrvy2miUWS/mL82Aonj0grPTZCft4dTH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLF5rw/btrvy2miUWS/mL82Aonj0grPTZCft4dTH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLF5rw/btrvy2miUWS/mL82Aonj0grPTZCft4dTH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLF5rw%2Fbtrvy2miUWS%2FmL82Aonj0grPTZCft4dTH0%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;640&quot; height=&quot;161&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;266&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;양방향 모두 Stream으로 데이터를 전송하는 경우는 Bidirectional Stream 이라고 부릅니다.&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;Stream 처리 방법은 개념적으로 어렵지 않고 이번 포스팅에서는 사용 방법 보다는 성능 비교가 주 목적이므로 모든 Stream 방식에 대한 구현을 다루지는 않겠습니다.&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;Stream 처리 관련해서 다루어볼 내용은 Client Stream 방식을 활용해서 Unary, REST 방식의 테스트 시나리오를 동일하게 적용하여 어떤 차이점이 있는지를 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646728356190&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...(중략)...
service PersonService {
    ...(중략)...
    rpc registerBatch(stream Person) returns (google.protobuf.Empty);
}&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 처리를 위해 서비스에 RPC를 등록합니다. 이후 Build를 수행합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646728426498&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class PersonGrpcService : PersonServiceGrpcKt.PersonServiceCoroutineImplBase() {
    ...(중략)...
    override suspend fun registerBatch(requests: Flow&amp;lt;Person&amp;gt;): Empty {
        val start = System.currentTimeMillis()
        requests
            .catch {
                //TODO : Error 처리
            }
            .onCompletion {
                println(&quot;${System.currentTimeMillis() - start} ms elapsed. &quot;)
            }
            .collect {
                //TODO : request 처리
            }
        return Empty.getDefaultInstance()
    }

    
}&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;Build 이후 해당 Stub 메소드 구현을 위해서 PersonServiceCoroutineImplBase Stub 클래스에서 RPC 관련 메소드를 override 합니다. 이때 Stream으로 전달받은 데이터를 기반으로 비즈니스 로직 처리는 수행하지 않기 때문에 collect 부분은 아무런 작업을 수행하지 않도록 구성했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646728450420&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    val channel = ManagedChannelBuilder.forAddress(&quot;localhost&quot;, 6565)
        .usePlaintext()
        .build()

    val stub = PersonServiceGrpcKt.PersonServiceCoroutineStub(channel)

    runBlocking { execute(stub, 50) } // warm up phase

    val base = 10.0
    val dec = DecimalFormat(&quot;#,###&quot;)

    runBlocking {
        for (exponent in 1..5) {
            val iterCount = base.pow(exponent).toInt()
            val time = measureTimeMillis {
                execute(stub, iterCount)
                println(&quot;count : ${dec.format(iterCount)}&quot;)
            }
            println(&quot;elapsed time $time ms&quot;)
            println(&quot;------------------------------------&quot;)
        }
    }
}

suspend fun execute(stub: PersonServiceGrpcKt.PersonServiceCoroutineStub, count: Int) {
    try {
        stub.registerBatch(
            IntRange(1, count)
                .map {
                    person {
                        name = &quot;kevin&quot;
                        age = (1..50).random()
                        address = address {
                            city = &quot;seoul&quot;
                            zipCode = &quot;123456&quot;
                        }
                        hobbies.addAll(listOf(&quot;foot ball&quot;, &quot;basket ball&quot;))
                    }
                }
                .asFlow()
        )
    } catch (e: StatusException) {
        println(e)
    }
}&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;Client 프로그램은 위와 같이 구성했습니다. gRPC의 Stream 처리를 구현하기 위해서 StreamObserver를 활용해서 구현하는 방식과 Kotlin의 Coroutine 방식 두 가지 방식으로 구현 가능한데, 위 코드는 Coroutine 방식으로 구현하였습니다.&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;내용을 살펴보면 이전 Unary 코드와 크게 다르지는 않으며, 데이터 전달시 Flow로 변환하여 전달하는 것을 확인할 수 있습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;293&quot; data-origin-height=&quot;309&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bl4Tct/btrvt2mJJpv/GtmYuC6VFLVyFie9kVt1c1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bl4Tct/btrvt2mJJpv/GtmYuC6VFLVyFie9kVt1c1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bl4Tct/btrvt2mJJpv/GtmYuC6VFLVyFie9kVt1c1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbl4Tct%2Fbtrvt2mJJpv%2FGtmYuC6VFLVyFie9kVt1c1%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;293&quot; height=&quot;309&quot; data-origin-width=&quot;293&quot; data-origin-height=&quot;309&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;실행 결과를 살펴보면, REST와 gRPC(Unary)와 비교했을 때 엄청난 개선이 이루어진 것을 확인할 수 있습니다.&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;횟수&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;REST&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;gRPC(Unary)&lt;/td&gt;
&lt;td style=&quot;width: 25%; text-align: center;&quot;&gt;gRPC(Client Stream)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;10&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;23 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;14 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;100&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;165 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;101 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;20 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;1,000&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;1,000 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;694 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;106 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;10,000&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;4,109 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;2,132 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;468 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;100,000&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;41,491 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;13,768 ms&lt;/td&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;2,880 ms&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;요청 횟수가 적을 때보다 횟수가 늘어감에 따라 차이가 더 커지는 것을 확인할 수 있습니다. 가령 10만번 데이터 전송의 경우 REST 방식보다 14.4배 Unary 방식에 비교하면 4.78배 효율이 좋은 것을 확인할 수 있습니다.&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;그렇다면 Stream 처리 방식은 왜 이리 많은 차이를 보이는 것일까요? 이전과 마찬가지로 패킷의 흐름을 살펴보겠습니다.&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;1181&quot; data-origin-height=&quot;705&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcezM4/btrvwii8ZTm/aK1kiU2Skod1LIfeDHBOO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcezM4/btrvwii8ZTm/aK1kiU2Skod1LIfeDHBOO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcezM4/btrvwii8ZTm/aK1kiU2Skod1LIfeDHBOO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcezM4%2Fbtrvwii8ZTm%2FaK1kiU2Skod1LIfeDHBOO0%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;1181&quot; height=&quot;705&quot; data-origin-width=&quot;1181&quot; data-origin-height=&quot;705&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;위 내용은 Stream 형식으로 Person 데이터를 50회 전송했을 때 네트워크 흐름입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unary와 REST 방식은 5회만 전송했는데도 많은 Network 요청이 있었던 것과 비교하여 50회 데이터를 전송했는데도 패킷의 횟수가 그리 많지 않습니다.&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;671&quot; data-origin-height=&quot;307&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BDALP/btrvykU5Vop/09j5FbpkGQMcPnfduh6kCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BDALP/btrvykU5Vop/09j5FbpkGQMcPnfduh6kCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BDALP/btrvykU5Vop/09j5FbpkGQMcPnfduh6kCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBDALP%2FbtrvykU5Vop%2F09j5FbpkGQMcPnfduh6kCk%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;307&quot; data-origin-width=&quot;671&quot; data-origin-height=&quot;307&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;데이터 전송 부분만 살펴보면, 요청을 전달할 때 Header는 한번만 전송한 것을 확인할 수 있고, 응답 또한 한번만 전달받은 것을 확인할 수 있습니다.&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;755&quot; data-origin-height=&quot;475&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgepEx/btrvy3lfDuQ/aKB1Qa4IISPyZ4koBwpkk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgepEx/btrvy3lfDuQ/aKB1Qa4IISPyZ4koBwpkk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgepEx/btrvy3lfDuQ/aKB1Qa4IISPyZ4koBwpkk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgepEx%2Fbtrvy3lfDuQ%2FaKB1Qa4IISPyZ4koBwpkk0%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;755&quot; height=&quot;475&quot; data-origin-width=&quot;755&quot; data-origin-height=&quot;475&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;그리고 데이터는 여러번 전달한 것이 아니라 한 Packet안에 여러개의 요청이 포함되어 전달된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 패킷 흐름에는 총 2번 전달하는 과정에서 50개의 요청이 담겨있는 것을 확인할 수 있습니다.&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;한번에 동일 요청 다수를 함께 전달할 경우, Stream 방식이 매번 요청을 수행하는 Unary 방법보다 효율적인 데이터 전송이 가능합니다. 따라서 네트워크 전달 과정에서 많은 비용을 감소하여 성능이 더욱 좋다고 볼 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난 포스팅과 이번 포스팅을 통해서 gRPC의 성능 이점에 대해서 다양한 각도로 살펴봤습니다. 다음 포스팅부터는 gRPC를 사용하는 방법에 대해서 차차 알아보도록 하겠습니다.&lt;/p&gt;</description>
      <category>MSA/gRPC</category>
      <category>GRPC</category>
      <category>gRPC kotlin</category>
      <category>gRPC 구조</category>
      <category>gRPC 네트워크 방식</category>
      <category>gRPC 성능</category>
      <category>gRPC 패킷</category>
      <category>HTTP 2.0</category>
      <category>protobuf</category>
      <category>REST vs gRPC</category>
      <category>spring grpc</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/177</guid>
      <comments>https://cla9.tistory.com/177#entry177comment</comments>
      <pubDate>Thu, 10 Mar 2022 09:16:54 +0900</pubDate>
    </item>
    <item>
      <title>2. gRPC는 왜 빠를까? (Payload) - 1</title>
      <link>https://cla9.tistory.com/176</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cla9.tistory.com/175&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;이전 포스팅&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;에서는 gRPC에 대한 기본적인 소개를 다루어 봤습니다. 이번에는 gRPC에서 사용하는 Protocol Buffer(aka&amp;nbsp; Protobuf)와 보편적으로 사용하는 JSON 메시지 포맷에 대한 비교를 통해 어떤 부분에서 Protobuf가 이점이 있는지를 살펴보겠습니다.&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;style1&quot; /&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;1. JSON, Protobuf 변환 속도 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://cla9.tistory.com/175&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;이전 포스팅&lt;/a&gt;&lt;/u&gt;&lt;/span&gt;에서 살펴봤듯이 REST 통신에서는 JSON 규격으로 메시지를 주고 받았고 이때 발생하는 Serialization &amp;amp; Deserialization 과정은 비용이 소모되는 작업임을 살펴봤습니다. 반면 gRPC에서는 binary 포맷으로 데이터를 주고받기 때문에 변환 과정에 따른 비용이 JSON에 비해서 적다고 설명했습니다.&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;그렇다면 실제 Protobuf 변환 과정과 JSON 변환 과정을 측정해보면 얼마나 유의미한 결과를 나타낼까요? 테스트를 통해 차이가 얼마나 발생하는지 살펴봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646544549585&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;data class PersonDto(
    val name : String,
    val age : Int,
    val hobbies : List&amp;lt;String&amp;gt;? = null,
    val address : AddressDto? = null
)

data class AddressDto(
    val city : String,
    val zipCode : String
)&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;JSON 변환 테스트를 위해 Sample 객체를 위와 같이 디자인합니다. 위 데이터 구조는 Person이라는 객체를 생성함에 있어 이름, 나이, 주소 정보를 입력받으며 취미의 경우 다수가 존재하므로 List로 입력받도록 디자인 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646544414010&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;syntax = &quot;proto3&quot;;

option java_multiple_files = true;
option java_package = &quot;grpc.polar.penguin&quot;;

message Address{
    string city = 1;
    string zip_code = 2;
}

message Person{
    string name = 1;
    int32 age = 2;
    repeated string hobbies = 3;
    optional Address address = 4;
}&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;앞서 구현한 data class에 대응되는 Proto 파일은 위와 같이 구현합니다. 아직 Protobuf에 대해서 본격적으로 다루어보지 않은만큼 syntax가 이해되지 않더라도 좋습니다.&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;1333&quot; data-origin-height=&quot;491&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DCM7D/btrvgk9GtIF/ne8NnclQrNOxZaVuMkmi91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DCM7D/btrvgk9GtIF/ne8NnclQrNOxZaVuMkmi91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DCM7D/btrvgk9GtIF/ne8NnclQrNOxZaVuMkmi91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDCM7D%2Fbtrvgk9GtIF%2Fne8NnclQrNOxZaVuMkmi91%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;1333&quot; height=&quot;491&quot; data-origin-width=&quot;1333&quot; data-origin-height=&quot;491&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;기본 Spec을 정의하였으면 이제 변환 과정 테스트 시나리오를 정의해봅시다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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: 100%;&quot;&gt;1. 10, 100 ... 천만번까지 10의 거듭제곱 횟수만큼 변환 과정을 수행하면서 각 단계에서 걸린 총 시간을 측정한다.&lt;br /&gt;&lt;br /&gt;2. 단계별 warm up 과정을 추가하고 해당 단계에서의 결과는 제외한다. 따라서 단계별 50회 변환 과정을 추가한다.&lt;br /&gt;&lt;br /&gt;3. JSON, Proto 변환 측정 과정은 다음과 같다.&lt;br /&gt;&amp;nbsp; &amp;nbsp;- JSON : DTO를 JSON Byte 배열로 변환한 다음 해당 Byte을 다시 DTO로 변환하는데 걸린 시간&lt;br /&gt;&amp;nbsp; &amp;nbsp;- Proto : Stub을 Byte 배열로 변환한 다음 해당 Byte 배열을 다시 Stub 객체로 변환하는데 걸린 시간&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;355&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbRsmX/btrveMk3cND/f39Vf8bLx0lI7JfhgOPc4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbRsmX/btrveMk3cND/f39Vf8bLx0lI7JfhgOPc4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbRsmX/btrveMk3cND/f39Vf8bLx0lI7JfhgOPc4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbRsmX%2FbtrveMk3cND%2Ff39Vf8bLx0lI7JfhgOPc4K%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;355&quot; data-origin-width=&quot;664&quot; data-origin-height=&quot;355&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;테스트 시나리오를 위해 작성한 메인 프로그램의 흐름은 위와 같습니다. 10 부터 천만번까지 각각 변환과정을 수행한 결과를 출력하도록 구성했습니다.&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;501&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qaHCr/btrvnfeYjw4/xtMJWMEq9ClZtRykLKOJ5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qaHCr/btrvnfeYjw4/xtMJWMEq9ClZtRykLKOJ5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qaHCr/btrvnfeYjw4/xtMJWMEq9ClZtRykLKOJ5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqaHCr%2FbtrvnfeYjw4%2FxtMJWMEq9ClZtRykLKOJ5k%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;501&quot; height=&quot;148&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;148&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;측정 과정은 앞서 시나리오대로 단계별 변환 횟수에 맞추어 변환 작업을 수행하며, 단계별 최초 50회는 warm up 단계로 구성하여 결과에서 제외한 총 수행시간을 반환하도록 작성했습니다.&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;640&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sU1tA/btrvl7BuBr2/H3pT2Fqx7lHE0WO7hUlqVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sU1tA/btrvl7BuBr2/H3pT2Fqx7lHE0WO7hUlqVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sU1tA/btrvl7BuBr2/H3pT2Fqx7lHE0WO7hUlqVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsU1tA%2Fbtrvl7BuBr2%2FH3pT2Fqx7lHE0WO7hUlqVk%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;640&quot; height=&quot;417&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;417&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;Stub 객체를 Byte 배열로 변환하고 이를 다시 Stub 객체로 변환하는 코드는 위와 같습니다.&amp;nbsp;&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;831&quot; data-origin-height=&quot;424&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI2xiX/btru8IJPy5H/lhd5V9IeXEjnNM2CGXU1WK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI2xiX/btru8IJPy5H/lhd5V9IeXEjnNM2CGXU1WK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI2xiX/btru8IJPy5H/lhd5V9IeXEjnNM2CGXU1WK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI2xiX%2Fbtru8IJPy5H%2Flhd5V9IeXEjnNM2CGXU1WK%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;424&quot; data-origin-width=&quot;831&quot; data-origin-height=&quot;424&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;DTO 객체를 JSON Byte 배열로 저장한 다음 이를 다시 Person DTO 객체로 변환하는 코드는 위와 같습니다. 이 과정에서 Parser로는 Jackson을 사용했습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;753&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m0tG0/btrviIIZsbI/EYMhzPzhHPM7d9kNnnbvbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m0tG0/btrviIIZsbI/EYMhzPzhHPM7d9kNnnbvbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m0tG0/btrviIIZsbI/EYMhzPzhHPM7d9kNnnbvbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm0tG0%2FbtrviIIZsbI%2FEYMhzPzhHPM7d9kNnnbvbK%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;403&quot; height=&quot;753&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;753&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;측정 결과는 위와 같습니다. 살펴보면 변환 횟수가 증가하면서 두 방식의 변환 시간의 차가 크게 벌어지는 것을 확인할 수 있습니다. 가령 천만번 변환의 경우 7배 빠른 것으로 확인되었습니다.&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; gRPC가 REST 방식에 비해 7배 빠르다고 말할 수 있을까요?&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;759&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUXRax/btrvatSA7l7/lzK80QJdGkqCpQAD7US0V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUXRax/btrvatSA7l7/lzK80QJdGkqCpQAD7US0V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUXRax/btrvatSA7l7/lzK80QJdGkqCpQAD7US0V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUXRax%2FbtrvatSA7l7%2FlzK80QJdGkqCpQAD7US0V0%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;480&quot; height=&quot;35&quot; data-origin-width=&quot;759&quot; data-origin-height=&quot;56&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;요청에 대해서 응답을 처리하는 전체 flow를 아주 간략하게 표현한다면, 위와 같이 표현할 수 있을 것입니다. 위 과정에서 오래걸리는 영역은 당연히 Business Logic 처리를 위한 수행시간일 것입니다. 따라서 Business Logic 수행 시간이 오래 걸릴 수록 격차는 현격히 줄어들 것입니다.&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;하지만 TPS가 높은 시스템에서는 1ms라도 응답 속도를 줄이는 것이 중요하기 때문에 이런 경우 매우 유의미한 결과라고 볼 수 있습니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. JSON, Protobuf 크기 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 기존에 사용했던 DTO, Stub 인스턴스를 byte 배열로 변환하였을 때 크기에 대해서 비교해보고 차이점을 통해 Protobuf의 특징을 확인해보겠습니다.&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;685&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z7yeQ/btru8H4U6xE/7XjSDWuVzPZ89LZRSIICzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z7yeQ/btru8H4U6xE/7XjSDWuVzPZ89LZRSIICzk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z7yeQ/btru8H4U6xE/7XjSDWuVzPZ89LZRSIICzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz7yeQ%2Fbtru8H4U6xE%2F7XjSDWuVzPZ89LZRSIICzk%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;685&quot; height=&quot;460&quot; data-origin-width=&quot;685&quot; data-origin-height=&quot;460&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;사이즈 크기 비교를 위해 작성한 프로그램은 위와 같습니다. 이전 내용과 같이 PersonDTO와 Stub 객체를 생성 후 둘 다 byte 배열로 변환한 크기를 출력하도록 구성했습니다.&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;207&quot; data-origin-height=&quot;45&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCMzGf/btrvhmrCirp/k64jUVCHaKZKmBVo4X4x61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCMzGf/btrvhmrCirp/k64jUVCHaKZKmBVo4X4x61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCMzGf/btrvhmrCirp/k64jUVCHaKZKmBVo4X4x61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCMzGf%2FbtrvhmrCirp%2Fk64jUVCHaKZKmBVo4X4x61%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;207&quot; height=&quot;45&quot; data-origin-width=&quot;207&quot; data-origin-height=&quot;45&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;실행 결과를 보면, 동일한 데이터 입력에 있어 JSON 방식과 Proto 방식간의 결과물 크기가 상당히 차이나는 것을 확인할 수 있습니다.&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;이러한 차이가 발생하는 이유는 Proto 메시지 정의에 따라서 Binary 데이터를 만드는 encoding 과정에서 데이터가 압축되기 때문입니다. 이와 관련하여 자세한 기술적인 내용은 아래 네이버 기술 블로그와 구글 Protocol Encoding 공식문서를 살펴보시면 도움 되실 것 같습니다.&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;&lt;a title=&quot;네이버 기술 블로그 gRPC 깊게 파고들기&quot; href=&quot;https://medium.com/naver-cloud-platform/nbp-%EA%B8%B0%EC%88%A0-%EA%B2%BD%ED%97%98-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%9D%90%EB%A6%84-grpc-%EA%B9%8A%EA%B2%8C-%ED%8C%8C%EA%B3%A0%EB%93%A4%EA%B8%B0-2-b01d390a7190&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;네이버 기술 블로그 grpc 깊게 파고들기&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1646551543443&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[NBP 기술&amp;amp;경험] 시대의 흐름, gRPC 깊게 파고들기 #2&quot; data-og-description=&quot;google에서 개발한 오픈소스 RPC(Remote Procedure Call) 프레임워크, gRPC를 알아봅니다.&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/naver-cloud-platform/nbp-%EA%B8%B0%EC%88%A0-%EA%B2%BD%ED%97%98-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%9D%90%EB%A6%84-grpc-%EA%B9%8A%EA%B2%8C-%ED%8C%8C%EA%B3%A0%EB%93%A4%EA%B8%B0-2-b01d390a7190&quot; data-og-url=&quot;https://medium.com/naver-cloud-platform/nbp-%EA%B8%B0%EC%88%A0-%EA%B2%BD%ED%97%98-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%9D%90%EB%A6%84-grpc-%EA%B9%8A%EA%B2%8C-%ED%8C%8C%EA%B3%A0%EB%93%A4%EA%B8%B0-2-b01d390a7190&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bKuWWN/hyNC4zpXvn/6BmmdDoMqw8IWlYs61xpGK/img.png?width=966&amp;amp;height=483&amp;amp;face=0_0_966_483,https://scrap.kakaocdn.net/dn/n8Mzl/hyNC22FQ0h/4a8mZQNz25lKTBKS3RzSp1/img.png?width=719&amp;amp;height=833&amp;amp;face=0_0_719_833,https://scrap.kakaocdn.net/dn/brKFol/hyNC89GuXM/JoDlwSplid7Iz9JszUrcok/img.png?width=966&amp;amp;height=483&amp;amp;face=0_0_966_483&quot;&gt;&lt;a href=&quot;https://medium.com/naver-cloud-platform/nbp-%EA%B8%B0%EC%88%A0-%EA%B2%BD%ED%97%98-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%9D%90%EB%A6%84-grpc-%EA%B9%8A%EA%B2%8C-%ED%8C%8C%EA%B3%A0%EB%93%A4%EA%B8%B0-2-b01d390a7190&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/naver-cloud-platform/nbp-%EA%B8%B0%EC%88%A0-%EA%B2%BD%ED%97%98-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%9D%90%EB%A6%84-grpc-%EA%B9%8A%EA%B2%8C-%ED%8C%8C%EA%B3%A0%EB%93%A4%EA%B8%B0-2-b01d390a7190&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bKuWWN/hyNC4zpXvn/6BmmdDoMqw8IWlYs61xpGK/img.png?width=966&amp;amp;height=483&amp;amp;face=0_0_966_483,https://scrap.kakaocdn.net/dn/n8Mzl/hyNC22FQ0h/4a8mZQNz25lKTBKS3RzSp1/img.png?width=719&amp;amp;height=833&amp;amp;face=0_0_719_833,https://scrap.kakaocdn.net/dn/brKFol/hyNC89GuXM/JoDlwSplid7Iz9JszUrcok/img.png?width=966&amp;amp;height=483&amp;amp;face=0_0_966_483');&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;[NBP 기술&amp;amp;경험] 시대의 흐름, gRPC 깊게 파고들기 #2&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;google에서 개발한 오픈소스 RPC(Remote Procedure Call) 프레임워크, gRPC를 알아봅니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developers.google.com/protocol-buffers/docs/encoding&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;구글 Protocol Buffer Encoding 공식 문서&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1646551542823&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;Encoding &amp;nbsp;|&amp;nbsp; Protocol Buffers &amp;nbsp;|&amp;nbsp; Google Developers&quot; data-og-description=&quot;Encoding This document describes the binary wire format for protocol buffer messages. You don't need to understand this to use protocol buffers in your applications, but it can be very useful to know how different protocol buffer formats affect the size of&quot; data-og-host=&quot;developers.google.com&quot; data-og-source-url=&quot;https://developers.google.com/protocol-buffers/docs/encoding&quot; data-og-url=&quot;https://developers.google.com/protocol-buffers/docs/encoding&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/DpdAe/hyNC8IAVgm/SzfXR3T0Kx4wtPLbkVEWp1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://developers.google.com/protocol-buffers/docs/encoding&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://developers.google.com/protocol-buffers/docs/encoding&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/DpdAe/hyNC8IAVgm/SzfXR3T0Kx4wtPLbkVEWp1/img.png?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;Encoding &amp;nbsp;|&amp;nbsp; Protocol Buffers &amp;nbsp;|&amp;nbsp; Google Developers&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Encoding This document describes the binary wire format for protocol buffer messages. You don't need to understand this to use protocol buffers in your applications, but it can be very useful to know how different protocol buffer formats affect the size of&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;developers.google.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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;이번에는 address와 hobbies를 제거한 다음의 수행 결과를 비교해보도록 하겠습니다.&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;678&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bX9Jp0/btrvaujGuaI/T1HjgfyqAjg532gwWDZbT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bX9Jp0/btrvaujGuaI/T1HjgfyqAjg532gwWDZbT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bX9Jp0/btrvaujGuaI/T1HjgfyqAjg532gwWDZbT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbX9Jp0%2FbtrvaujGuaI%2FT1HjgfyqAjg532gwWDZbT1%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;678&quot; height=&quot;306&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;306&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;205&quot; data-origin-height=&quot;48&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqSqWq/btrvcGSoUA4/IM7sixadOhQOnoACULCLT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqSqWq/btrvcGSoUA4/IM7sixadOhQOnoACULCLT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqSqWq/btrvcGSoUA4/IM7sixadOhQOnoACULCLT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqSqWq%2FbtrvcGSoUA4%2FIM7sixadOhQOnoACULCLT0%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;205&quot; height=&quot;48&quot; data-origin-width=&quot;205&quot; data-origin-height=&quot;48&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;결과를 측정해보면 값이 모두 들어있을 때보다 일부 필드에 값이 입력되지 않았을 경우 Stub 객체의 Byte 배열 크기와 JSON의 결과값이 더욱 차이가 나며, 이는 전체 값을 입력했을 때 보다 압축률이 더 좋음을 의미합니다.&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;pre id=&quot;code_1646554268247&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{&quot;name&quot;:&quot;polar penguin&quot;,&quot;age&quot;:20,&quot;hobbies&quot;:null,&quot;address&quot;:null}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 결과는 DTO를 JSON으로 변환한 결과입니다. 길이를 살펴보면 63바이트인 것을 확인할 수 있습니다.&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;결과를 통해 살펴본 흥미로운 사실은 hobbies와 address는 실질적으로 아무런 값을 입력하지 않았음에도 불구하고 JSON에서는 Key와 value를 포함시킨다는 사실입니다. 이로인해 불필요한 overhead가 추가됩니다.&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;반면 Protobuf의 경우는 무엇이 다를까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646554545016&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;message Person{
    string name = 1;
    int32 age = 2;
    repeated string hobbies = 3;
    optional Address address = 4;
}&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;이전에 살펴본 Person의 proto 정의는 위와 같습니다. 그리고 테스트 프로그램에서 수행한 실제 Stub 객체에는 hobbies와 address가 포함되지 않았음을 확인할 수 있습니다.&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;proto 파일에서 눈여겨 볼 점은 실제 Property 옆에 표시된 field 번호가 존재하는 점입니다. 가령 name에는 1이 age에는 2가 지정되어있습니다.&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;해당 번호는 Protobuf의 필드를 인식하게 만들어주는 Key를 구성하는 요소입니다. 참고로 이전에 첨부한 &lt;a href=&quot;https://medium.com/naver-cloud-platform/nbp-%EA%B8%B0%EC%88%A0-%EA%B2%BD%ED%97%98-%EC%8B%9C%EB%8C%80%EC%9D%98-%ED%9D%90%EB%A6%84-grpc-%EA%B9%8A%EA%B2%8C-%ED%8C%8C%EA%B3%A0%EB%93%A4%EA%B8%B0-2-b01d390a7190&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Naver 기술 블로그&lt;/span&gt;&lt;/u&gt;&lt;/a&gt;나 &lt;a href=&quot;https://developers.google.com/protocol-buffers/docs/encoding&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;Google 공식 문서&lt;/u&gt;&lt;/span&gt;&lt;/a&gt;에서는 해당 Field 번호와 Wiretype가 조합된 Key를 이용하여 Encoding 및 Decoding을 수행하여 필드 값을 Parsing 함을 자세히 확인할 수 있습니다.&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;그렇다면 hobbies와 address가 입력되지 않았을 때 개념적으로 어떤 변화가 발생했을까요? 먼저 개념적으로 이해하기 위해 추상적으로 어떻게 표현되었는지 살펴봅시다.&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;1057&quot; data-origin-height=&quot;170&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uLIde/btrvhNqhYet/N0HYehB7WqZngePYMKGAsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uLIde/btrvhNqhYet/N0HYehB7WqZngePYMKGAsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uLIde/btrvhNqhYet/N0HYehB7WqZngePYMKGAsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuLIde%2FbtrvhNqhYet%2FN0HYehB7WqZngePYMKGAsK%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;1057&quot; height=&quot;170&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;170&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;protobuf에서는 field 번호가 해당 객체 내에서 필드 값을 식별하는데 있어 주요 역할을 수행합니다. 따라서 protobuf를 설계할 때 field 별로 부여하는 field 번호는 unique 해야합니다.&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;결과물을 살펴보면, JSON 표현 방식에 비해서 2가지 특징을 지닌 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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: 100%;&quot;&gt;&lt;span&gt;1. 해당 객체 값에 값이 입력되지 않았을 경우 결과물에 포함시키지 않습니다. 따라서 JSON에 비해서 Byte 배열 크기가 줄어들 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span&gt;2. 실제 필드명의 길이가 어떻든 관계없이 field 번호를 기반으로 Binary 데이터가 만들어지기 때문에 payload 크기가 감소됩니다. 이는 field 명이 길어질 수록 payload 크기가 커지는 JSON과 대비하여 공간을 절약할 수 있습니다.&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;&amp;nbsp;&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;409&quot; data-origin-height=&quot;259&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmIolR/btrvuPV5RED/kYOfs4MSkehaXgkLkK8hW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmIolR/btrvuPV5RED/kYOfs4MSkehaXgkLkK8hW0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmIolR/btrvuPV5RED/kYOfs4MSkehaXgkLkK8hW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmIolR%2FbtrvuPV5RED%2FkYOfs4MSkehaXgkLkK8hW0%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;409&quot; height=&quot;259&quot; data-origin-width=&quot;409&quot; data-origin-height=&quot;259&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 패킷 수준에서 메시지 내용을 자세하게 살펴보겠습니다. 내용을 보면 방금전 설명했던 설명과 유사함을 확인할 수 있습니다.&lt;/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;데이터 구조를 살펴보면 Field Number와 Wire Type을 기반으로 ( (Field Number &amp;lt;&amp;lt; 3) | Wire Type ) 형태로 Hex 값으로 구성되어 있습니다. 또한 모든 Field 내용이 저장되어있지 않고 사용자가 기입한 내용만 저장되어있는 것을 확인할 수 있습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;91&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/p4vEI/btru3NESROJ/c5uUqjLSR0Bf4kZ5CAAZc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/p4vEI/btru3NESROJ/c5uUqjLSR0Bf4kZ5CAAZc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/p4vEI/btru3NESROJ/c5uUqjLSR0Bf4kZ5CAAZc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fp4vEI%2Fbtru3NESROJ%2Fc5uUqjLSR0Bf4kZ5CAAZc0%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;702&quot; height=&quot;91&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;91&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;더 자세히 확인하기 위해 실제 Stub 객체에서 생성되는 Binary 내용을 해석해보도록 하겠습니다.&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;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: 100%;&quot;&gt;&lt;span&gt;&lt;b&gt;0A&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: name의 field 번호 1, wire type 2이므로 ( (1 &amp;lt;&amp;lt; 3) | 2 ) 수행하면 10입니다. 따라서 이는 Hex 값으로 0A입니다.&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;0D&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: value의 길이를 의미합니다. 여기서 name에 저장된 값은 polar penguin 총 13자이므로 이는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Hex&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;값으로 0D입니다.&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;70 6F 6C 61 72 20 70 65 6E 67 75 69 6E&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: &quot;polar penguin&quot; 문자열의 Hex 값입니다.&amp;nbsp;&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;10&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: age의 field 번호 2, wire type 0이므로 ( (2 &amp;lt;&amp;lt; 3) | 0 ) 수행하면 16입니다. 따라서 이는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;Hex&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;값으로 10입니다.&lt;/span&gt;&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;&lt;b&gt;14&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;: age의 값인 20입니다. 이는&amp;nbsp;&lt;span&gt;Hex&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;값으로 14입니다.&lt;/span&gt;&lt;br /&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Proto에 저장되는 결과를 알아보기 위해 실제 저장된 Binary 구조까지 살펴봤습니다. 모든 기술이 장점이 있으면 단점이 존재하듯이 Protobuf는 결과물이 Binary 포맷이기 때문에 결과 값을 유추하기 쉽지 않은 점은 단점이라고 볼 수 있습니다. 하지만 성능이 더 중요시되는 환경에서는 짧은 Payload는 전송 속도에 있어 강점입니다.&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 Protobuf와 JSON을 비교하여 변환 속도와 Payload 크기 차이점을 비교해봤습니다. Protobuf는 gRPC의 핵심 요소로써 gRPC가 가지는 성능 이점의 주요 부분 중 하나라고 생각합니다. 다음 포스팅에서는 HTTP 2.0 기반으로 gRPC의 통신 방법에 대해서 살펴보겠습니다.&lt;/p&gt;</description>
      <category>MSA/gRPC</category>
      <category>GRPC</category>
      <category>gRPC protobuf</category>
      <category>gRPC 구조</category>
      <category>gRPC 성능</category>
      <category>JSON</category>
      <category>kotlin grpc</category>
      <category>protobuf</category>
      <category>REST</category>
      <category>Spring boot</category>
      <category>spring grpc</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/176</guid>
      <comments>https://cla9.tistory.com/176#entry176comment</comments>
      <pubDate>Thu, 10 Mar 2022 09:15:37 +0900</pubDate>
    </item>
    <item>
      <title>1. gRPC 개요</title>
      <link>https://cla9.tistory.com/175</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 서론&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 MSA가 각광받으면서 많은 회사에서 Monolithic 구조를 여러개의 마이크로 서비스로 분리하려고 시도하고 있습니다.&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;MSA 구성은 다양한 장점을 내포하고 있으나 그만큼 다양한 문제점 또한 상존합니다. 이 글에서는 MSA의 문제점 중 하나인 네트워크 통신 overhead에 초점을 맞추어 gRPC 기술이 어떤 부분을 해소해줄 수 있는지에 대해서 다루어보고 해당 기술은 어떻게 사용할 수 있는지에 대해서 설명해보고자 합니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 마이크로 서비스간 통신 이슈&lt;/h3&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;494&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bylhvg/btrvbiKSolq/oQwYqBuVBODSTBtzkjDfg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bylhvg/btrvbiKSolq/oQwYqBuVBODSTBtzkjDfg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bylhvg/btrvbiKSolq/oQwYqBuVBODSTBtzkjDfg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbylhvg%2FbtrvbiKSolq%2FoQwYqBuVBODSTBtzkjDfg0%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;320&quot; height=&quot;295&quot; data-origin-width=&quot;494&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Monolithic 구조에서는 하나의 프로그램으로 동작하기 때문에 그 안에서 구조적인 2개의 서비스간의 데이터는 공유 메모리를 통해서 주고받을 수 있습니다. 따라서 이 경우 서비스간 메시지 전송 성능은 큰 이슈가 되지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;151&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OZX6b/btrvbgFtJ9P/ylDjqIo9YJ70uWr8OTBao1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OZX6b/btrvbgFtJ9P/ylDjqIo9YJ70uWr8OTBao1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OZX6b/btrvbgFtJ9P/ylDjqIo9YJ70uWr8OTBao1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOZX6b%2FbtrvbgFtJ9P%2FylDjqIo9YJ70uWr8OTBao1%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;569&quot; height=&quot;151&quot; data-origin-width=&quot;569&quot; data-origin-height=&quot;151&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;&lt;span&gt;반면 MSA에서는 여러 모듈로 분리되어있고 동일 머신에 존재하지 않을 수 있습니다. 따라서 일반적으로는 보편화된 방식인 REST 통신을 통해 메시지를 주고 받습니다.&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;문제는 Frontend 요청에 대한 응답을 만들어내기 위해 여러 마이크로 서비스간의 협력이 필요하다면, 구간별 REST 통신에 따른 비효율로 인해 응답속도가 저하된다는 점입니다. 그렇다면 구체적으로 어떤 요인으로 인해 응답 속도 저하가 발생될까요? 이에 대해서 알아보기 전에 HTTP 1.1의 특징에 대해서 이해하고 HTTP 1.1의 또 다른 이슈를 확인해보도록 하겠습니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. HTTP 1.1 통신 방법&lt;/h3&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;573&quot; data-origin-height=&quot;923&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H0S5q/btrvaH5BeES/H7uGV3yFqvMTrK72fHpIAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H0S5q/btrvaH5BeES/H7uGV3yFqvMTrK72fHpIAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H0S5q/btrvaH5BeES/H7uGV3yFqvMTrK72fHpIAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH0S5q%2FbtrvaH5BeES%2FH7uGV3yFqvMTrK72fHpIAk%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;320&quot; height=&quot;515&quot; data-origin-width=&quot;573&quot; data-origin-height=&quot;923&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;HTTP는 TCP위에서 동작하므로 데이터 송수신에 앞서서 TCP 연결 시점에 3 way handshake 과정을 거치며, 연결을 종료할 때도 4 way handshake 방식으로 종료하게됩니다.&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;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;361&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SoMYP/btrvhOoDji9/1jodV4ULIqXHvKHCDqFFEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SoMYP/btrvhOoDji9/1jodV4ULIqXHvKHCDqFFEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SoMYP/btrvhOoDji9/1jodV4ULIqXHvKHCDqFFEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSoMYP%2FbtrvhOoDji9%2F1jodV4ULIqXHvKHCDqFFEk%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;180&quot; height=&quot;423&quot; data-origin-width=&quot;361&quot; data-origin-height=&quot;848&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;앞서 살펴본 HTTP 1.0은 요청/응답을 하기에 앞서 매번 Connection을 맺고 끊어야했기 때문에 연결 요청/해제 비용이 상당히 높았습니다.&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;따라서 이러한 성능 이슈를 해결하고자 HTTP 1.0 기반의 브라우저와 서버에서는 자체적으로 Keep-alive 기능을 지원하기도 했습니다. 이 경우 Header에 Keep alive 관련 헤더를 포함해서 Connection을 유지하는 경우도 있었습니다. 하지만 해당 기능은 공식 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;HTTP 1.1에서는 1.0의 문제점을 해결하고자 Persistent Connection과 Pipelining 기법을 제공하였습니다. 해당 기능이 무엇인지 알아봅시다.&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;361&quot; data-origin-height=&quot;669&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bszia/btrvkHJosih/3ihpAEkKk5RiTmwpxcvHq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bszia/btrvkHJosih/3ihpAEkKk5RiTmwpxcvHq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bszia/btrvkHJosih/3ihpAEkKk5RiTmwpxcvHq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBszia%2FbtrvkHJosih%2F3ihpAEkKk5RiTmwpxcvHq1%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;519&quot; data-origin-width=&quot;361&quot; data-origin-height=&quot;669&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;Persistent Connection의 경우 Keep Alive와 같이 요청/응답을 위해 매번 Connection을 맺는 것이 아니라 연결을 일정시간 지속하는 것을 의미합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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: 50%; text-align: center;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;361&quot; data-origin-height=&quot;669&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czUixc/btrviIohlDq/OY7x4dI5brmbXuczjbSKG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czUixc/btrviIohlDq/OY7x4dI5brmbXuczjbSKG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czUixc/btrviIohlDq/OY7x4dI5brmbXuczjbSKG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczUixc%2FbtrviIohlDq%2FOY7x4dI5brmbXuczjbSKG1%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;519&quot; data-origin-width=&quot;361&quot; data-origin-height=&quot;669&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;669&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9mgis/btrvbiSa5qw/7av0YKTFsECxYnvrbExiw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9mgis/btrvbiSa5qw/7av0YKTFsECxYnvrbExiw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9mgis/btrvbiSa5qw/7av0YKTFsECxYnvrbExiw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9mgis%2FbtrvbiSa5qw%2F7av0YKTFsECxYnvrbExiw0%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;520&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;669&quot;/&gt;&lt;/span&gt;&lt;/figure&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;다만 Persistent Connection만 적용했을 경우 왼쪽 그림과 같이 1개의 요청을 보내고 요청에 대한 응답이 와야 그 다음 요청을 보내기 위해 기다려야 합니다. 따라서 오른쪽과 같이 추가로 Pipelining을 적용하여 각 요청마다 응답을 기다리지 않고, 요청을 하나의 Packet에 담아 지속적으로 요청을 전달할 수 있도록 개선하였습니다.&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;Pipelining을 살펴보면 HTTP 1.0과 비교해서 많은 부분이 개선된 것으로 보입니다. 하지만 Pipelining에서도 성능 이슈는 존재합니다. 과연 무엇일까요?&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. HTTP 1.1 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. HOLB(Head Of Line Blocking)&lt;/h4&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;725&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1UIDQ/btrvcGysIEF/XffK4JxbE9beaYlrvqqUNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1UIDQ/btrvcGysIEF/XffK4JxbE9beaYlrvqqUNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1UIDQ/btrvcGysIEF/XffK4JxbE9beaYlrvqqUNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1UIDQ%2FbtrvcGysIEF%2FXffK4JxbE9beaYlrvqqUNk%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;51&quot; data-origin-width=&quot;725&quot; data-origin-height=&quot;97&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;Pipelining에서 요청 자체는 응답 여부와 관계없이 보낼 수 있습니다. 하지만 여전히 순차적으로 응답을 받아야합니다. 따라서 첫 번째 요청에 대한 응답이 오래걸리는 상황이라면, 두 번째 세번 째 요청 응답은 첫번째 요청이 응답처리가 완료되기 전까지 대기해야합니다. 이러한 문제를 Head Of Line Blocking(HOLB)라고 합니다.&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;만약 위 예시와 같이 B, C, D, E 자원의 경우 크기가 작아 빠르게 처리될 수 있다면, 사용자 응답성이 좋아질 수 있습니다. 하지만 HTTP 1.1의 경우에는 A 자원의 응답처리가 완료되지 않았기 때문에 결과적으로는 전체 응답의 대기가 발생합니다. 이는 곧 사용성이 나빠지는 원인이 됩니다.&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;990&quot; data-origin-height=&quot;298&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brlPz2/btrvhNwCoqT/lqCvGKaue40DsRPb4qvOBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brlPz2/btrvhNwCoqT/lqCvGKaue40DsRPb4qvOBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brlPz2/btrvhNwCoqT/lqCvGKaue40DsRPb4qvOBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrlPz2%2FbtrvhNwCoqT%2FlqCvGKaue40DsRPb4qvOBK%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;540&quot; height=&quot;163&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;298&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;이러한 이슈를 해소하기 위해 대개 브라우저에서는 도메인당 기본 6개(브라우저 별 상이)의 Connection을 맺어놓고 데이터를 병렬적으로 요청 및 응답을 통해서 응답성을 개선하고 있습니다.&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;996&quot; data-origin-height=&quot;425&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1w59G/btrvhNXGL5m/GEf1wcqJpETh0uSXIHyaPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1w59G/btrvhNXGL5m/GEf1wcqJpETh0uSXIHyaPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1w59G/btrvhNXGL5m/GEf1wcqJpETh0uSXIHyaPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1w59G%2FbtrvhNXGL5m%2FGEf1wcqJpETh0uSXIHyaPK%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;540&quot; height=&quot;230&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;425&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;또한 개발자 입장에서는 브라우저 특성을 활용하여 자원 다운로드 속도를 빠르게 하기 위해 여러 기법을 사용합니다. 그 중 대표적인 방법은 여러 도메인으로 데이터를 분산하여 저장하고 도메인마다 병렬적으로 Connection 맺어 빠르게 많은 자원을 다운로드하도록 개선하는 방법입니다. 이러한 기법을 도메인 샤딩(Domain Sharding)이라고 합니다.&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;2. Header 문제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP 통신시 헤더에는 많은 메타 정보가 저장되어 있습니다. 이때 사용자가 특정 사이트를 접속하게되면 방문 시점에 다수의 HTTP 요청이 발생하게 될 것입니다. 그리고 매 요청마다 중복된 헤더 값을 전달하며, 쿠키 또한 매 정보 요청마다 포함되어 전송됩니다. 더욱이 Header 정보는&amp;nbsp;Plain text로 전달되고 이는 Binary에 비해 상대적으로 크기가 크기 때문에 전송시 많은 비효율이 발생한다고 볼 수 있습니다.&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. HTTP 2.0 등장&lt;/h3&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;1087&quot; data-origin-height=&quot;291&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cclgF3/btrvt21UZRB/fvYYbseOlnk4qFsLJKKaK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cclgF3/btrvt21UZRB/fvYYbseOlnk4qFsLJKKaK1/img.png&quot; data-alt=&quot;출처 : https://developers.google.com/web/fundamentals/performance/http2/?hl=ko&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cclgF3/btrvt21UZRB/fvYYbseOlnk4qFsLJKKaK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcclgF3%2Fbtrvt21UZRB%2FfvYYbseOlnk4qFsLJKKaK1%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;166&quot; data-origin-width=&quot;1087&quot; data-origin-height=&quot;291&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://developers.google.com/web/fundamentals/performance/http2/?hl=ko&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;HTTP 2.0은 2014년에 표준안이 제안되고 15년에 공개된 프로토콜입니다. HTTP 1.x 버전의 성능 개선을 위해 Multiplexed Streams 기술을 사용합니다. 해당 기술은 이전에 살펴본 HTTP pepelining의 개선 버전으로 하나의 Connection으로 여러개의 데이터를 주고 받을 수 있도록 Stream 처리가 가능합니다.&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;1041&quot; data-origin-height=&quot;889&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eeAM7j/btrvyhDJcEr/efy6EdKFnSk5D03FCRwz9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eeAM7j/btrvyhDJcEr/efy6EdKFnSk5D03FCRwz9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eeAM7j/btrvyhDJcEr/efy6EdKFnSk5D03FCRwz9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeeAM7j%2FbtrvyhDJcEr%2Fefy6EdKFnSk5D03FCRwz9k%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;580&quot; height=&quot;495&quot; data-origin-width=&quot;1041&quot; data-origin-height=&quot;889&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;또한 응답에 대해서 우선순위(Priority)가 주어져서 요청 순서와 관계없이 우선순위가 높을 수록 더 빨리 응답을 할 수 있는 것이 특징입니다.&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;871&quot; data-origin-height=&quot;719&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhOlBk/btrvqccWIKW/fGedRxQKTuwAHgp3KtbKZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhOlBk/btrvqccWIKW/fGedRxQKTuwAHgp3KtbKZ0/img.png&quot; data-alt=&quot;출처 : https://developers.google.com/web/fundamentals/performance/http2/?hl=ko&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhOlBk/btrvqccWIKW/fGedRxQKTuwAHgp3KtbKZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhOlBk%2FbtrvqccWIKW%2FfGedRxQKTuwAHgp3KtbKZ0%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;480&quot; height=&quot;396&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;719&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://developers.google.com/web/fundamentals/performance/http2/?hl=ko&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;세 번째 특징으로는 HTTP 1.1에서는 매 요청마다 동일한 Header 정보를 보내야하는데 반해서 HTTP 2.0 버전에서는 Header 압축을 통해서 지속적인 데이터 요청에 대한 Header 크기를 줄일 수 있습니다.&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;즉 HTTP 2.0을 사용하게되면 더 적은 Connection으로 더 적은 Header 크기를 전송할 수 있으며 Stream 통신으로 인해 여러 데이터를 주고 받을 수 있게 되었습니다.&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;그 밖에 여러 특징이 존재하며, HTTP 2.0에 대해서 더 자세한 내용은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://developers.google.com/web/fundamentals/performance/http2/?hl=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;구글 개발자 페이지&lt;/u&gt;&lt;/a&gt;&lt;/span&gt;를 참고하시기 바랍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. REST API 이슈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 HTTP 2.0 기반위에서 동작하기 때문에 지금까지 HTTP 2.0의 특징에 대해서 살펴봤습니다. 짧게 정리하자면, Header 압축, Multiplexed Stream 처리 지원 등으로 인해 네트워크 비용을 많이 감소시켰습니다.&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;그렇다면 HTTP 2.0 특징을 제외한 gRPC만의 특징은 무엇이 있을까요? 먼저 REST API 통신의 문제점에 대해서 먼저 살펴본 다음 gRPC의 특징에 대해서 살펴보도록 하겠습니다.&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;1) JSON Payload 비효율&lt;/h4&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;694&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qS8hk/btrvwijlITE/1ccjfeSraGlKjJxFWZZK9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qS8hk/btrvwijlITE/1ccjfeSraGlKjJxFWZZK9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qS8hk/btrvwijlITE/1ccjfeSraGlKjJxFWZZK9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqS8hk%2FbtrvwijlITE%2F1ccjfeSraGlKjJxFWZZK9K%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;694&quot; height=&quot;225&quot; data-origin-width=&quot;694&quot; data-origin-height=&quot;225&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;REST 구조에서는 JSON 형태로 데이터를 주고 받습니다. JSON은 데이터 구조를 쉽게 표현할 수 있으며, 사람이 읽기 좋은 표현 방식입니다. 하지만 사람이 읽기 좋은 방식이라는 의미는 머신 입장에서는 자신이 읽을 수 있는 형태로 변환이 필요하다는 것을 의미합니다.&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;964&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mAum1/btrvvCPVokL/B1LhhKbslvRqbFcA4yZQUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mAum1/btrvvCPVokL/B1LhhKbslvRqbFcA4yZQUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mAum1/btrvvCPVokL/B1LhhKbslvRqbFcA4yZQUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmAum1%2FbtrvvCPVokL%2FB1LhhKbslvRqbFcA4yZQUK%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;964&quot; height=&quot;225&quot; data-origin-width=&quot;964&quot; data-origin-height=&quot;225&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;따라서 Client와 Server간의 데이터 송수신간에 JSON 형태로 Serialization 그리고 Deserialization 과정이 수반되어야합니다. JSON 변환은 컴퓨터 CPU 및 메모리 리소스를 소모하므로 수많은 데이터를 빠르게 처리하는 과정에서는 효율이 떨어질 수 밖에 없습니다.&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;2) API Spec 정의 및 문서 표준화 부재&lt;/h4&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;234&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIhdrO/btrvvaFW1DB/c5R74vjA1fGKlPtaSKK98k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIhdrO/btrvvaFW1DB/c5R74vjA1fGKlPtaSKK98k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIhdrO/btrvvaFW1DB/c5R74vjA1fGKlPtaSKK98k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIhdrO%2FbtrvvaFW1DB%2Fc5R74vjA1fGKlPtaSKK98k%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;180&quot; height=&quot;181&quot; data-origin-width=&quot;234&quot; data-origin-height=&quot;235&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;REST API를 사용할 때 가장 큰 고민은 API 개발자와 API를 사용자 간의 효율적인 커뮤니케이션 방법입니다. 가령 API가 어떻게 디자인 되었는지, 그리고 해당 속성은 어떤 값을 입력해야하는지에 대해 상호간의 이해가 필요합니다. REST를 사용한다면 이를 위해서 자체적인 문서나 Restdocs 혹은 Swagger를 통해서 API 문서를 공유합니다. 하지만 이러한 방식은 REST와 관련된 표준은 아닙니다.&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;321&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZEhkY/btrvymMkgmI/611rBiG7amsKNU0K2sfgcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZEhkY/btrvymMkgmI/611rBiG7amsKNU0K2sfgcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZEhkY/btrvymMkgmI/611rBiG7amsKNU0K2sfgcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZEhkY%2FbtrvymMkgmI%2F611rBiG7amsKNU0K2sfgcK%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;321&quot; height=&quot;281&quot; data-origin-width=&quot;321&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 이슈는 JSON 구조는 값은 String으로 표현됩니다. 따라서 사전에 타입 제약 조건에 대한 명확한 합의가 없거나 문서를 보고 개발자가 인지하지 못한다면, Server에 전달전에 이를 검증할 수 없습니다. 가령 위 예시와 같이 Server에서 zipCode는 숫자 타입으로 처리되어야하지만 Client에서는 이에 대한 제약 없이 문자열을 포함시켜 전달할 수 있음을 의미합니다.&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;그렇다면 gRPC 기술은 위 두 가지 이슈를 어떻게 풀어내었을까요?&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;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. gRPC Protobuf&lt;/h3&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;618&quot; data-origin-height=&quot;271&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cJpphG/btrvob6HPN8/r5HbsTwfFA3UjLQl0KkaQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cJpphG/btrvob6HPN8/r5HbsTwfFA3UjLQl0KkaQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cJpphG/btrvob6HPN8/r5HbsTwfFA3UjLQl0KkaQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcJpphG%2Fbtrvob6HPN8%2Fr5HbsTwfFA3UjLQl0KkaQ0%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;480&quot; height=&quot;210&quot; data-origin-width=&quot;618&quot; data-origin-height=&quot;271&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;Client에서 Server측의 API를 호출하기 위해서 기존에는 어떤 Endpoint로 호출해야할 지 그리고 전달 Spec에 대해서 API 문서 작성 혹은 Client와 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;이러한 문제를 감소시키기 위해 다양한 방법이 존재합니다. 그 중 한가지는 Server의 기능을 사용할 수 있는 전용 Library를 Client에게 제공하는 것입니다. 그러면 Client는 해당 Library에서 제공하는 Util 메소드를 활용해서 호출하면 내부적으로는 Server와 통신하여 올바른 결과를 제공할 수 있습니다. 또한 해당 방법은 Server에서 요구하는 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;&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;783&quot; data-origin-height=&quot;431&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pfgTy/btrvygEULu2/geBlbhZJkrFK7EUnJBAElK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pfgTy/btrvygEULu2/geBlbhZJkrFK7EUnJBAElK/img.png&quot; data-alt=&quot;출처 : gRPC 공식 문서(https://grpc.io/docs/what-is-grpc/introduction/)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pfgTy/btrvygEULu2/geBlbhZJkrFK7EUnJBAElK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpfgTy%2FbtrvygEULu2%2FgeBlbhZJkrFK7EUnJBAElK%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;640&quot; height=&quot;352&quot; data-origin-width=&quot;783&quot; data-origin-height=&quot;431&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : gRPC 공식 문서(https://grpc.io/docs/what-is-grpc/introduction/)&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;gRPC에서는 위 그림과 같이 이와 유사한 형태인 Stub 클래스를 Client에게 제공하여 Client는 Stub을 통해서만 gRPC 서버와 통신을 수행하도록 강제화 했습니다.&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;그렇다면 Stub 클래스는 무엇이고 위 그림에서 보이는 Proto는 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1646835808265&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;message Address{
    string city = 1;
    string zip_code = 2;
}

message Person{
    string name = 1;
    int32 age = 2;
    repeated string hobbies = 3;
    optional Address address = 4;
}

service PersonService {
    rpc register(Person) returns (google.protobuf.Empty);
    rpc registerBatch(stream Person) returns (google.protobuf.Empty);
}&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;Protocol Buffer는 Google이 공개한 데이터 구조로써, 특정 언어 혹은 특정 플랫폼에 종속적이지 않은 데이터 표현 방식입니다. 하지만 Protocol Buffer는 특정 언어에 속하지 않으므로 Java나 Kotlin, Golang 언어에서 직접적으로 사용할 수 없습니다.&amp;nbsp;&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;1798&quot; data-origin-height=&quot;1098&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmEUYv/btrvAb4L673/RcVK28rA4kE84Q1eITiyi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmEUYv/btrvAb4L673/RcVK28rA4kE84Q1eITiyi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmEUYv/btrvAb4L673/RcVK28rA4kE84Q1eITiyi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmEUYv%2FbtrvAb4L673%2FRcVK28rA4kE84Q1eITiyi0%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;1798&quot; height=&quot;1098&quot; data-origin-width=&quot;1798&quot; data-origin-height=&quot;1098&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;p data-ke-size=&quot;size16&quot;&gt;따라서 Protocol Buffer를 언어에서 독립적으로 활용하기 위해서는 이를 기반으로 Client 혹은 Server에서 사용할 수 있는 Stub 클래스를 생성해야합니다. 이때 protoc 프로그램을 활용해서 다양한 언어에서 사용할 수 있는 Stub 클래스를 자동 생성할 수 있습니다.&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;1370&quot; data-origin-height=&quot;365&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qa5t2/btrvAVgcDmQ/FPP2j97rACLjEipMSnv1k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qa5t2/btrvAVgcDmQ/FPP2j97rACLjEipMSnv1k1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qa5t2/btrvAVgcDmQ/FPP2j97rACLjEipMSnv1k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqa5t2%2FbtrvAVgcDmQ%2FFPP2j97rACLjEipMSnv1k1%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;1370&quot; height=&quot;365&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;365&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;만약 Server가 Java 혹은 Kotlin 기반으로 구성되어있고 Client도 Java 혹은 Kotlin이라면, 위와 같이 Stub 생성을 자동으로 해주는 Library를 활용할 수 있습니다.&amp;nbsp;&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;353&quot; data-origin-height=&quot;533&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsUPza/btrvBkf6ioM/L2BRDUyUWLkgiEvBF8YaG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsUPza/btrvBkf6ioM/L2BRDUyUWLkgiEvBF8YaG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsUPza/btrvBkf6ioM/L2BRDUyUWLkgiEvBF8YaG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsUPza%2FbtrvBkf6ioM%2FL2BRDUyUWLkgiEvBF8YaG1%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;533&quot; data-origin-width=&quot;353&quot; data-origin-height=&quot;533&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;위 그림은 Library를 활용해서 Build 시점에 Proto 파일을 찾고 컴파일 단계에서 이를 분석해서 Stub 클래스를 자동으로 생성된 모습입니다.&amp;nbsp;&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;919&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dWBmzS/btrvyltjDXI/sGG0fq9TrPFNBmPR8h2lO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dWBmzS/btrvyltjDXI/sGG0fq9TrPFNBmPR8h2lO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWBmzS/btrvyltjDXI/sGG0fq9TrPFNBmPR8h2lO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdWBmzS%2FbtrvyltjDXI%2FsGG0fq9TrPFNBmPR8h2lO0%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;643&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;643&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;Stub 클래스를 생성하면, 해당 클래스 정보를 Server와 Client에 공유한 다음 Stub 클래스를 활용하여 서로 양방향 통신을 수행할 수 있습니다.&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;523&quot; data-origin-height=&quot;263&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzX22h/btrvwijwDHZ/HHPKf350Jg124Hir2E8zn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzX22h/btrvwijwDHZ/HHPKf350Jg124Hir2E8zn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzX22h/btrvwijwDHZ/HHPKf350Jg124Hir2E8zn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzX22h%2FbtrvwijwDHZ%2FHHPKf350Jg124Hir2E8zn0%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;263&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;263&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;위 코드는 Stub 객체를 활용하여 Client에서 특정 RPC를 호출한 모습입니다. REST 방식을 활용한다면 RestTemplate 혹은 Webclient나 Retrofit2와 같은 도구 활용해서 JSON으로 데이터를 전송해야합니다. 반면 gRPC 방법에서는 위와같이 Stub 객체에 정의된 메소드 호출을 통해서 Client/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;지금까지 학습한 Protocol Buffer 내용을 정리하면 다음과 같은 장점을 지닌 것을 확인할 수 있습니다.&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;1. 스키마 타입 제약이 가능하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. Protocol buffer가 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;위 두가지 특징은 이전에 REST에서 다룬 이슈 중 하나인 API Spec 정의 및 문서 표준화 부재의 문제를 어느정도 해소해줄 수 있습니다. 그렇다면 또 하나의 이슈인 JSON Payload 비효율 문제와 대비하여 gRPC는 어떠한 이점을 지니고 있을까요?&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;1669&quot; data-origin-height=&quot;386&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxbe5v/btrvBle23ux/jVOKUVPQS4C4prNCqXoRF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxbe5v/btrvBle23ux/jVOKUVPQS4C4prNCqXoRF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxbe5v/btrvBle23ux/jVOKUVPQS4C4prNCqXoRF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcxbe5v%2FbtrvBle23ux%2FjVOKUVPQS4C4prNCqXoRF0%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;1669&quot; height=&quot;386&quot; data-origin-width=&quot;1669&quot; data-origin-height=&quot;386&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;JSON 타입은 위와같이 사람이 읽기는 좋지만 데이터 전송 비용이 높으며, 해당 데이터 구조로 Serialization, Deserialization 하는 비용이 높음을 앞서 지적했습니다.&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RRQjf/btrvvCJq6nB/0gg9chclkot8EUCKKIL470/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RRQjf/btrvvCJq6nB/0gg9chclkot8EUCKKIL470/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RRQjf/btrvvCJq6nB/0gg9chclkot8EUCKKIL470/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRRQjf%2FbtrvvCJq6nB%2F0gg9chclkot8EUCKKIL470%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;1456&quot; height=&quot;145&quot; data-origin-width=&quot;1456&quot; data-origin-height=&quot;145&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;gRPC의 통신에서는 데이터를 송수신할 때 Binary로 데이터를 encoding 해서 보내고 이를 decoding 해서 매핑합니다. 따라서 JSON에 비해 payload 크기가 상당히 적습니다.&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;또한 JSON에서는 필드에 값을 입력하지 않아도 구조상에 해당 필드가 포함되어야하기 때문에 크기가 커집니다.&amp;nbsp; 반면 gRPC에서는 입력된 값에 대해서만 Binary 데이터에 포함시키기 때문에 압축 효율이 JSON에 비해 상당히 좋습니다.&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;결론적으로 이러한 적은 데이터 크기 및 Serialization, Deserialization 과정의 적은 비용은 대규모 트래픽 환경에서 성능상 유리합니다.&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. gRPC 단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 gRPC에서 사용되는 기반 기술에 대해서 살펴봤습니다. gRPC는 MSA 환경에서 문제점인 네트워크 지연 문제를 어느정도 해결해 줄 수 있는 기술로써 점차 많은 곳에서 도입을 진행하고 있지만 다음과 같은 문제점 또한 존재합니다.&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;1) 브라우저에서 gRPC를 직접 지원 안함&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 gRPC-WEB을 사용해서 직접 브라우저에서 서버로 gRPC 통신을 수행할 수 없습니다. 따라서 Envoy와 같은 Proxy 서버를 통해 요청을 Forwarding 해야합니다.&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;또 다른 방법으로는 gRPC 서버와 브라우저 사이에 Aggregator 서버를 별도로 두어 Aggregator와 브라우저간에는 REST 통신을 수행하고 Aggregator와 gRPC 서버간에 gRPC 통신을 수행하는 방법을 사용해야합니다.&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;2) Stub 관리 비용 추가&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client와 Server는 Stub 클래스를 통해 서로 통신을 수행합니다. 하지만 요구사항 변경으로인해 Stub 클래스 변경이 필요할 때 Server에서 변경한 내용을 Client에서도 적용을 해야합니다. 이 경우 버전 차이로 인한 하위 호환성 문제가 발생할 수 있기 때문에 서비스간 Stub 관리 방법을 정의해야합니다.&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;가장 많이 사용하는 방법으로는 Proto 파일을 중앙에서 gitops 형식으로 관리하고 변경이 생겼을 때 이를 감지하고 언어별로 컴파일하여 Stub 클래스를 라이브러리 형태로 배포하는 방법을 많이 사용합니다.&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 gRPC가 MSA 환경에서 왜 대두되었는지 기존의 방식과 어떠한 차이점이 있는지에 대해서 간략하게 알아봤습니다. 다음 포스팅에서는 gRPC와 REST를 다각도로 비교해보면서 gRPC가 어떠한 장점이 있는지를 분석해보겠습니다.&lt;/p&gt;</description>
      <category>MSA/gRPC</category>
      <category>GRPC</category>
      <category>gRPC 구조</category>
      <category>HTTP 1.1</category>
      <category>HTTP 2.0</category>
      <category>kotlin grpc</category>
      <category>protobuf</category>
      <category>REST</category>
      <category>Spring boot</category>
      <category>Spring Boot gRPC</category>
      <category>spring grpc</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/175</guid>
      <comments>https://cla9.tistory.com/175#entry175comment</comments>
      <pubDate>Sat, 5 Mar 2022 14:49:28 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot Excel 업로드 라이브러리 개발기</title>
      <link>https://cla9.tistory.com/118</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Excel Upload 기능이 필요하여 많이쓰는 POI 라이브러리를 살펴보았으나, 2가지 아쉬운점이 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1606027652322&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1. 비즈니스 로직과 POI 라이브러리 코드의 강결합
2. DOM과 SAX 방식은 코드 작성 방법이 달라 둘 다 쓰는데 있어 유지보수의 어려움&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;DOM 방식&lt;/p&gt;
&lt;pre id=&quot;code_1606021807212&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    Workbook workbook = WorkbookFactory.create(file.getInputStream());
    Sheet sheet = workbook.getSheetAt(0);
    for(int i = 0 ; i &amp;lt; sheet.getPhysicalNumberOfRows() ; i++){
        final Row row = sheet.getRow(i);
        for(int j = 0; j &amp;lt; row.getPhysicalNumberOfCells(); j++){
            final Cell cell = row.getCell(j);
            //Business Code
        }
    }
} catch (IOException e) {
    e.printStackTrace();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;SAX 방식&lt;/p&gt;
&lt;pre id=&quot;code_1606021822752&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    OPCPackage pkg = OPCPackage.open(file.getInputStream());
    XSSFReader r = new XSSFReader(pkg);
    SharedStringsTable sst = r.getSharedStringsTable();
    StylesTable styles = r.getStylesTable();
    XMLReader parser = XMLHelper.newXMLReader();


    SAXSheetHandler sheetHandler = //사용자가 정의한 SAXSheetHandler(? extends DefaultHandler)
    ContentHandler handler = new XSSFSheetXMLHandler(styles, sst, sheetHandler, false);
    SAXRowHandler rowHandler = new SAXRowHandler();


    parser.setContentHandler(handler);
    try (InputStream sheet = r.getSheetsData().next()) {
        parser.parse(new InputSource(sheet));
    }
}
catch (Exception e) {
     e.printStackTrace();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이러한 문제를 어떻게 해결할 수 있을까 고민하던 와중 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://woowabros.github.io/experience/2020/10/08/excel-download.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;우아한 형제들 Excel 기술 블로그&lt;/a&gt;&lt;/u&gt;&lt;/span&gt;를 보고 영감을 얻어 Excel 업로드 라이브러리를 개발하기로 했습니다. 이번 포스팅은 개인 프로젝트로 진행한 라이브러리 설계 과정과 적용 기술 및 개발 당시 어려움을 겪은 내용을 다루겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;라이브러리 사용법 및 공식 문서는 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://github.com/cla9/excel-parser-spring-boot-starter&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Github&lt;/a&gt;&lt;/u&gt;&lt;/span&gt; 및 &lt;a href=&quot;https://github.com/cla9/excel-parser-spring-boot-starter/wiki&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;Wiki&lt;/u&gt; &lt;/span&gt;&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 개발에 앞서 필요 기능을 리스트업 했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. &lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;DOM&lt;/b&gt;과 &lt;b&gt;SAX&lt;/b&gt; 방법에 대한 추상화된 API를 제공해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. &lt;b&gt;Streaming&lt;/b&gt; 방식과 &lt;b&gt;Collection&lt;/b&gt; 방식을 제공해야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3. 사용자 코드에서 POI 코드가 직접적으로 &lt;b&gt;의존&lt;/b&gt;되지 않아야한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;4. &lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;학습비용&lt;/b&gt;이 낮아야한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;5. &lt;b&gt;다국어&lt;/b&gt; 처리를 지원해야한다.&lt;/p&gt;
&lt;p&gt;6. &lt;b&gt;Validation&lt;/b&gt; 기능을 제공해야한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. DOM 과 SAX 방식에 대한 공통 API 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;POI에서 제공하는 DOM과 SAX 방식은 구현 방법이 완전히 다릅니다. 그 이유는 제공하는 API도 다를 뿐더러 DOM 방식은 Pull 방식, SAX 방식은 Push 방식으로 Parsing 결과를 제공하기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서, 먼저 이러한 두 가지 방법에 대해서 공통으로 처리할 수 있는 API를 설계하고 이를 Interface 제공하도록 구상하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qhLaJ/btqNXFnvwop/1gcGqIQ9DfliZDG0UT5s00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qhLaJ/btqNXFnvwop/1gcGqIQ9DfliZDG0UT5s00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qhLaJ/btqNXFnvwop/1gcGqIQ9DfliZDG0UT5s00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqhLaJ%2FbtqNXFnvwop%2F1gcGqIQ9DfliZDG0UT5s00%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같이 interface를 정의하면 사용자는 구현의 Detail은 알 필요없이 API 호출만으로 SAX 방식 혹은 DOM 방식으로 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3iO0o/btqNWEP8Vfy/61cbEXX0YUvClRyQtgBHEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3iO0o/btqNWEP8Vfy/61cbEXX0YUvClRyQtgBHEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3iO0o/btqNWEP8Vfy/61cbEXX0YUvClRyQtgBHEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3iO0o%2FbtqNWEP8Vfy%2F61cbEXX0YUvClRyQtgBHEk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;두번째는 Excel 파일에서 데이터를 Parsing하기 위해서는 Sheet에 대한 처리, 각각의 Row에 대한 처리가 필요합니다. 따라서, 이전과 마찬가지로 Row와 Sheet에 대한 각각의 inteface를 정의한 다음 각각의&amp;nbsp; Reader는 interface에 의존함으로써, 공통화된 기능을 제공할 수 있도록 설계했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QtCpW/btqNYxitRl4/UXg9vWL1jXoHOSqGKuAKwk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QtCpW/btqNYxitRl4/UXg9vWL1jXoHOSqGKuAKwk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QtCpW/btqNYxitRl4/UXg9vWL1jXoHOSqGKuAKwk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQtCpW%2FbtqNYxitRl4%2FUXg9vWL1jXoHOSqGKuAKwk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1606031873235&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ReaderFactory {
    private final ExcelMetaModelMappingContext context;
    
    public ReaderFactory(ExcelMetaModelMappingContext context) {
        this.context = context;
    }
    
    public &amp;lt;T&amp;gt; Reader&amp;lt;T&amp;gt; createInstance(ReaderType type, Class&amp;lt;T&amp;gt; tClass)  {
        final boolean isCached = context.hasMetaModel(tClass);
        if (type == ReaderType.WORKBOOK) {
            return isCached ? new WorkBookReader&amp;lt;&amp;gt;(tClass, context.getMetaModel(tClass)) : new WorkBookReader&amp;lt;&amp;gt;(tClass);
        }
        return isCached ? new SAXReader&amp;lt;&amp;gt;(tClass, context.getMetaModel(tClass)) : new SAXReader&amp;lt;&amp;gt;(tClass);
    }

    public  &amp;lt;T&amp;gt; Reader&amp;lt;T&amp;gt; createInstance(Class&amp;lt;T&amp;gt; tClass){
        final ExcelBody entity = tClass.getAnnotation(ExcelBody.class);
        return createInstance(entity.type(), tClass);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기에, Factory 클래스를 추가하여 사용자가 Enum 값으로 SAX 혹은 DOM(WorkBook) 방식 중 하나를 지정하면, 그에 해당하는 Excel Reader를 생성하도록 추가하였습니다. 지금까지 설명한 내용을 도식화하면 위 그림과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2.&amp;nbsp; Annotation 기반 메타 정보 작성&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Excel로 읽는 각 Row 데이터는 결국 특정 Entity로 변환되어 DB에 저장되거나 비즈니스 로직에서 사용될 것입니다. 이러한 Entity를 POJO스럽게 유지하면서도 라이브러리에서 필요한 다양한 메타 정보를 기록할 수 있는 방법 중 하나는 &lt;b&gt;@Annotation&lt;/b&gt; 활용입니다. Spring 환경에서 개발하면, 다양한 Annotation을 접하게 되는데, 라이브러리를 개발함에 있어서도 이러한 Annotation을 사용하여 Entity 클래스내에 라이브러리 코드가 직접 침투되지 않도록 설계하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또한, Annotation을 사용함에 있어 &lt;b&gt;JPA&lt;/b&gt;와 유사한 스타일을 적용하면, 학습곡선을 많이 낮출 수 있다고 생각하여 비슷하게 디자인했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1606029611526&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExcelBody(dataRowPos = 3, 
           type = ReaderType.SAX,
           headerRowRange = @RowRange(start = 1, end = 2),
           messageSource = PersonMessageConverter.class)
@ExcelBody(dataRowPos = 2)
@ExcelMetaCachePut
@ExcelColumnOverrides({
        @ExcelColumnOverride(headerName = &quot;생성일&quot;, index = 8, column = @ExcelColumn(headerName = &quot;생성일자&quot;)),
        @ExcelColumnOverride(headerName = &quot;수정일&quot;, index = 10, column = @ExcelColumn(headerName = &quot;수정일자&quot;))
})
public class Person extends BaseAuditEntity{
    @ExcelColumn(headerName = &quot;이름&quot;)
    @NotNull
    private String name;
    
    @Merge(headerName = &quot;전화번호&quot;)
    @ExcelColumnOverrides(@ExcelColumnOverride(headerName = &quot;집전화번호&quot;, index = 5, column = @ExcelColumn(headerName = &quot;휴대전화번호&quot;, index = 4)))
    private Phone phone;
    
    @ExcelEmbedded
    private Address address;
    
    @ExcelColumn(headerName = &quot;생성일자&quot;)
    @DateTimeFormat(pattern = &quot;yyyyMMdd&quot;)
    private LocalDate createdAt;
    
    @ExcelColumn(headerName = &quot;성별&quot;)
    @ExcelConvert(converter = GenderConverter.class)
    private Gender gender;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;결과적으로 위와 같이 Entity내 라이브러리 코드 작성 없이 메타 Annotation을 작성하게되면, 라이브러리 코드내에서 해당 Annotation 정보들을 참조하여 Entity 생성 및 데이터를 주입할 수 있도록 하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;(사용법은 &lt;a href=&quot;https://github.com/cla9/excel-parser-spring-boot-starter/wiki&quot;&gt;Excel-Parser Wiki&lt;/a&gt; 페이지를 참고하시기 바랍니다)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Reflection 활용&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사용자 코드에서 &lt;b&gt;무엇을&lt;/b&gt;(What) 처리 해야할지 명시하고 &lt;b&gt;어떻게&lt;/b&gt;(How) 처리해야할지는 기술하지 않았습니다. 즉 원하는 바만 선언하였으니, 라이브러리내에서 메타 정보를 읽어들여 사용자가 원하는대로 처리하고 반환 해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Java에서는 Runtime 시점에 Reflection을 통해서 Instance 및 Class의 내부 정보를 알 수 있는 방법을 제공합니다. 따라서 이를 활용해서 라이브러리 내부에서 &lt;b&gt;Annotation 분석 &amp;rarr; 데이터 Parsing &lt;b&gt;&amp;rarr;&lt;/b&gt; Entity 생성 &lt;b&gt;&amp;rarr;&lt;/b&gt; 데이터 주입 &lt;b&gt;&amp;rarr; 데이터&lt;/b&gt; Validation 검증 &lt;/b&gt;과정 순서대로 처리할 수 있도록 구상하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;540&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1Chnl/btqNXGfGlhX/cf4202Rw4OGLeUkloWyKK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1Chnl/btqNXGfGlhX/cf4202Rw4OGLeUkloWyKK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1Chnl/btqNXGfGlhX/cf4202Rw4OGLeUkloWyKK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1Chnl%2FbtqNXGfGlhX%2Fcf4202Rw4OGLeUkloWyKK0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;540&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 4가지 단계에서 데이터 Parsing은 SAX Reader, WorkBook Reader가 담당하는 것을 이전 내용을 통해 확인했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서, Annotation 분석과, Entity 생성을 위해 이를 담당할 Class를 추가로 생성하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ExcelEntityParser와 EntityInstantiator는 Reflection을 활용하여, Entity 내부를 탐색하는 과정을 담당합니다. Parser는 이 과정에서 Entity에 작성된 Annotation의 유효성 검증 및 헤더 정보 등을 취합하는 역할을 담당하고, Instantiator는 Entity를 생성하고, Parser에서 취합된 헤더 정보를 토대로 데이터를 주입하는 역할을 담당합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1606033056105&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ExcelEntityParser implements EntityParser {
    ...(중략)...
    private void doParse() {
        visited.add(tClass);
        findAllFields(tClass);
        final int annotatedFieldHeight = extractHeaderNames();

        calcHeaderRange(annotatedFieldHeight);
        validateHeaderRange();
        calcDataRowRange();
        validateOverlappedRange();
        extractOrder();
        validateOrder();
        validateHeaderNames();
    }
    
    ...(중략)...
    private void findAllFields(final Class&amp;lt;?&amp;gt; tClass) {
        ReflectionUtils.doWithFields(tClass, field -&amp;gt; {
            final Class&amp;lt;?&amp;gt; clazz = field.getType();

            if(field.isAnnotationPresent(ExcelConvert.class)){
                final Class&amp;lt;?&amp;gt; converterType = field.getAnnotation(ExcelConvert.class).converter();
                if(!converterType.getSuperclass().isAssignableFrom(ExcelColumnConverter.class)){
                    throw new InvalidHeaderException(String.format(&quot;Only ExcelColumnConverter is allowded. Entity : %s Converter: %s&quot;,this.tClass.getName(), converterType.getName()));
                }
            }
            else if(instantiatorSource.isSupportedDateType(clazz) &amp;amp;&amp;amp; !field.isAnnotationPresent(DateTimeFormat.class)){
                throw new InvalidHeaderException(String.format(&quot;Date Type must be placed @DateTimeFormat Annotation. Entity : %s Field : %s &quot;, this.tClass.getName(), clazz.getName()));
            }
            else if(!instantiatorSource.isSupportedInjectionClass(clazz) &amp;amp;&amp;amp; visited.contains(clazz)){
                throw new UnsatisfiedDependencyException(String.format(&quot;Unsatisfied dependency expressed between class '%s' and '%s'&quot;, tClass.getName(), clazz.getName()));
            }

            if (instantiatorSource.isSupportedInjectionClass(clazz)) {
                declaredFields.add(field);
            } else {
                visited.add(clazz);
                findAllFields(clazz);
                visited.remove(clazz);
            }
        });
    }
    ...(중략)...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1606033133247&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class EntityInstantiator&amp;lt;T&amp;gt; {
    ...(중략)...
    public &amp;lt;R&amp;gt; EntityInjectionResult&amp;lt;T&amp;gt; createInstance(Class&amp;lt;? extends T&amp;gt; clazz, List&amp;lt;String&amp;gt; excelHeaderNames, ExcelMetaModel excelMetaModel, RowHandler&amp;lt;R&amp;gt; rowHandler) {
        resourceCleanUp();
        final T object = BeanUtils.instantiateClass(clazz);

        ReflectionUtils.doWithFields(clazz, f -&amp;gt; {
            if (!excelMetaModel.isPartialParseOperation()) {
                instantiateFullInjectionObject(object, excelHeaderNames, excelMetaModel, f, rowHandler);
            } else if (excelMetaModel.getInstantiatorSource().isCandidate(f)) {
                instantiatePartialInjectionObject(object, excelHeaderNames, excelMetaModel, f);
            }
        });

        if (excelMetaModel.isPartialParseOperation()) {
            setupInstance(excelHeaderNames, excelMetaModel.getInstantiatorSource(), rowHandler);
        }

        return new EntityInjectionResult&amp;lt;&amp;gt;(object, List.copyOf(exceptions));
    }
    
    ...(중략)...
    private &amp;lt;U&amp;gt; void setupInstance(final List&amp;lt;? extends String&amp;gt; headers, EntitySource entitySource, final RowHandler&amp;lt;U&amp;gt; rowHandler) {
        for (int i = 0; i &amp;lt; instances.size(); i++) {
            if (Objects.isNull(instances.get(i))) continue;

            Field field = instances.get(i).field;
            Class&amp;lt;?&amp;gt; type = field.getType();
            field.setAccessible(true);
            String value = rowHandler.getValue(i);

            try {
                final Object instance = instances.get(i).instance;
                if (!StringUtils.isEmpty(value)) {
                    inject(entitySource, field, type, value, instance);
                }
                validate(instance, headers.get(i), value, field.getName()).ifPresent(exceptions::add);
            } catch (IllegalAccessException | ParseException e) {
                addException(headers, field, value, e.getLocalizedMessage());
            }

        }
    }    
    ...(중략)...
}    &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rGDQw/btqNYww6NIU/NWDy4MIRhDi1nWcHXurts0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rGDQw/btqNYww6NIU/NWDy4MIRhDi1nWcHXurts0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rGDQw/btqNYww6NIU/NWDy4MIRhDi1nWcHXurts0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrGDQw%2FbtqNYww6NIU%2FNWDy4MIRhDi1nWcHXurts0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Entity Parser와 Instantiator까지 적용되면, 라이브러리로 Excel Parsing 요청시, 위 흐름대로 처리되는 것을 이해할 수 있습니다.&lt;/p&gt;
&lt;p&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전 내용을 토대로 기본적인 구현을 마친 이후 테스트를 해보자 몇가지 추가 고민이 생겼습니다. 그리고 이것은 이후 시작되는 삽질의 첫삽을 푼 순간이었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;고민거리&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Entity에 지정된 Annotation 유효성 검증을 런타임에 수행하는데, Spring Boot 기동시점인 로드 타임에 검증하는 것이 더 좋지 않을까?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Maven Central에 배포해보자!!!&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;삽질 1. 대상 Entity 클래스 Scanning&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 기동 시점에 검증을 하려면, Excel Parser 라이브러리의 대상 Entity를 모두 찾을 수 있어야 합니다. 따라서, Spring에서 Bean Scanning 하는 코드 및 관련 클래스를 사용해야겠다고 생각했지만 검색 능력의 부족으로 인해 찾는데 많은 어려움을 겪었습니다. 많은 시행착오 끝에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;u&gt;&lt;b&gt;ClassPathScanningCandidateComponentProvider&lt;/b&gt;&lt;/u&gt; &lt;span style=&quot;color: #000000;&quot;&gt;클래스가 해당 기능을 제공하는 것을 확인할 수 있었습니다.&lt;/span&gt;&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_1606036579848&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.findCandidateComponents(&quot;base 패키지명&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;style3&quot; /&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;u&gt;삽질 2. Default base 패키지명은 어떻게 알 수 있을까?&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClassPathScanningCandidateComponentProvider 클래스를 통해 base 패키지명을 String 타입으로 전달하면, 하위 패키지내 클래스를 탐색하는 기능을 제공해줌을 알 수 있습니다.&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;size16&quot;&gt;&lt;i&gt;'&lt;u&gt;Spring Data JPA에서는 @EnableJpaRepositories Annotation을 통해 basePackages를 입력하지 않아도 Repository Bean을 만들 수 있었는데, 어떤 원리로 그런것일까?&lt;/u&gt; '&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이것을 알기위해 구글링을 해봤지만, 어떠한 keyword로 검색해야할지 몰라 정확한 정보를 찾을 수 없었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;(대부분 @EnableJpaRepositories 설정 방법이나 basePackage를 지정하는 방법 관련된 검색결과가 대다수였습니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;결국, 선택한 방법은 코드내 EnableJpaRepositories 부터 시작해서 관련된 클래스 Debugger를 걸어놓고 코드를 따라 거슬러 오르는 방법이었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qnaTq/btqNXewYhnn/cSuwkqNRuKMonRLOR3u3pk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qnaTq/btqNXewYhnn/cSuwkqNRuKMonRLOR3u3pk/img.png&quot; data-alt=&quot;EnableJpaRepositores 검색결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qnaTq/btqNXewYhnn/cSuwkqNRuKMonRLOR3u3pk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqnaTq%2FbtqNXewYhnn%2FcSuwkqNRuKMonRLOR3u3pk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;EnableJpaRepositores 검색결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5Bo0n/btqNXdx7XY1/nNvWDZetaC53x7YqSEcps1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5Bo0n/btqNXdx7XY1/nNvWDZetaC53x7YqSEcps1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5Bo0n/btqNXdx7XY1/nNvWDZetaC53x7YqSEcps1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5Bo0n%2FbtqNXdx7XY1%2FnNvWDZetaC53x7YqSEcps1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;추적끝에 찾은 결과는 위와 같습니다. @EnableJpaRepositories 어노테이션을 Configuration 클래스에 선언하면, JpaRepositoriesRegistrar 클래스 정보가 같이 Import 됩니다. 이때, &lt;b&gt;JpaRepositoryConfigExtension&lt;/b&gt; 클래스가 Bean으로 등록됩니다. 그리고 RepositoryConfigurationDelegate에게 Bean 탐색을 위임합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuvztQ/btqNXFuncsN/nNHs08Rk9kuXrpfPLddvu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuvztQ/btqNXFuncsN/nNHs08Rk9kuXrpfPLddvu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuvztQ/btqNXFuncsN/nNHs08Rk9kuXrpfPLddvu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuvztQ%2FbtqNXFuncsN%2FnNHs08Rk9kuXrpfPLddvu0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이때, basePackages를 설정하게 되는데, 사용자가 지정한 &lt;b&gt;@EnableRepositories&lt;/b&gt;&amp;nbsp;Package 정보를 가져와서 지정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bK9gS9/btqNWf4cp83/AsJ7sVQNdVOKaUJ6VU3wA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bK9gS9/btqNWf4cp83/AsJ7sVQNdVOKaUJ6VU3wA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bK9gS9/btqNWf4cp83/AsJ7sVQNdVOKaUJ6VU3wA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbK9gS9%2FbtqNWf4cp83%2FAsJ7sVQNdVOKaUJ6VU3wA0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;JpaRepositoriesAutoConfiguration은 JpaRepository 관련 자동설정을 하는데, JpaRepositoryConfigExtension 클래스가 Bean으로 등록되어있으면, 관련 자동설정을 하지 않습니다. @EnableJpaRepositories Annotation을 사용자가 지정했다면, 이전에 설명했듯이, &lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;JpaRepositoryConfigExtension&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;가 Bean으로 등록되었기 때문에 자동설정을 하지 않습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CIE9C/btqNWhAZzEg/4z7pKjULTq4xPd25fVDhk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CIE9C/btqNWhAZzEg/4z7pKjULTq4xPd25fVDhk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CIE9C/btqNWhAZzEg/4z7pKjULTq4xPd25fVDhk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCIE9C%2FbtqNWhAZzEg%2F4z7pKjULTq4xPd25fVDhk0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;반면 @EnableJpaRepositories Annotation이 존재하지 않는다면, 마찬가지로 &lt;span style=&quot;color: #333333;&quot;&gt;RepositoryConfigurationDelegate에게 Bean 탐색을 위임합니다. 이때 사용되는 basePackges는 AutoConfigurationPackages.get 메소드를 통해 얻을 수 있습니다. 그리고 해당 메소드가 바로 &lt;b&gt;Spring Boot에서 사용되는 기본 basePackges 정보&lt;/b&gt;임을 알 수 있었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;u&gt;삽질 3. JPA는 되는데 난 안돼!!!&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Spring Data JPA에서 사용되는 자동설정 Idea를 토대로 개발중인 라이브러리에 적용하기로 했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span&gt;@EnableExcelEntityScan&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;Annotation과 AutoConfiguration 클래스를 만들어서 사용자가 Annotation을 지정하여 basePackage를 지정하지 않으면 AutoConfiguration의 설정을 따르도록 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 아무리 &lt;b&gt;AutoConfigurationPackages.&lt;span&gt;get&lt;/span&gt;&lt;/b&gt;&amp;nbsp;메소드를 호출해도 Bean 정보가 없다는 Exception이 발생하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;처음에는 &lt;span style=&quot;color: #333333;&quot;&gt;AutoConfigurationPackages.get가 아니라 혹시 다른 메소드가 이를 대신하나 싶어서 샅샅히 찾아봤지만 코드상에서는 찾을 수 없었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그렇게 한참을 삽질하다 문득 spring.factories에 EnableAutoConfiguration 설정을 하지 않았음을 알게 되었고, 설마 이것때문에? 라는 생각으로 관련 AutoConfiguration 클래스를 등록시켰습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FMT0r/btqN2VXgIan/wKemvYB9yU0LmahR11TTPk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FMT0r/btqN2VXgIan/wKemvYB9yU0LmahR11TTPk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FMT0r/btqN2VXgIan/wKemvYB9yU0LmahR11TTPk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFMT0r%2FbtqN2VXgIan%2FwKemvYB9yU0LmahR11TTPk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그 결과, 설정 이후에 정상적으로 basePackage 정보를 가져오는 것을 확인하고 많이 부족함을 재차 느꼈습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p&gt;&lt;u&gt;삽질 4. Gradle기반 Spring Boot Starter 만들기&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Spring Boot Starter 관련하여, Maven 기반으로 Starter를 작성하는 방법에 대해서는 다수 있지만, Gradle로 만드는 방법은 찾기 어려웠습니다. 다만 Spring Boot Starter 개념은 아래 링크에 참고된 블로그를 통해서 학습할 수 있었습니다. 한참의 삽질끝에 완성할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고 블로그&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- &lt;u&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://nevercaution.github.io/spring-boot-starter-custom/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;nevercaution.github.io/spring-boot-starter-custom/&lt;/a&gt;&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p&gt;&lt;u&gt;삽질 5. Maven Central 배포하기&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;운이 좋게 저보다 앞서 고생하시고 그 기록을 남겨주신&amp;nbsp;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://siyoon210.tistory.com/167&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;siyoon210님 블로그&lt;/a&gt;&lt;/u&gt;&lt;/span&gt;를 통해서 다른 과정과 비교했을 때 큰 문제 없이 업로드할 수 있었습니다.&lt;/p&gt;
&lt;p&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;숲을 제대로 모른 상태에서 나무만 보면서 만들다보니 삽질이 많았습니다. 하지만 그런 시행착오를 겪으면서 배워서 그런지 학습한 내용이 보다 오랫동안 기억에 남을 것같습니다. 공식 문서에 사용법에 대해서 작성했으나 나중에 기회가된다면 튜토리얼 포스팅을 작성해볼까 합니다. 관련된 자료는 아래 링크를 참고하시기 바랍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로 해당 라이브러리에 대한 코드기여는 언제나 환영입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;GitHub :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://github.com/cla9/excel-parser-spring-boot-starter&quot;&gt;Excel-Parser&lt;/a&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Documentation :&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;u&gt;&lt;a style=&quot;color: #006dd7;&quot; href=&quot;https://github.com/cla9/excel-parser-spring-boot-starter/wiki&quot;&gt;Excel-Parser Wiki&lt;/a&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>JAVA/Excel</category>
      <category>apache</category>
      <category>dom</category>
      <category>Excel</category>
      <category>POI</category>
      <category>SAX</category>
      <category>Spring boot</category>
      <category>라이브러리</category>
      <category>스프링 부트</category>
      <category>스프링 엑셀 업로드</category>
      <category>엑셀 업로드</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/118</guid>
      <comments>https://cla9.tistory.com/118#entry118comment</comments>
      <pubDate>Mon, 23 Nov 2020 22:07:04 +0900</pubDate>
    </item>
    <item>
      <title>2. Redis 샤딩</title>
      <link>https://cla9.tistory.com/102</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전 포스팅에서 Redis의 기본적인 구조와 복제(Replication)에 대해서 살펴봤습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;잠시 복기해보자면, 복제는 Master의 데이터를 Replica에 모두 저장하여 가용성과 읽기 작업의 성능을 높일 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 데이터 양이 폭발적으로 증가한다면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Replication은 모든 데이터를 복제해야하기 때문에 단일 서버에서 저장 가능한 Memory를 초과하면 이를 복제할 수 없습니다. 따라서 메모리 증설등을 통한 Scale-Up 만으로 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;데이터 저장 공간을 확보할 수 없다면 다른 방법이 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 데이터 분산을 통해 고가용성을 확보할 수 있는 파티셔닝 개념과&amp;nbsp;Redis에서 사용되는 샤딩전략 그리고 Cluster에 대해 다루도록 하겠습니다.&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;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;파티셔닝은 DB의 관리 용이성 및 읽기 최적화를 위해 논리적인 테이블의 물리 구조를 여러개의 파티션(Partition)으로 분할하여 분산 저장하는 기법을 말합니다. 파티셔닝 개념에 대한 이해를 돕기위해 잠시 RDBMS에서의 파티셔닝에 대해서 간략하게 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;540&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n4jhO/btqGmQKPUEs/fvvHoJE2INnhYJxuf3jPV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n4jhO/btqGmQKPUEs/fvvHoJE2INnhYJxuf3jPV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n4jhO/btqGmQKPUEs/fvvHoJE2INnhYJxuf3jPV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn4jhO%2FbtqGmQKPUEs%2FfvvHoJE2INnhYJxuf3jPV1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;540&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림과 같은 회원 테이블이 존재한다고 가정해봅시다. 이때 가입자가 매일 증가하여 테이블 크기가 점점 커진다면, 해당 테이블에 조회 성능을 높이기 위해 인덱스 추가등의 작업이 쉽지 않을 뿐더러 점차 조회 성능도 떨어지게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;가령 수십억건의 데이터가 존재하는 테이블에서 매월마다 가입일이 5년지난 데이터를 삭제해야 한다면 어떻게 해야할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;데이터를 지우기 위해서 수십억건의 테이블을 탐색하면서 조건에 해당하는 데이터를 삭제해야합니다. 이렇게 되면 오랜시간동안 Lock으로 인해 동시성 저하가 발생할 수 있으며, &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;테이블 크기가 점점 더 커질 수록 해당 작업은 어려워질 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;설령 가입일에 인덱스가 생성되어있다 할지라도 디스크 Random I/O로 인해 좋은 성능이 나오지 않을 뿐더러 데이터 지속 삭제로 인해 &lt;u&gt;&lt;a href=&quot;http://www.dbguide.net/db.db?cmd=view&amp;amp;boardUid=148220&amp;amp;boardConfigUid=9&amp;amp;boardIdx=140&amp;amp;boardStep=1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;인덱스 Sparse 현상&lt;/a&gt;&lt;/u&gt;이 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서, 이러한 경우 사용자에게 논리적으로 보여지는 테이블은 하나이지만 기저에 물리적으로는 여러 파티션에 데이터를 나누어 저장한다면, 조회 성능 향상 및 관리가 용이해집니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bThs2E/btqGso0qYNs/t2GCFcqVN7norSseNw74B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bThs2E/btqGso0qYNs/t2GCFcqVN7norSseNw74B1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bThs2E/btqGso0qYNs/t2GCFcqVN7norSseNw74B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbThs2E%2FbtqGso0qYNs%2Ft2GCFcqVN7norSseNw74B1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 가입일 기준으로 20년 1월에 발생한 데이터는 202001 파티션에 저장하도록 하였고, 2월에 가입한 회원들은 202002 파티션에 저장되도록 파티션을 구성하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이와같이 가입일 기준으로 파티션을 구성하면 다음과 같은 이점이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 매월 가입일 기준으로 사용자를 삭제한다면, 기존에는 테이블내 모든 데이터를 탐색해야했지만 지금은 특정 월에 해당하는 파티션만 Drop(DDL 작업) 하면 되므로 작업 부담이 줄어듭니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그리고 특정 월에 해당하는 데이터를 조회할 때, 해당 파티션에 속한 데이터에 대해서만&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;Multi block I/O를 실시할 수 있기 때문에 인덱스를 사용하는 방법보다 빠른 조회가 가능할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정리하자면 파티셔닝은 위 사례와 같이 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;대용량의 논리적 구조&lt;/b&gt;&lt;/span&gt;를 여러 물리적인 파티션으로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;분할&lt;/b&gt;&lt;/span&gt;하여 조회 및 관리 용이성을 위해 사용됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 파티셔닝 종류&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 파티셔닝 종류에 대해서 알아보겠습니다. 파티셔닝은 수직적 파티셔닝과 수평적 파티셔닝 2가지 종류가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;수평적 파티셔닝은 이전 파티셔닝 개념에서 살펴보았듯이, 특정 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;데이터&lt;/b&gt;&lt;/span&gt;(가입일) 기준으로 데이터를 다른 파티션에 저장하는 방법을 말합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pmQq5/btqGteYpRzN/T4G3TjAvygqnlEUNI742V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pmQq5/btqGteYpRzN/T4G3TjAvygqnlEUNI742V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pmQq5/btqGteYpRzN/T4G3TjAvygqnlEUNI742V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpmQq5%2FbtqGteYpRzN%2FT4G3TjAvygqnlEUNI742V1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;반면, 수직적 파티셔닝은 특정 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;컬럼&lt;/b&gt;&lt;/span&gt;을 기준으로 데이터를 분할하는 방법을 말합니다. 위 그림과 같이 기존 회원 테이블을 특정 컬럼을 기준으로 2개의 파티션으로 분할한 경우가 이에 해당됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;수직적 파티셔닝의 이점은 한쪽 세그먼트에서 발생하는 DML이 다른쪽에 영향을 끼치지 않습니다. 반면, 레코드 전체 데이터를 읽어야할 경우에는 데이터가 물리적으로 분산되었으므로 I/O에서 다소 비효율이 발생합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면, NoSQL 제품군에서 주로 사용되는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;샤딩(Sharding)&lt;/b&gt;&lt;/span&gt;은 무엇일까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 402px;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 402px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 402px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E2sWf/btqGuHFAxh7/qa3YJ3KR1KdOVY5RYFTP6k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E2sWf/btqGuHFAxh7/qa3YJ3KR1KdOVY5RYFTP6k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E2sWf/btqGuHFAxh7/qa3YJ3KR1KdOVY5RYFTP6k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE2sWf%2FbtqGuHFAxh7%2Fqa3YJ3KR1KdOVY5RYFTP6k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 402px;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dn78YF/btqGtoNxTNX/HRKR7G103flzvZmMnbj4u1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dn78YF/btqGtoNxTNX/HRKR7G103flzvZmMnbj4u1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dn78YF/btqGtoNxTNX/HRKR7G103flzvZmMnbj4u1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdn78YF%2FbtqGtoNxTNX%2FHRKR7G103flzvZmMnbj4u1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;샤딩이란 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;수평적 파티셔닝&lt;/b&gt;&lt;/span&gt;의 한 종류입니다. 수평적 파티셔닝과 비교하여 다른점은 파티셔닝은 단일 DBMS내에서의 데이터 분할 정책이고, 샤딩은 분할된 여러 데이터베이스 서버로 데이터를 분할하는 방법입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서, 샤딩을 구성하게되면 샤드의 수만큼 노드가 존재하며, 서버가 여러대 존재하므로 부하를 적절히 분산할 수 있는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;지금까지 파티셔닝 종류에 대해서 알아봤습니다. 용량을 고려하여 데이터 크기를 분할할 때, 수직적 파티션보다는 수평적 파티션이 분배에 용이하므로 이후 내용은 수평적 파티셔닝 전략을 기준으로 작성하였음을 참고바랍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 파티셔닝 전략&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bThs2E/btqGso0qYNs/t2GCFcqVN7norSseNw74B1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bThs2E/btqGso0qYNs/t2GCFcqVN7norSseNw74B1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bThs2E/btqGso0qYNs/t2GCFcqVN7norSseNw74B1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbThs2E%2FbtqGso0qYNs%2Ft2GCFcqVN7norSseNw74B1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전에 살펴본 예제는 가입일 컬럼 대상 특정 월을 기준으로 데이터 파티션을 나누었습니다. 이때 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;특정 범위&lt;/b&gt;&lt;/span&gt;를 기준으로 데이터를 분할한 파티션을 Range 파티션이라고 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Range 파티셔닝의 장점은&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;논리적인 범위의 분산에 효율적입니다. 또한, 원하는 데이터가 특정 파티션에 모여있어 관리하기가 용이합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇듯 사용자가 원하는대로 데이터를 분산시킬 수 있는 장점이 있지만 다음과 같은 문제점을 지니고 있습니다. 사례를 통해 Range 파티션의 문제점을 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 대한민국 전체 국민의 개인정보를 관리하는 시스템 테이블에서 사람들의 나이 10살 범위 기준으로 Range 파티셔닝 했다고 가정해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw6ibp/btqGkVeyGdi/DzuAKOZXvSqDrojsJPbtbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw6ibp/btqGkVeyGdi/DzuAKOZXvSqDrojsJPbtbK/img.png&quot; data-alt=&quot;출처 : 국가 통계 포털 20년 7월 기준 대한민국 인구 통계&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw6ibp/btqGkVeyGdi/DzuAKOZXvSqDrojsJPbtbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw6ibp%2FbtqGkVeyGdi%2FDzuAKOZXvSqDrojsJPbtbK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 국가 통계 포털 20년 7월 기준 대한민국 인구 통계&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;차트를 통해 알 수 있듯이 50대 파티션에 가장 많은 데이터가 적재되며 파티션별로 데이터 편차가 크게 나는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 통해 알 수 있는 사실은 Range 파티셔닝의 가장 큰 문제점은 데이터 분포도가 고르지 못할 경우 데이터 배분을 균등하게 할 수 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금까지 RDBMS 테이블 구조로 Range 파티션 구조를 살펴보았습니다. 만약 Redis로 회원 데이터를 샤딩하면 어떤 모습일까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tR1hC/btqGnIy4n5a/Na9RMrXyNxZu4jsgVXsax0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tR1hC/btqGnIy4n5a/Na9RMrXyNxZu4jsgVXsax0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tR1hC/btqGnIy4n5a/Na9RMrXyNxZu4jsgVXsax0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtR1hC%2FbtqGnIy4n5a%2FNa9RMrXyNxZu4jsgVXsax0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;대략 위 그림과 같이 전체 데이터를 특정 범위로 나뉘어 각각의 Master 서버에게 할당할 것입니다. 그리고 범위에 따른 파티션 지정의 책임은 Client에게 있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이처럼 Redis에서의 샤딩 전략은 RDMBS 파티셔닝과 차이가 존재합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp73c7/btqGpxZeRID/Q8OeCks9s0oR1Qanomqptk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp73c7/btqGpxZeRID/Q8OeCks9s0oR1Qanomqptk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp73c7/btqGpxZeRID/Q8OeCks9s0oR1Qanomqptk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp73c7%2FbtqGpxZeRID%2FQ8OeCks9s0oR1Qanomqptk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dT0Hl3/btqGuFAZ0lM/qo4nzNuTW65oOHp4tnP9d1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dT0Hl3/btqGuFAZ0lM/qo4nzNuTW65oOHp4tnP9d1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dT0Hl3/btqGuFAZ0lM/qo4nzNuTW65oOHp4tnP9d1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdT0Hl3%2FbtqGuFAZ0lM%2Fqo4nzNuTW65oOHp4tnP9d1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;RDBMS의 경우에는 테이블 생성시, 파티션 전략을 지정하면, 이후 데이터 입력이나 조회가 필요할 경우에는 테이블을 대상으로 입력/조회 작업을 수행합니다. 그러면 옵티마이저가 &lt;span style=&quot;color: #333333;&quot;&gt;적당한 파티션으로 분배할 것입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 Redis에서 파티셔닝 전략을 사용하려면 데이터 분배 책임은 Client에게 있습니다. 다시말해 이는 Client에서 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;데이터를 어디에 저장할지&lt;/span&gt; &lt;/b&gt;혹은 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;데이터를 어디서 찾아야할지를 정해야함&lt;/span&gt;&lt;/b&gt;을 의미합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Range 파티셔닝의 경우에는 특정 범위와 이에 해당하는 Master 노드를 매핑시키는 Mapping 테이블이나 Client 로직에 범위를 지정하여 분배하는 로직이 들어가야 합니다. 또한 데이터를 균등하게 분배하기 위해서는 철저한 전략 수립이 필요합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;결론적으로 Range 파티셔닝을 구현하는 것은 꽤 번거로운 일입니다. &lt;a href=&quot;https://redis.io/topics/partitioning&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;공식 홈페이지&lt;/u&gt;&lt;/a&gt;에서 해당 내용에 대해서 다루고 있으며, 데이터 균등 분배를 위해 다음에 설명할 해시 파티셔닝 전략을 사용하는 것을 권하고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;해시 파티셔닝&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Redis에서 샤딩을 구현하기 위해서는 데이터를 서버별로 균등하게 분포해야 부하를 고르게 분산할 수 있습니다. 따라서 Range 파티셔닝은 데이터가 고르게 분포되지 못하므로 적절하지 못합니다. 이번에는 해시 파티셔닝을 통해서 데이터를 균등분배 하는 방법에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;해시 파티셔닝은 Redis의 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Key&lt;/b&gt; &lt;/span&gt;값에 대하여 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;해시함수&lt;/b&gt;&lt;/span&gt;를 적용한 결과를 Redis Master의 개수만큼 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;나머지 연산&lt;/b&gt;&lt;/span&gt;을 토대로 데이터를 저장할 Master 서버를 지정하는 방법입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음과 같이 hash 함수를 적용한 다음 Modulo 연산을 통해서 데이터를 저장하거나 찾아야할 Redis 노드를 지정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596982113079&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var hosts = {Master1, Master2, Master3, ... }
var index = hash(key) % hosts.length;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를들어 Master 서버가 3대가 있고, 이를 배열로써 담았다고 가정합시다.&lt;/p&gt;
&lt;p&gt;그러면 master1번은 0번 인덱스, master2는 1번 인덱스, master3은 2번 인덱스가 될 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vbfma/btqGtfiIqX3/xFf4tTSe9T1MKlgbMOCR70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vbfma/btqGtfiIqX3/xFf4tTSe9T1MKlgbMOCR70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vbfma/btqGtfiIqX3/xFf4tTSe9T1MKlgbMOCR70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvbfma%2FbtqGtfiIqX3%2FxFf4tTSe9T1MKlgbMOCR70%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이때, Redis Key에 대하여 hash 함수를 적용한 결과가 1934라면 1934 % 3(서버 개수) 의 결과인 2에 해당하는 Master 3노드가 해당 Key의 저장소가 되며, 데이터를 조회할 때도 Client는 해당 저장소에서 데이터를 찾으려고 할 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;해시파티셔닝의 경우에는 Modulo 연산을 통해 데이터의 분포여부와 상관없이 고르게 분포시킬 수 있는 장점이 있어 주로 사용되는 전략입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rebalancing 문제&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금까지 Redis의 샤딩, Range 파티셔닝의 문제점 및 해시 파티셔닝을 통한 데이터 균등 분배 방법에 대해서 살펴봤습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 해시 파티셔닝이 적용된 상황에서 데이터 용량이 더욱 커져 Master 서버 추가를 해야한다면, 어떤 이슈가 존재할까요? 사례를 통해서 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ozyrq/btqGqQKLKGs/rTpp7dSTYF39wORCQoZvRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ozyrq/btqGqQKLKGs/rTpp7dSTYF39wORCQoZvRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ozyrq/btqGqQKLKGs/rTpp7dSTYF39wORCQoZvRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fozyrq%2FbtqGqQKLKGs%2FrTpp7dSTYF39wORCQoZvRk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림과 같이 현재 총 12개의 데이터가 3개의 Master 서버에 고르게 분배되어있다고 가정해봅시다. 위와 같은 상황에서 Hash 함수 결과가 9를 저장하고 있는 Master는 9 % 3(서버 대수)에 의하여 Master 1번이 선택될 것이고 Client는 해당 연산을 통해서 Master1번에게 질의할 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이러한 상황에서 Master4를 새롭게 추가한다고 가정해봅시다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnvAIi/btqGtnA7qVF/oif6BXdqcnuVHJtKlHoGKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnvAIi/btqGtnA7qVF/oif6BXdqcnuVHJtKlHoGKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnvAIi/btqGtnA7qVF/oif6BXdqcnuVHJtKlHoGKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnvAIi%2FbtqGtnA7qVF%2Foif6BXdqcnuVHJtKlHoGKK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같은 상황에서 기존과 같이 Hash 함수 9의 결과를 가지고 있는 Redis 서버를 찾고자하면 어떤일이 발생할까요?&lt;/p&gt;
&lt;p&gt;서버의 개수가 증가하였으므로 9 % 4 = 1이되어 엉뚱한 서버에 질의 하는 결과를 낳게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서, 서버를 추가할 경우에는 그에 맞게 데이터의 재분배(Rebalancing) 작업이 필요합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그럼 데이터 재분배를 진행한 결과를 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZVmKj/btqGtCkuD5a/alAK0q1kM9HLfPyRXHkjZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZVmKj/btqGtCkuD5a/alAK0q1kM9HLfPyRXHkjZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZVmKj/btqGtCkuD5a/alAK0q1kM9HLfPyRXHkjZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZVmKj%2FbtqGtCkuD5a%2FalAK0q1kM9HLfPyRXHkjZK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림에서 초록색으로 표기된 데이터는 원래 노드에서 새로운 노드로 재분배된 데이터를 의미합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 통해서 알 수 있는 사실은 Master 노드 추가 이후 75%의 데이터가 재분배 작업을 통해 다른 노드로 이전하였음을 확인할 수 있습니다. 이는 데이터를 재분배하는 과정에서 굉장히 많은 부하가 발생할 수 있으며, 운영중에 노드 추가 작업이 자유롭지 못함을 의미합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 단순 Modulo를 적용한 해시파티셔닝 전략으로는 신규 노드 추가/삭제 작업으로부터 자유롭지 못합니다.&lt;/p&gt;
&lt;p&gt;그렇다면 어떻게 하면 데이터 재분배 작업을 최소화할 수 있을까요?&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Consistent Hashing&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전에 살펴보았듯이, 노드 추가에 따른 Rebalancing 부하를 줄이기 위해서는 재분배되는 데이터 양이 적어야 합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Consistent Hashing 기법은 데이터와 더불어 Master 서버에 대하여 동일한 해시 함수를 적용하고, Master 서버 해시값 구간에 해당되는 데이터를 저장하는 방법입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 들어 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;560&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E4VW6/btqGtXCcWUa/rLjVsKuOrfgS1kyHMnFzeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E4VW6/btqGtXCcWUa/rLjVsKuOrfgS1kyHMnFzeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E4VW6/btqGtXCcWUa/rLjVsKuOrfgS1kyHMnFzeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE4VW6%2FbtqGtXCcWUa%2FrLjVsKuOrfgS1kyHMnFzeK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;560&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림과 같이 Redis Master 노드에 대하여 해시함수를 적용합니다.&lt;/p&gt;
&lt;p&gt;그리고 해시함수 결과를 기준으로 데이터를 처리할 Master 노드를 결정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 100px;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px; text-align: center;&quot;&gt;노드&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px; text-align: center;&quot;&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;Master1&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;해시값 &amp;lt;= 10724 OR 해시값 &amp;gt; 12345&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;Master2&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;10725 &amp;lt; &lt;span style=&quot;color: #333333;&quot;&gt;해시값 &amp;lt;= &lt;/span&gt;11224&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;Master3&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;11224 &amp;lt; 해시값 &amp;lt;= 11965&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;Master4&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px;&quot;&gt;11965 &amp;lt; 해시값 &amp;lt;= 12345&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Case 1. 해시값 10756 데이터 입력시&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;560&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CtzXT/btqGsplF0Y0/ukLVH63QrB4Xfqh3n8w0k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CtzXT/btqGsplF0Y0/ukLVH63QrB4Xfqh3n8w0k0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CtzXT/btqGsplF0Y0/ukLVH63QrB4Xfqh3n8w0k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCtzXT%2FbtqGsplF0Y0%2FukLVH63QrB4Xfqh3n8w0k0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;560&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Redis Master 서버에 대하여 해시 함수가 적용된 상황에서 해시 값이 10756인 데이터가 입력되었다고 가정해봅시다.&lt;/p&gt;
&lt;p&gt;이는 10724보다는 크고 11224보다는 작으므로 Master2 노드가 해당 데이터의 저장소로 선정됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p&gt;&lt;b&gt;Case 2. 12086 해시값 데이터 입력시&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;580&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkj526/btqGtejQFK2/GjQpjdSwrlRrPK3ZsY45Uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkj526/btqGtejQFK2/GjQpjdSwrlRrPK3ZsY45Uk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkj526/btqGtejQFK2/GjQpjdSwrlRrPK3ZsY45Uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdkj526%2FbtqGtejQFK2%2FGjQpjdSwrlRrPK3ZsY45Uk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;580&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;12086 해시값 데이터가 입력되면, 12345 보다 작고 11965보다 크므로 Master4 노드가 해당 데이터의 저장소가 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p&gt;&lt;b&gt;Case 3. 13567 해시값 데이터 입력시&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;580&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbhCLt/btqGyxo42Wr/hrK2kvokxcHzTuecHSszj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbhCLt/btqGyxo42Wr/hrK2kvokxcHzTuecHSszj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbhCLt/btqGyxo42Wr/hrK2kvokxcHzTuecHSszj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbhCLt%2FbtqGyxo42Wr%2FhrK2kvokxcHzTuecHSszj1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;580&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;12345보다 큰 값이 입력되었으므로, 해당 데이터의 저장소는 Master 1번이 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p&gt;&lt;b&gt;Case 4. 5576 해시값 데이터 입력시&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;580&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WeCp7/btqGxOq4QmF/4A9iJLxn47YfYb1h6PUPp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WeCp7/btqGxOq4QmF/4A9iJLxn47YfYb1h6PUPp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WeCp7/btqGxOq4QmF/4A9iJLxn47YfYb1h6PUPp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWeCp7%2FbtqGxOq4QmF%2F4A9iJLxn47YfYb1h6PUPp0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;580&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;10724보다 작은 데이터가 입력되었으므로 해당 데이터의 저장소는 Master 1번이 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이렇듯 Consistent Hashing 기법에서는 서버를 추가하면 해시 함수를 적용하여 Hash Ring 형태로 만듭니다. 이후 입력되는 데이터는 해시 값 결과에 따라 저장소가 결정됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 위와 같은 상황에서 노드가 삭제되거나 추가될 때 효율적인 Rebalancing이 일어날까요? Master 1 노드를 제거하는 상황을 가정해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;580&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0D7Bd/btqGvPEaWFj/nzKu7HOPW1v16IX2AWQIuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0D7Bd/btqGvPEaWFj/nzKu7HOPW1v16IX2AWQIuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0D7Bd/btqGvPEaWFj/nzKu7HOPW1v16IX2AWQIuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0D7Bd%2FbtqGvPEaWFj%2FnzKu7HOPW1v16IX2AWQIuk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;580&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Master1 노드 삭제로 인해 Master1 노드가 가지고 있던 데이터를 재분배 해야하는 상황입니다. 따라서, 지금 상황에서는 Master2 노드가 Master1 노드의 데이터를 전부 이관 받아야하는 상황이며, 이는 효율적으로 데이터 재분배가 일어났다고 보기 힘듭니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면, 어떻게 해야 데이터를 효율적으로 나눌 수 있을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;가상 노드 추가&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Hash Ring에 단일 노드만 배치하니까 발생한 문제는, 노드를 제거했을 때, 인접해있는 다른 노드 하나에 모든 데이터를 이관해야하는 문제점이 있습니다. 따라서 이를 해소하는 방법은 Hash Ring에 여러 가상 노드를 배치하는 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qGgzB/btqGvequRbj/xlTTT2asDJgI38x7MKGXAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qGgzB/btqGvequRbj/xlTTT2asDJgI38x7MKGXAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qGgzB/btqGvequRbj/xlTTT2asDJgI38x7MKGXAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqGgzB%2FbtqGvequRbj%2FxlTTT2asDJgI38x7MKGXAK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같이 하나의 노드가 아니라 여러개의 가상 노드를 Hash Ring에 배치하면, 노드와 데이터사이 해시 값 범위가 좁아집니다. 따라서 이런 상황에서 Master1 노드를 제거하면, 해당 노드의 데이터가 다른 노드로 적절히 분배될 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JJ6PG/btqGxNTdrZp/dctSaS9axHgXbDqb6AytUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JJ6PG/btqGxNTdrZp/dctSaS9axHgXbDqb6AytUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JJ6PG/btqGxNTdrZp/dctSaS9axHgXbDqb6AytUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJJ6PG%2FbtqGxNTdrZp%2FdctSaS9axHgXbDqb6AytUK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 사례는 Master1번 노드가 삭제된 이후 Master1번 노드가 가지고 있었던 데이터(빨간색 음영)가 다른 노드로 적절히 분산되었음을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약, 노드를 추가할 경우에는 특정 노드가 담당하고 있던 적은 범위의 데이터 영역을 재분배합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정리하자면, 해시 파티셔닝을 사용함으로 인하여 데이터를 균등하게 분포하면서, 데이터 재분배에 대한 영향도를 최소화 하기 위해서 Consistent Hashing 알고리즘을 이용할 수 있습니다. Consistent Hashing 알고리즘을 사용시 고려 사항은 Hash Ring에 가상 노드를 촘촘하게 그리고 노드가 간격을 균일하게 배치할 수 있도록 Hash 알고리즘이 적용되야 합니다. 특정 노드간의 범위가 벌어지게된다면, 그만큼 재분배해야할 데이터 양이 많아지므로 이를 유의해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고&lt;/p&gt;
&lt;p&gt;- &lt;a href=&quot;https://charsyam.wordpress.com/2016/10/02/%EC%9E%85-%EA%B0%9C%EB%B0%9C-consistent-hashing-%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B8%B0%EC%B4%88/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;강대명님 블로그&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;- &lt;a href=&quot;https://www.joinc.co.kr/w/man/12/hash/consistent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Play Joinc님 블로그&lt;/u&gt;&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Redis 샤딩에 대해서 알아봤습니다. 처음 작성할 때 계획은 Redis Cluster까지 다루려고 했지만, 내용이 길어 여기서 마무리하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/Redis</category>
      <category>Redis</category>
      <category>Redis 구조</category>
      <category>Redis 샤딩</category>
      <category>Spring Data Reds</category>
      <category>샤딩</category>
      <category>파티셔닝</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/102</guid>
      <comments>https://cla9.tistory.com/102#entry102comment</comments>
      <pubDate>Tue, 11 Aug 2020 22:03:46 +0900</pubDate>
    </item>
    <item>
      <title>1. Redis 구조</title>
      <link>https://cla9.tistory.com/101</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금부터 다룰 Redis 시리즈는 개인 공부 내용 정리 목적으로 작성하였습니다. 주요 포스팅 내용은 redis-cli 명령어 학습과 Spring Data Redis를 활용해서 해당 명령어 적용방법에 대해서 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅은 Redis를 다루는 첫 포스팅으로 Redis 구조에 대해서 개략적으로 살펴보겠습니다.&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. Redis 접속&amp;nbsp;&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7AVg2/btqGriFLEsk/kSILadumTygQUaGSaCkX20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7AVg2/btqGriFLEsk/kSILadumTygQUaGSaCkX20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7AVg2/btqGriFLEsk/kSILadumTygQUaGSaCkX20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7AVg2%2FbtqGriFLEsk%2FkSILadumTygQUaGSaCkX20%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;DBMS을 사용하기 위해서는 명령어 처리를 위한 별도 프로그램이 필요합니다. 이를 위해 각 DBMS 벤더사에서 DB와 통신을 위한 CLI(Command Line Interface) 프로그램을 제공합니다. 가령 oracle 사용한다면, oracle에서 기본적으로 제공하는 sql * plus 프로그램을 사용해서 DB에 접속할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tglQO/btqGqb1rHiC/5m3jNVKH5b9m9MU1hyXDg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tglQO/btqGqb1rHiC/5m3jNVKH5b9m9MU1hyXDg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tglQO/btqGqb1rHiC/5m3jNVKH5b9m9MU1hyXDg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtglQO%2FbtqGqb1rHiC%2F5m3jNVKH5b9m9MU1hyXDg1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마찬가지로 Redis를 사용하기 위해서는 Redis에 접속할 수 있는 프로그램이 필요합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7eeYK/btqGnZOi6hq/7j8ktHSPQo9CeE4IEbHQ4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7eeYK/btqGnZOi6hq/7j8ktHSPQo9CeE4IEbHQ4K/img.png&quot; data-alt=&quot;redis 접속화면&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7eeYK/btqGnZOi6hq/7j8ktHSPQo9CeE4IEbHQ4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7eeYK%2FbtqGnZOi6hq%2F7j8ktHSPQo9CeE4IEbHQ4K%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;redis 접속화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Redis에서는 이를 위해 redis-cli를 제공하며, Redis가 설치된 환경에서 사용자가 Redis를 사용하기 위해 가장 처음 접하게 되는 프로그램이 redis-cli입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oOOH9/btqGnHGBQr7/WC6xgGRu74riTHV3YykKs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oOOH9/btqGnHGBQr7/WC6xgGRu74riTHV3YykKs1/img.png&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://github.com/redis/redis&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oOOH9/btqGnHGBQr7/WC6xgGRu74riTHV3YykKs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoOOH9%2FbtqGnHGBQr7%2FWC6xgGRu74riTHV3YykKs1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://github.com/redis/redis&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1596881669866&quot; class=&quot;c++ arduino&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;c++&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
    int refcount;
    void *ptr;
} robj;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Redis에 접속하게되면, &lt;span style=&quot;color: #333333;&quot;&gt;redis-server&lt;/span&gt;는 Client 구조체로 저장하여 &lt;span&gt;linked list 형식으로 관리합니다. &lt;/span&gt;&lt;span&gt;여기서 주목할 점은 Client 명령을 처리하기 위해 필요한 인자는 argc와 argv 멤버 변수를 통해 전달되며, 해당 구조는 redisObject 구조체로 정의되어있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;해당 구조체에서 type 멤버 변수는 Redis에서 지원하는 데이터 타입을 의미합니다. 지원하는 타입 종류로는 &lt;b&gt;String, List, Set, Sorted Set, Hash, Bitmap, HyperLogLogs&lt;/b&gt; 등이 있습니다. 각 데이터 타입에 대한 소개는 차후 포스팅을 통해 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그외 redisObject 구조체 각 멤버 변수에 대한 설명은 &lt;a href=&quot;http://redisgate.kr/redis/configuration/internal_string.php&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;redisgate 홈페이지&lt;/u&gt;&lt;/a&gt;에 자세히 소개되어있으니 참고 바랍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Redis 구조&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTgUUV/btqGmPrpvmT/abSYnkhm4bBCDGHoXze4dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTgUUV/btqGmPrpvmT/abSYnkhm4bBCDGHoXze4dK/img.png&quot; data-alt=&quot;출처 : 빅데이터 저장 및 분석을 위한 NoSQL &amp;amp;amp;amp; Redis 책 5.1절 Redis 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTgUUV/btqGmPrpvmT/abSYnkhm4bBCDGHoXze4dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTgUUV%2FbtqGmPrpvmT%2FabSYnkhm4bBCDGHoXze4dK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 빅데이터 저장 및 분석을 위한 NoSQL &amp;amp; Redis 책 5.1절 Redis 아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;인스턴스에 정상적으로 접속했다면, 명령어 전달을 통해 데이터를 저장하거나 조작할 수 있습니다. Redis는 In Memory 데이터 구조 저장소로써 위 그림에서 해당되는 데이터 구조는 모두 메모리에 상주합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 구조에서 Resident Area는 명령어를 통해 실제 데이터가 저장 및 작업이 수행되는 공간입니다. 초록색 영역은 내부적으로 서버 상태를 저장하고 관리하기 위한 메모리 공간으로 사용되며, Data Structure 영역으로 불립니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Data Structure 영역에 대한 설명은 차후 데이터 타입 포스팅을 진행할 때 다루어보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkwHpZ/btqGnH0ZFib/0g7cKIWv6Kti8fQCQ701Q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkwHpZ/btqGnH0ZFib/0g7cKIWv6Kti8fQCQ701Q1/img.png&quot; data-alt=&quot;출처 : 빅데이터 저장 및 분석을 위한 NoSQL &amp;amp;amp;amp; Redis 책 5.1절 Redis 아키텍처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkwHpZ/btqGnH0ZFib/0g7cKIWv6Kti8fQCQ701Q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkwHpZ%2FbtqGnH0ZFib%2F0g7cKIWv6Kti8fQCQ701Q1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : 빅데이터 저장 및 분석을 위한 NoSQL &amp;amp; Redis 책 5.1절 Redis 아키텍처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;앞서 Redis는 In Memory 데이터 구조 저장소라고 설명했습니다. 하지만 Memory는 휘발성이기 때문에, 프로세스를 종료하게되면 데이터는 모두 유실됩니다. 따라서 단순 캐시용도가 아닌 Persistence 저장소로 활용을 위해서는 Disk에 저장하여 데이터 유실이 발생되지 않도록 해야합니다. 이를 위해 &lt;b&gt;AOF&lt;/b&gt;(Append Only File)기능과 &lt;b&gt;RDB&lt;/b&gt;(Snapshot) 기능이 존재합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;240&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LiWcY/btqGqQbIjVm/oNay27RP2ftdibwHedmOwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LiWcY/btqGqQbIjVm/oNay27RP2ftdibwHedmOwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LiWcY/btqGqQbIjVm/oNay27RP2ftdibwHedmOwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLiWcY%2FbtqGqQbIjVm%2FoNay27RP2ftdibwHedmOwK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;240&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AOF는 전달된 명령을 별도에 파일에 기록하는 방법으로 RDBMS의 Redo 메커니즘과 유사합니다. AOF의 역할은 재기동시 파일에 기록된 명령어를 일괄 수행하여 데이터를 복구하는데 사용됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9KnVt/btqGmQqfOm5/lp2yfLF22KAjm7WeqfThL0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9KnVt/btqGmQqfOm5/lp2yfLF22KAjm7WeqfThL0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9KnVt/btqGmQqfOm5/lp2yfLF22KAjm7WeqfThL0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9KnVt%2FbtqGmQqfOm5%2Flp2yfLF22KAjm7WeqfThL0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;480&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AOF의 장점으로는 데이터 유실이 발생하지 않습니다. 하지만 매 명령어마다 File과의 동기화가 필요하기 때문에 처리속도가 현격히 줄어듭니다. 따라서 이를 해소하기위해 File Sync 옵션(&lt;span style=&quot;color: #000000;&quot;&gt;appendfsync&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;이 존재하며, 해당 옵션에 따라 Sync 주기를 조절할 수 있으나 그만큼 데이터 유실이 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;반면 RDB는 특정 시점의 메모리 내용을 복사하여 파일에 기록하는 방법으로 RDBMS Full Backup에 해당합니다. 따라서, 정기적 혹은 비정기적으로 저장이 필요할 시점에 데이터를 저장이 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;RDB의 장점으로는 AOF에 비해 부하가 적으며, LZF 압축을 통해 파일 압축이 가능합니다. 또한 덤프파일을 그대로 메모리에 복원(Restore)하므로 AOF에 비해 빠릅니다. &lt;span style=&quot;color: #333333;&quot;&gt;반면 덤프를 기록한 시점이후 데이터는 저장되지 않으므로 복구시에 데이터 유실이 발생할 수 있는 문제점이 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7gr7p/btqGqozW0Ct/ldKG2LGkRcYJjO8HK4bQfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7gr7p/btqGqozW0Ct/ldKG2LGkRcYJjO8HK4bQfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7gr7p/btqGqozW0Ct/ldKG2LGkRcYJjO8HK4bQfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7gr7p%2FbtqGqozW0Ct%2FldKG2LGkRcYJjO8HK4bQfk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AOF와 RDB를 사용할 때는 유의해야할 점이 있습니다. 바로 Copy On Write입니다. AOF를 백그라운드로 수행하거나 RDB를 수행할때 &lt;span style=&quot;color: #333333;&quot;&gt;redis-server에서 자식 프로세스를 fork하여 처리를 위임합니다.&amp;nbsp;&lt;/span&gt;만약 이과정에서 redis-server의 데이터에 쓰기 작업을 수행한다면, 기존 페이지를 수정하는 것이 아닌 이를 별도 공간에 저장 후 처리합니다. 따라서 해당 작업을 수행도중에 쓰기 작업이 증가한다면, 메모리 사용량이 급격히 증가될 수 있습니다.(최대 2배) 따라서 이를 유의해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Redis 복제(Replication)&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 Redis 복제에 대해서 알아보겠습니다. 먼저 복제가 필요한 이유에 대해서 먼저 알아봅시다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;480&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qxd4a/btqGkj7ISxZ/0Z5XIkckrsKvMmZbcz0sGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qxd4a/btqGkj7ISxZ/0Z5XIkckrsKvMmZbcz0sGk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qxd4a/btqGkj7ISxZ/0Z5XIkckrsKvMmZbcz0sGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqxd4a%2FbtqGkj7ISxZ%2F0Z5XIkckrsKvMmZbcz0sGk%2Fimg.png&quot; width=&quot;480&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Redis는 충분히 빠르고 안정적입니다. 하지만 서비스가 갑자기 잘되어 트래픽이 몰린다면 어떻게 될까요?&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;서버의 한계점을 넘어간다면, 인스턴스 장애가 발생할 수 있습니다. 이때 만약 단일 인스턴스로만 구성되었다면, Redis의 장애가 모든 Application에 영향을 미칩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;한편, Redis를 운영하는 입장에서 버전 업그레이드 혹은 서버 PM 작업이 필요하나 Application 영향도로 인해 섯불리 작업할 수 없는 문제가 생깁니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;마지막으로, 캐시 목적으로 사용하는 Redis는 쓰기 작업보다는 읽기 작업이 주로 발생합니다. 따라서 읽기 작업 성능을 높힐 수 있는 아키텍처 구성이 필요할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 위해 Redis에서는 어느정도 고가용성을 확보 및 쓰기/읽기 작업 성능을 개선할 수 있는 Master/Replica 토폴로지를 제공합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Master/Replica 구조&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boO3q9/btqGmPd1qlV/bb7kjvNpqbuOnvC61c9A4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boO3q9/btqGmPd1qlV/bb7kjvNpqbuOnvC61c9A4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boO3q9/btqGmPd1qlV/bb7kjvNpqbuOnvC61c9A4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboO3q9%2FbtqGmPd1qlV%2Fbb7kjvNpqbuOnvC61c9A4k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 Master/Replica의 구조를 나타냅니다.&lt;/p&gt;
&lt;p&gt;최초 Master/Replica 구성시 Master의 데이터는 모든 Replica에 복사합니다. 따라서 어느 Redis 인스턴스에서 데이터를 조회해도 원하는 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 데이터의 변경이 발생한다면, 변경 작업은 Master에서만 가능합니다. 이후 변경된 데이터는 비동기적으로 모든 Replica에게 전달되어 반영됩니다.&lt;/p&gt;
&lt;p&gt;(※&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;replica-read-only 옵션을&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;no로 설정하면, Replica 상태를 변경할 수 있으나 전체 동기화가 발생하면 모두 유실되므로 추천하지 않습니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이러한 과정은 Oracle Data Guard의 SQL Apply 서비스와 유사합니다. 데이터가 아닌 문장(Statement)이 전달되므로 LuaScript가 적용된 문장이라면 Master와 Replica의 결과가 다를 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Master/Replica 동기화 과정&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 Master/Replica 동기화 과정에 대해서 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;Replica에는 Master 노드에 적재된 데이터가 하나도 존재하지 않으므로 최초 구성시에는 전체 동기화가 발생합니다.&lt;/p&gt;
&lt;p&gt;전체 동기화 과정은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL3rPn/btqGspdRe6h/CBLgwycngbpUYnC8ztKIZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL3rPn/btqGspdRe6h/CBLgwycngbpUYnC8ztKIZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL3rPn/btqGspdRe6h/CBLgwycngbpUYnC8ztKIZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL3rPn%2FbtqGspdRe6h%2FCBLgwycngbpUYnC8ztKIZK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Master와 Replica 인스턴스를 별도로 구성합니다.&lt;/p&gt;
&lt;p&gt;2. Replica 인스턴스에서 &lt;a href=&quot;https://redis.io/commands/replicaof&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;&lt;b&gt;ReplicaOf&lt;/b&gt; &lt;/u&gt;&lt;/a&gt;명령어를 통해 Master 인스턴스와의 동기화 명령을 수행합니다.&lt;/p&gt;
&lt;p&gt;3. Master에서는 fork를 통해 자식 프로세스를 생성합니다.&lt;/p&gt;
&lt;p&gt;4. 자식 프로세스에서는 Master 메모리에 있는 모든 데이터를 Disk로 dump 합니다.&lt;/p&gt;
&lt;p&gt;5. dump가 완료되면, 이를 Replica에 전달하여 반영합니다.&lt;/p&gt;
&lt;p&gt;6. Master에서는 복제가 진행되는 동안 변경 데이터를 Replication Buffer에 저장합니다.&lt;/p&gt;
&lt;p&gt;7. Dump 전송이 완료되면, Replication Buffer의 내용을 Replica에게 전달하여 데이터를 최신상태로 만듭니다.&lt;/p&gt;
&lt;p&gt;8. 작업이 완료되면, 이후에는 데이터 변경발생분만 &lt;b&gt;비동기&lt;/b&gt;방식으로 전달됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;최초 구성시에는 전체 동기화(Full Syncronization)이 발생하고, 이때 fork가 발생하므로 메모리 사용량이 증가할 수 있습니다. 따라서 이를 유의해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 Master/Replica 구성이 완료된 이후에 네트워크 지연이 발생되면 동기화는 어떻게 처리가 될까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q3OeG/btqGrAs5bxw/EfZvJdzPo1DLo53qJ4rYf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q3OeG/btqGrAs5bxw/EfZvJdzPo1DLo53qJ4rYf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q3OeG/btqGrAs5bxw/EfZvJdzPo1DLo53qJ4rYf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ3OeG%2FbtqGrAs5bxw%2FEfZvJdzPo1DLo53qJ4rYf1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ReplicaOf 명령어를 통해 Master/Replica 구조가되면, Master 인스턴스에서는 내부적으로 &lt;span style=&quot;color: #000000;&quot;&gt;repl-backlog-size 옵션 만큼의 Backlog Buffer가 만들어집니다. 이후 Replica와의 단절이 발생하게 되면, Master 인스턴스에서는 변경 데이터를 Backlog Buffer에 저장합니다. Backlog Buffer는 유한한 크기를 지녔으므로, 지연이 오랫동안 발생한다면 Buffer가 넘칠 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v4tFz/btqGsoF01wB/hckkUGyFKLTkDcYKIbuWZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v4tFz/btqGsoF01wB/hckkUGyFKLTkDcYKIbuWZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v4tFz/btqGsoF01wB/hckkUGyFKLTkDcYKIbuWZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv4tFz%2FbtqGsoF01wB%2FhckkUGyFKLTkDcYKIbuWZK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;단절 이후 다시 재연결 되었을 때 과정은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. Replica에서 Master와 동기화를 위해 부분 동기화를 시도합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. 만약 Backlog Buffer에 네트워크 단절 이후의 데이터가 모두 존재하면, Buffer에있는 데이터를 전달받아 최신 상태를 만듭니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 만약 오랜 시간 네트워크 단절로 인해 Backlog Buffer에 데이터가 유실되었을 경우에는 전체 동기화 과정을 진행합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ADB, RDB를 설명할때도 살펴봤지만, 프로세스 fork가 일어나게되면 메모리 사용율이 급격하게 증가할 수 있으므로 전체 동기화 작업 혹은 Replica 추가 작업시에는 모니터링과 메모리 조정등이 필요합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고 :&lt;/p&gt;
&lt;p&gt;&amp;nbsp;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://redis.io/topics/replication&quot;&gt;&lt;u&gt;Redis 공식 문서&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://youtu.be/mPB2CZiAkKM?t=3078&quot;&gt;&lt;u&gt;우아한 테크 세미나 by 강대명님&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- &lt;a href=&quot;https://redislabs.com/blog/top-redis-headaches-for-devops-replication-buffer/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Redis Lab&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Master/Replica 장단점&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn6Bom/btqGkkyPS80/49RwlHbJxQWxodY4HQ2ZhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn6Bom/btqGkkyPS80/49RwlHbJxQWxodY4HQ2ZhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn6Bom/btqGkkyPS80/49RwlHbJxQWxodY4HQ2ZhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn6Bom%2FbtqGkkyPS80%2F49RwlHbJxQWxodY4HQ2ZhK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금까지 Master/Replica 구조에 대해서 살펴보았습니다. 해당 구조의 장점은 무엇이 있을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;우선 Master 인스턴스와 Replica 인스턴스간 데이터가 공유되어있습니다. 따라서 Replica 중 어느 인스턴스가 다운되더라도 Application의 영향을 최소화할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또한, 데이터 조회를 위해 굳이 Master에게 요청하지 않아도 되므로 Read 작업에 대한 부하를 여러 인스턴스로 분산시킬 수 있는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 Master/Replica 구성했을 경우 발생되는 문제점에 대해서 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oxaSp/btqGqQbUzlH/9w3bXrrkMrGKL9YIOTaOk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oxaSp/btqGqQbUzlH/9w3bXrrkMrGKL9YIOTaOk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oxaSp/btqGqQbUzlH/9w3bXrrkMrGKL9YIOTaOk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoxaSp%2FbtqGqQbUzlH%2F9w3bXrrkMrGKL9YIOTaOk1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Master 인스턴스에 장애가 발생하여도 다른 Replica에 데이터가 모두 복제되어있으므로 읽기 연산은 문제가 없습니다. 하지만 쓰기 작업은 Master를 통해 이루어지므로 더이상 쓰기 작업 수행될 수 없는 문제가 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btvJtu/btqGre4RwLO/YrBrpMXvvXOXxXN1sDXuhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btvJtu/btqGre4RwLO/YrBrpMXvvXOXxXN1sDXuhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btvJtu/btqGre4RwLO/YrBrpMXvvXOXxXN1sDXuhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtvJtu%2FbtqGre4RwLO%2FYrBrpMXvvXOXxXN1sDXuhk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 단순 Master/Replica 구성을 했을 경우에는 관리자가 모니터링을 통해 장애 여부를 감지하고, 수동으로 Replica 인스턴스 중 하나를 Master로 선정하고, 나머지 Replica에서 새로 변경한 Master를 바라보도록 설정을 변경해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;즉 다시말해 Master 인스턴스 Crash 발생 시, 자동으로 Failover 해주지 않습니다. Master/Replica를 구성하는 이유 중 하나는 고가용성을 달성하기 위함인데, Failover를 자동으로 해주지 않는 것은 운영자 입장에서는 많이 불편할 수 밖에 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 이러한 이슈를 해결하기 위해 Redis Sentinel 기능을 제공하였습니다. Sentinel은 별도의 프로세스로 Master 인스턴스 다운시, &lt;span style=&quot;color: #333333;&quot;&gt;이를 감지하여 &lt;/span&gt;Replica 중 하나를 Master 인스턴스로 Failover 및 이를 Application에게 통지하는 기능을 포함하고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고:&lt;/p&gt;
&lt;p&gt;&amp;nbsp;- &lt;a href=&quot;https://redis.io/topics/sentinel&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Redis 공식 홈페이지 Sentinel 소개 자료&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Master/Replica/Sentinel 구조&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c8W1KS/btqGsM7TQCD/RKE07Ukvdki1RMR7mHXmxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c8W1KS/btqGsM7TQCD/RKE07Ukvdki1RMR7mHXmxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c8W1KS/btqGsM7TQCD/RKE07Ukvdki1RMR7mHXmxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc8W1KS%2FbtqGsM7TQCD%2FRKE07Ukvdki1RMR7mHXmxk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 Master/Replica/Sentinel의 구조입니다. 여기서 녹색으로 연결된 선이 Sentinel과 연결된 네트워크 Path를 의미합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Sentinel은 다른 Sentinel을 포함한 모든 Redis 인스턴스와 연결합니다. 이후 1초마다 HeartBeat 통신을 통해 Master 및 Replica 서버가 정상적으로 작동중인지 여부 확인하고 이상 발생시 자동으로 Failover 및 Application에 알림을 전송합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면, Sentinel을 통해 Failover는 어떤 방식으로 이루어질까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ukgS9/btqGpAHQ3gG/6XpLgQZ57p2Id5rS5x0ktk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ukgS9/btqGpAHQ3gG/6XpLgQZ57p2Id5rS5x0ktk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ukgS9/btqGpAHQ3gG/6XpLgQZ57p2Id5rS5x0ktk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FukgS9%2FbtqGpAHQ3gG%2F6XpLgQZ57p2Id5rS5x0ktk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Sentinel과 연결된 노드 중 HeartBeat에 일정 시간동안 응답하지 않는 경우 해당 Sentinel은 장애가 발생한 것으로 간주하고 해당 노드를 주관적 다운(Subjectively Down)으로 인지합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;주관적 다운으로 별도 지정한 이유는 해당 Sentinel과의 일시적인 네트워크 연결이 끊긴 것일 수도 있기 때문에 정확한 장애 여부는 아직 확정할 수 없기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blmR8B/btqGmQRyTwe/enR3tVT1c0a6ZFa0wrdk00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blmR8B/btqGmQRyTwe/enR3tVT1c0a6ZFa0wrdk00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blmR8B/btqGmQRyTwe/enR3tVT1c0a6ZFa0wrdk00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblmR8B%2FbtqGmQRyTwe%2FenR3tVT1c0a6ZFa0wrdk00%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 장애가 발생한 인스턴스가 Master일 경우에는 모든 Sentinel에게 Master Down 여부를 묻습니다. Master Down 여부를 전달받은 Sentinel 들은 실제 Master 인스턴스가 죽었는지를 확인 후 이를 응답합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이때 Master 인스턴스에 장애가 발생했다고 응답하는 비율이 정족&lt;span style=&quot;color: #000000;&quot;&gt;수(Quorum)를 넘게 되면 이를 객관적 다운(Objectively Down)이라고 인지하게됩니다. 위 구조에서는 2개의 Sentinel 인스턴스가 Master 장애&lt;/span&gt;를 인지하게되면, 정족수를 넘게되는 것이므로 객관적 다운으로 인지하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;객관적 다운이 발생하게 되면, 다른 Sentinel 인스턴스와 통신하여 장애 조치 작업을 시작합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Master 장애가 발생하게 되면 더이상 쓰기 작업이 안되므로 가장 먼저 해야할 작업은 새로운 Master를 선출하는 일입니다. 이를 위해서 Sentinel 프로세스는 새로운 Master 선출 권한이 있는 Sentinel 리더를 뽑는 작업을 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ewQ5Vr/btqGkV6GY1D/dnO96SnCW4kS66lbKKRiik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ewQ5Vr/btqGkV6GY1D/dnO96SnCW4kS66lbKKRiik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ewQ5Vr/btqGkV6GY1D/dnO96SnCW4kS66lbKKRiik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FewQ5Vr%2FbtqGkV6GY1D%2FdnO96SnCW4kS66lbKKRiik%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Sentinel 리더가 선출되면, 리더는 Replica 인스턴스 중 하나를 Master로 승격합니다. 이후 나머지 Replica에서 Master 인스턴스를 모니터링할 수 있도록 명령을 수행하고 장애 복구 작업을 종료합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 기존 Master 인스턴스가 다시 살아난다면, 이미 Master가 바뀌었으므로 기존 Master는 Replica로 자동으로 변경됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이로써, 운영자의 개입없이 자동으로 Failover 하여 고가용성을 어느정도 확보할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;여기서 어느정도라는 말을 쓰는 이유는 Master/Replica/Sentinel 구조에서도 데이터 유실이 발생할 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 위해 다음과 같은 상황을 가정해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGqWA5/btqGqQ37qzN/oDjySkOQrxJ1ZILcafHe4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGqWA5/btqGqQ37qzN/oDjySkOQrxJ1ZILcafHe4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGqWA5/btqGqQ37qzN/oDjySkOQrxJ1ZILcafHe4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGqWA5%2FbtqGqQ37qzN%2FoDjySkOQrxJ1ZILcafHe4k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 기존 Master 인스턴스와 나머지 Redis 인스턴스 사이 네트워크가 단절된 모습입니다. 네트워크 이슈이므로 Master 인스턴스는 현재 정상적으로 Client와 통신이 가능하며, 데이터 쓰기 작업이 발생하면 Master 인스턴스에 데이터가 저장됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 Sentinel과 연결이 끊겼기 때문에 Sentinel은 Master가 죽은 것으로 판단하고 Replica 중 하나를 Master로 선출하게됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 이러한 상황에서 네트워크 단절 이슈가 해결된다면 기존 Master 인스턴스는 Sentinel에 의하여 Replica로 변경이 될 것이고, 이때 기존 Master 인스턴스에 새롭게 수정된 데이터는 유실됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;참고:&lt;/p&gt;
&lt;p&gt;- &lt;a href=&quot;https://aphyr.com/posts/287-acynchronous-replication-with-failover&quot;&gt;https://aphyr.com/posts/287-acynchronous-replication-with-failover&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;- &lt;a href=&quot;http://antirez.com/news/56&quot;&gt;http://antirez.com/news/56&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Redis의 기본 구조와 복제에 대해서 살펴봤습니다.&amp;nbsp;다음 포스팅에서는 Redis 파티셔닝에 대해서 다루도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>DB/Redis</category>
      <category>java redis</category>
      <category>lettuce</category>
      <category>Redis</category>
      <category>redis 사용법</category>
      <category>redis 아키텍처</category>
      <category>spring data redis</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/101</guid>
      <comments>https://cla9.tistory.com/101#entry101comment</comments>
      <pubDate>Sun, 9 Aug 2020 16:14:06 +0900</pubDate>
    </item>
    <item>
      <title>4. JPA Cache 적용하기</title>
      <link>https://cla9.tistory.com/100</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 JPA에 Cache 적용방법에 대해서 다루어보겠습니다. 먼저 Cache 선정 기준 및 패턴에 대한 소개 및 적용 방법을 설명합니다. Cache로는 Ehcache3을 적용하며, Spring Actuator를 통해서 캐시 Metric 변화도 함께 살펴보겠습니다.&lt;/p&gt;
&lt;p&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;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Cache 적용 기준&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;캐시란 간단하게 말해서 Key와 Value로 이루어진 Map이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 일반 Map과는 다르게 만료 시간을 통해 freshness 조절 및 캐시 제거 등을 통해서 공간을 조절할 수 있는 특징이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 캐시 적용을 위해 고려해야할 척도는 무엇이 있을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 얼마나 자주 사용하는가?&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;250px-Long_tail.svg.png&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;130&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EaC9H/btqGkQCmH4Z/yKV0xt77wjSSy1kaqFebzk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EaC9H/btqGkQCmH4Z/yKV0xt77wjSSy1kaqFebzk/img.png&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://ko.wikipedia.org/wiki/%EA%B8%B4_%EA%BC%AC%EB%A6%AC&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EaC9H/btqGkQCmH4Z/yKV0xt77wjSSy1kaqFebzk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEaC9H%2FbtqGkQCmH4Z%2FyKV0xt77wjSSy1kaqFebzk%2Fimg.png&quot; data-filename=&quot;250px-Long_tail.svg.png&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;130&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://ko.wikipedia.org/wiki/%EA%B8%B4_%EA%BC%AC%EB%A6%AC&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 파레토 법칙을 표현합니다. 즉 시스템 리소스 20%가 전체 전체 시간의 80% 정도를 소요함을 의미합니다. 따라서 캐시 대상을 선정할 때에는 캐시 오브젝트가 얼마나 자주 사용하는지, 적용시 전체적인 성능을 대폭 개선할 수 있는지 등을 따져야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. HitRatio&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HitRatio는 캐시에 대하여 자원 요청과 비례하여 얼마나 캐시 정보를 획득했는지를 나타내며, 계산 식은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596628595257&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;HitRatio = hits / (hits + misses) * 100&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;캐시공간은 한정된 공간이기 때문에, 만료시간을 설정하여 캐시 유지시간을 설정할 수 있습니다. misses가 높다는 것은 캐시공간의 여유가 없어 이미 캐시에서 밀려났거나, 혹은 자주 사용하지 않는 정보를 캐시하여 만료시간이 지난 오브젝트를 획득하고자할 때 발생할 수 있습니다. 따라서 캐시를 설정할 때는 캐시 공간의 크기 및 만료 시간을 고려해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Cache 패턴&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 캐시에 적용되는 패턴에 대해서 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. No Caching&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;560&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvg7yO/btqGisipDdU/8SMXlw4p3fcWsmWBSIIvck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvg7yO/btqGisipDdU/8SMXlw4p3fcWsmWBSIIvck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvg7yO/btqGisipDdU/8SMXlw4p3fcWsmWBSIIvck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbvg7yO%2FbtqGisipDdU%2F8SMXlw4p3fcWsmWBSIIvck%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;560&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;말 그대로 캐시없이 Application에서 직접 DB로 요청하는 방식을 말합니다. 별도 캐시한 내역이 없으므로 매번 DB와의 통신이 필요하며, 부하가 유발되는 SQL이 지속 수행되면 DB I/O에 영향을 줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Cache-aside&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;560&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp1gsz/btqGebu9mu1/SJwJCBMlZ2Zz5J4T3tf8Vk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp1gsz/btqGebu9mu1/SJwJCBMlZ2Zz5J4T3tf8Vk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp1gsz/btqGebu9mu1/SJwJCBMlZ2Zz5J4T3tf8Vk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp1gsz%2FbtqGebu9mu1%2FSJwJCBMlZ2Zz5J4T3tf8Vk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;560&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Application 기동시 캐시에는 아무 데이터가 없으며, Application이 요청시에 Cache Miss가 발생하면, DB로부터 데이터를 읽어와 Cache에 적재합니다. 이후에 동일한 요청을 반복하면, 캐시에 데이터가 존재하므로 DB 조회 없이 바로 데이터를 전달받을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;해당 패턴은 Application이 캐시 적재와 관련된 일을 처리하므로, Cache Miss가 발생했을 때 응답시간이 저하될 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Cache-through&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;캐시에 데이터가 없는 상황에서 Miss가 발생했을 때, Application이 아닌 캐시제공자가 데이터를 처리한 다음 Application에게 데이터를 전달하는 방법입니다. 즉 기존에는 동기화의 책임이 Application에 있었다면, 해당 패턴은 캐시 제공자에게 책임이 위임됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Cache-through 패턴은 다음과 같이 세분화할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Read-through&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;640&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gco8s/btqGh5A2FMc/N5wWiU5bifY8vg22sHmoKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gco8s/btqGh5A2FMc/N5wWiU5bifY8vg22sHmoKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gco8s/btqGh5A2FMc/N5wWiU5bifY8vg22sHmoKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGco8s%2FbtqGh5A2FMc%2FN5wWiU5bifY8vg22sHmoKk%2Fimg.png&quot; width=&quot;640&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;데이터 읽기 요청시, 캐시 제공자가 DB와의 연계를 통해 데이터를 적재하고 이를 반환합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Write-through&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYPnro/btqGiKQGShj/IVd7xZJTx986KcXKcxC8lK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYPnro/btqGiKQGShj/IVd7xZJTx986KcXKcxC8lK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYPnro/btqGiKQGShj/IVd7xZJTx986KcXKcxC8lK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYPnro%2FbtqGiKQGShj%2FIVd7xZJTx986KcXKcxC8lK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;데이터 쓰기 요청시, Application은 캐시에만 적용을 요청하면, 캐시 제공자가 DB에 데이터를 저장하고, Application에게 응답하는 방식입니다. 모든 작업은 동기로 진행됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Write-behind&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V0X4m/btqGfMnWkrp/t8M6EkqGwNK75ZpKtVlKzK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V0X4m/btqGfMnWkrp/t8M6EkqGwNK75ZpKtVlKzK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V0X4m/btqGfMnWkrp/t8M6EkqGwNK75ZpKtVlKzK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV0X4m%2FbtqGfMnWkrp%2Ft8M6EkqGwNK75ZpKtVlKzK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;데이터 쓰기 요청시, Application은 데이터를 캐시에만 반영한 다음 요청을 종료합니다. 이후 일정 시간을 간격으로 비동기적으로 캐시에서 DB로 데이터를 저장요청합니다. 이 방식은 Application의 쓰기 요청 성능을 높일 수 있으나 만약 캐시에 DB에 저장하기 전에 다운된다면, 데이터 유실이 발생합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. EhCache&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Ehcache는 Java에서 주로 사용하는 캐시 관련 오픈소스이며, Application에 Embedded되어 간편하게 사용할 수 있는 특징을 지니고 있습니다. EhCache3에서는 JSR-107에서 요구하는 표준을 준수하여 만들어졌기 때문에 2 버전과 3 버전 설정 방법이 다릅니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Ehcache에서는 이전에 설명한 캐시 패턴을 모두 적용할 수 있습니다. 그 중 Cache-through 전략은 &lt;span&gt;&lt;b&gt;CacheLoaderWriter&lt;/b&gt; 인터페이스 구현을 통해서 적용할 수 있으나 해당 내용에 대해서는 다루지 않겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;자세한 설명은 &lt;a href=&quot;https://www.ehcache.org/documentation/3.4/writers.html#introduction&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;공식 홈페이지&lt;/u&gt;&lt;/a&gt;를 참고바랍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;offheap.png&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;294&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wbgQE/btqGiTff31q/HgNYMAoKfLUnBsbWDzFWVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wbgQE/btqGiTff31q/HgNYMAoKfLUnBsbWDzFWVk/img.png&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://www.ehcache.org/documentation/3.2/caching-concepts.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wbgQE/btqGiTff31q/HgNYMAoKfLUnBsbWDzFWVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwbgQE%2FbtqGiTff31q%2FHgNYMAoKfLUnBsbWDzFWVk%2Fimg.png&quot; data-filename=&quot;offheap.png&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;294&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://www.ehcache.org/documentation/3.2/caching-concepts.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;공식 메뉴얼에 따르면, 캐시 중요도에 따라 세군데 영역으로 나뉘어 저장할 수 있습니다. 먼저 Heap Tier는 GC가 관여할 수 있는 JVM의 Heap영역을 말합니다. 반면, Off Heap은 GC에서 접근이 불가능하며 OS가 관리하는 영역입니다. 해당 영역에 데이터를 저장하려면, &lt;span&gt;-XX:MaxDirectMemorySize 옵션 설정을 통해 충분한 메모리를 확보해야합니다. 마지막 영역은 Disk 영역으로 해당 설명은 Skip 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;그럼 지금부터 지금까지 JPA 포스팅하면서 다룬 예제를 확장하여 EhCache를 적용하겠습니다. 예제 프로그램은 Cache-Aside 패턴을 통해 구현하며, 그외 나머지 패턴은 다루지 않겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;1. EhCache3 적용&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;build.gradle&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1596633927003&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...(중략)...
dependencies {
    ...(중략)...
    
    //cache
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation group: 'javax.cache', name: 'cache-api', version: '1.1.1'
    implementation group: 'org.hibernate', name: 'hibernate-jcache', version: '5.4.19.Final'
    implementation group: 'org.ehcache', name: 'ehcache', version: '3.8.1'
}
...(중략)...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;application.yaml&lt;/p&gt;
&lt;pre id=&quot;code_1596634013709&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  jpa:
    properties:
      javax:
        persistence:
          sharedcache:
            mode: ENABLE_SELECTIVE
      hibernate:
        cache:
          use_second_level_cache: true
          region:
            factory_class: org.hibernate.cache.jcache.internal.JCacheRegionFactory
        temp:
          use_jdbc_metadata_defaults: false
        format_sql: true
        show_sql: true
        use_sql_comments: true
    hibernate:
      ddl-auto: none
    database-platform: org.hibernate.dialect.Oracle10gDialect&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;JPA와 관련된 캐시 설정을 합니다. 설정 내용 중 캐시와 관련된 옵션을 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저&amp;nbsp;&lt;b&gt;SharedCache&lt;/b&gt;는 캐시모드를 설정할 수 있는 옵션으로 enable_selective는 @Cacheable이 설정된 엔티티에만 캐시를 적용함을 의미합니다. 만약 모든 엔티티에 적용하려면 all 옵션을 줄 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;560&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bq3svO/btqGkU5O6xZ/HEVSi2Ns6pue1e154HU9T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bq3svO/btqGkU5O6xZ/HEVSi2Ns6pue1e154HU9T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bq3svO/btqGkU5O6xZ/HEVSi2Ns6pue1e154HU9T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbq3svO%2FbtqGkU5O6xZ%2FHEVSi2Ns6pue1e154HU9T1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;560&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;use_second_level_cache&lt;/b&gt;는 2차 캐시 활성화 여부를 지정합니다. JPA에서 1차 캐시는 PersistentContext를 의미하며, 각 세션레벨에서 트랜잭션이 진행되는 동안에 캐시됩니다. 반면 2차 캐시는 SessionFactory 레벨에서의 캐시를 의미하며 모든 세션에게 공유되는 공간입니다. 해당 옵션을 통해서 2차 캐시 설정 여부를 지정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;span&gt;factory_class&lt;/span&gt;&lt;/b&gt;&lt;span&gt;는 캐시를 구현한 Provider 정보를 지정합니다. Ehcache3는 JSR-107 표준을 준수하여 개발되었기 때문에 JCacheRegionFactory를 지정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;2. Configuration 설정&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596635522181&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class CachingConfig {
    public static final String DB_CACHE = &quot;db_cache&quot;;

    private final javax.cache.configuration.Configuration&amp;lt;Object, Object&amp;gt; jcacheConfiguration;

    public CachingConfig() {
        this.jcacheConfiguration = Eh107Configuration.fromEhcacheCacheConfiguration(CacheConfigurationBuilder.newCacheConfigurationBuilder(Object.class, Object.class,
                ResourcePoolsBuilder.newResourcePoolsBuilder()
                        .heap(10000, EntryUnit.ENTRIES))
                .withSizeOfMaxObjectSize(1000, MemoryUnit.B)
                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(300)))
                .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(600))));
    }

    @Bean
    public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(javax.cache.CacheManager cacheManager) {
        return hibernateProperties -&amp;gt; hibernateProperties.put(ConfigSettings.CACHE_MANAGER, cacheManager);
    }

    @Bean
    public JCacheManagerCustomizer cacheManagerCustomizer() {
        return cm -&amp;gt; {
            cm.createCache(DB_CACHE, jcacheConfiguration);
        };
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Cache Config 클래스를 작성합니다. 먼저 생성자를 통해 캐시의 기본 설정을 구성했습니다. 위 구성은 테스트를 위해 임의로 지정하였으며, 커스터마이징하여 작성 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지정된 옵션 설명은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;총 10000개의 Entity를 저장할 수 있으며, 각 오브젝트 사이즈는 1000 Byte를 넘지 않도록 제한하였습니다. Object는 최초 캐시에 입력후 600초 동안 저장되며, 만약 마지막으로 캐시 요청이후에 300초동안 재요청이 없을 경우 만료되도록 지정하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자세한 설정 방법은 &lt;a href=&quot;https://www.ehcache.org/documentation/3.4/getting-started.html#configuring-ehcache&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;공식 문서&lt;/u&gt;&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596635750718&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table
@Getter
@Cacheable
@org.hibernate.annotations.Cache(region = CachingConfig.DB_CACHE, usage = CacheConcurrencyStrategy.READ_ONLY)
public class Customer {
    @Id
    @GeneratedValue
    @Column(name = &quot;id&quot;)
    private Long customerId;
    @Column(name = &quot;name&quot;)
    private String customerName;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;SharedCache 모드를 enable_selective로 지정하였으므로, @Cacheable 어노테이션을 추가하여 해당 엔티티를 캐시할 수 있도록 설정하였습니다. 캐시 제공자내에는 여러 캐시가 존재할 수 있으며, 캐시마다 이름이 부여되어있으므로 region영역에는 캐시내에서 참조할 캐시이름을 지정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;usage는 캐시와 관련된 동시성 전략을 지정할 수 있습니다. 지정할 수 있는 옵션으로는 &lt;b&gt;NONE, READ_ONLY, NONSTRICT_READ_WRITE, READ_WRITE, TRANSACTIONA&lt;/b&gt;L 총 5가지 입니다. 예제 프로그램에서는 읽기 전용으로만 지정하기 위해서 READ_ONLY 옵션을 부여했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596636460103&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Slf4j
public class CustomerService {
   ...(중략)...

    public CustomerDTO getCustomer(Long id) {
        log.info(&quot;getCustomer from db &quot;);
        Customer customer = repository.findById(id).orElseThrow(IllegalAccessError::new);
        return CustomerDTO.of(customer);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596636505806&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/customers&quot;)
public class CustomerController {
    ...(중략)...

    @GetMapping(&quot;/{id}&quot;)
    public CustomerDTO getCustomer(@PathVariable Long id) {
        return service.getCustomer(id);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;테스트를 위해 Service 및 Controller에 관련 메소드를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596636623617&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2020-08-05 23:09:30.493  INFO 1872 --- [nio-8080-exec-1] c.e.paging_demo.service.CustomerService  : getCustomer from db 
Hibernate: 
    select
        customer0_.id as id1_0_0_,
        customer0_.name as name2_0_0_ 
    from
        customer customer0_ 
    where
        customer0_.id=?
2020-08-05 23:09:40.278  INFO 1872 --- [nio-8080-exec-2] c.e.paging_demo.service.CustomerService  : getCustomer from db 
2020-08-05 23:09:51.598  INFO 1872 --- [nio-8080-exec-3] c.e.paging_demo.service.CustomerService  : getCustomer from db 
2020-08-05 23:09:52.384  INFO 1872 --- [nio-8080-exec-4] c.e.paging_demo.service.CustomerService  : getCustomer from db &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;실행 후 로그 출력 결과입니다. 최초 요청시에는 SQL 수행결과가 로그에 기록되었지만, 그 이후에는 SQL이 수행되지 않고 2차 캐시에서 정상적으로 가져온 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. DTO 레벨 캐시 추가 설정&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 테스트 목적으로 Entitiy 뿐만 아니라 DTO에도 캐시를 적용해보겠습니다. 테스트를 위해 두 대상은 다른 캐시영역에 저장되며, 두 캐시영역간 설정에 차이를 두겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596636807823&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class CachingConfig {
    public static final String DB_CACHE = &quot;db_cache&quot;;
    public static final String USER_CACHE = &quot;user_cache&quot;;

    ...(중략)...

    @Bean
    public JCacheManagerCustomizer cacheManagerCustomizer() {
        return cm -&amp;gt; {
            cm.createCache(DB_CACHE, jcacheConfiguration);
            cm.createCache(USER_CACHE, Eh107Configuration.fromEhcacheCacheConfiguration(CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, CustomerDTO.class,
                    ResourcePoolsBuilder.newResourcePoolsBuilder()
                            .heap(10000, EntryUnit.ENTRIES))
                    .withSizeOfMaxObjectSize(1000, MemoryUnit.B)
                    .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(10)))
                    .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(20)))));
        };
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;새로운 캐시를 추가하기 위해서 위와같이 Configuration 클래스를 수정했습니다. 신규로 추가하는 USER_CACHE는 만료시간을 훨씬 짧게하여 최장 20초간만 캐시에 저장되도록 설정하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596636914669&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@Slf4j
public class CustomerService {
    ...(중략)...

    @Cacheable(value = CachingConfig.USER_CACHE, key =&quot;#id&quot;)
    public CustomerDTO getCustomer(Long id) {
        log.info(&quot;getCustomer from db &quot;);
        Customer customer = repository.findById(id).orElseThrow(IllegalAccessError::new);
        return CustomerDTO.of(customer);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1596637082232&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/customers&quot;)
@Slf4j
public class CustomerController {
    ...(중략)...

    @GetMapping(&quot;/{id}&quot;)
    public CustomerDTO getCustomer(@PathVariable Long id) {
        log.info(&quot;Controller 영역&quot;);
        return service.getCustomer(id);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Service 클래스에 @Cacheable 어노테이션을 추가하여 캐시를 지정합니다. 이때 key는 파라미터로 전달받은 id를 지정하며, SPEL을 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Controller 클래스에는 별다른 로직 추가 없이 로그만 남기도록 수정했습니다. 이제 어플리케이션을 재기동 후 결과를 보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596637184331&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;2020-08-05 23:18:53.012  INFO 13244 --- [nio-8080-exec-1] c.e.p.controller.CustomerController      : Controller 영역
2020-08-05 23:18:53.055  INFO 13244 --- [nio-8080-exec-1] c.e.paging_demo.service.CustomerService  : getCustomer from db 
Hibernate: 
    select
        customer0_.id as id1_0_0_,
        customer0_.name as name2_0_0_ 
    from
        customer customer0_ 
    where
        customer0_.id=?
2020-08-05 23:18:57.093  INFO 13244 --- [nio-8080-exec-2] c.e.p.controller.CustomerController      : Controller 영역
2020-08-05 23:18:57.821  INFO 13244 --- [nio-8080-exec-3] c.e.p.controller.CustomerController      : Controller 영역
2020-08-05 23:19:03.127  INFO 13244 --- [nio-8080-exec-4] c.e.p.controller.CustomerController      : Controller 영역
2020-08-05 23:19:29.327  INFO 13244 --- [nio-8080-exec-6] c.e.p.controller.CustomerController      : Controller 영역
2020-08-05 23:19:29.329  INFO 13244 --- [nio-8080-exec-6] c.e.paging_demo.service.CustomerService  : getCustomer from db &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;최초 기동 후에는 모든 캐시에 정보가 없으므로 Controller -&amp;gt; Service -&amp;gt; DB 순으로 호출되어 데이터가 캐싱되었음을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이후에는 Service Layer의 DTO 또한 캐시되었으므로 지속 호출시에 Controller 로그는 출력되나 Service 레벨 메소드는 호출되지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;20초가 지난 이후에는 Service Layer에 지정된 DTO 캐시가 만료되므로 Entity에 접근하나 해당 캐시는 아직 유효하므로 별도 DB 통신 없이 캐시에서 데이터를 반환하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금까지 캐시 적재에 대해서만 살펴봤는데, 만약 수정, 삭제등으로 인해 캐시를 삭제해야한다면 &lt;b&gt;@CacheEvict &lt;/b&gt;어노테이션을 통해 삭제할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Actuator 적용&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Spring Actuator 프로젝트를 사용하게되면, 모니터링에 필요한 유용한 Metric 뿐만 아니라 HealthCheck 및 Dump 생성 그리고 Reload 등이 가능합니다. Spring Actuator를 통해서 Cache와 관련된 Metric을 활용해 Prometheus &amp;amp; Grafana 대시보드로 시각화 또한 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 Actuator 적용 후 URL 호출을 통해 Metric을 확인하는 방법에 대해서 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Actuator 설정&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1596637648964&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...(중략)...
dependencies {
    ...(중략)...
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}    &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 actuator 관련 의존성을 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;application.yaml&lt;/p&gt;
&lt;pre id=&quot;code_1596637700964&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;management:
  endpoints:
    health:
      show-details: always
    web:
      exposure:
        include: &quot;*&quot;
  metrics:
    cache:
      instrument: true&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;해당 설정을 통해 actuator 기능을 활성화합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 적용 확인&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;935&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ooNvS/btqGiqxBpyi/kMKJLHgr2578BjoaQAob9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ooNvS/btqGiqxBpyi/kMKJLHgr2578BjoaQAob9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ooNvS/btqGiqxBpyi/kMKJLHgr2578BjoaQAob9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FooNvS%2FbtqGiqxBpyi%2FkMKJLHgr2578BjoaQAob9k%2Fimg.png&quot; data-origin-width=&quot;473&quot; data-origin-height=&quot;935&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;프로그램 기동 후 ip:port/actuator URL을 호출하면 위와 같이 상세 정보를 볼 수 있는 URL이 제공됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rq1Wj/btqGkttXWfj/knssI7B0hUb0uOOT0TW7xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rq1Wj/btqGkttXWfj/knssI7B0hUb0uOOT0TW7xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rq1Wj/btqGkttXWfj/knssI7B0hUb0uOOT0TW7xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frq1Wj%2FbtqGkttXWfj%2FknssI7B0hUb0uOOT0TW7xK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Cache 관련 정보는 metrics 하위에 위 표시된 영역으로 정보를 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;693&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L09xa/btqGfMBvwbD/PnQx4U5trhqRcc2Qho0i90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L09xa/btqGfMBvwbD/PnQx4U5trhqRcc2Qho0i90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L09xa/btqGfMBvwbD/PnQx4U5trhqRcc2Qho0i90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL09xa%2FbtqGfMBvwbD%2FPnQx4U5trhqRcc2Qho0i90%2Fimg.png&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;693&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 gets과 관련된 metric 정보이며, 총 7번의 get 요청이 있었음을 확인할 수 있습니다. 그 밖에 다른 정보들도 제공되는 URL을 통해 정보 확인이 가능합니다.&lt;/p&gt;
&lt;p&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 JPA 캐시 적용에 대해서 알아봤습니다. 일반적으로 코드성 데이터나 공지사항과 같이 수정/삭제가 거의 없고 자주 사용되는 데이터에 대해서 캐시를 적용하면, 좋은 효과를 볼 수 있습니다. 하지만 무턱대고 적용했다가는 오히려 성능 저하가 발생될 수 있으니 전략 수립이 필요합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 각 Application 내부에서만 유효한 Local Cache에 대해서 다루었습니다. EhCache가 Clustering을 지원하지만, Application Scale Out에 영향을 주기때문에 개인적으로는 가급적 Local Cache로써 사용하고 캐시 무효화 시간을 짧게 가지는 것이 좋다고 생각합니다. 만약 캐시간의 동기화가 필요하다면 Clustering을 고려하거나 Redis와 같은 Third 캐시를 추가로 두는 것을 고려해볼 수 있습니다.&lt;/p&gt;</description>
      <category>JAVA/JPA</category>
      <category>Ehacache</category>
      <category>Ehacahe3</category>
      <category>JPA</category>
      <category>JPA 캐시</category>
      <category>jsr-107</category>
      <category>Spring Cache</category>
      <category>스프링 캐시</category>
      <category>캐시 방법</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/100</guid>
      <comments>https://cla9.tistory.com/100#entry100comment</comments>
      <pubDate>Wed, 5 Aug 2020 23:43:47 +0900</pubDate>
    </item>
    <item>
      <title>3. JPA  Hateoas 적용하기</title>
      <link>https://cla9.tistory.com/97</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서 왜 Hateoas를 적용해야하는지에 대해서 다루지 않습니다. 이와 관련된 자세한 이응준님의 &lt;a href=&quot;https://youtu.be/RP_f5dMoHFc&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;그런 REST API로 괜찮은가 발표 세션&lt;/u&gt;&lt;/a&gt;을 꼭 보시기를 권장드리며, 해당 내용을 알고 계시다는 전제하에 포스팅을 작성하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에 다룰 내용은 Rest API 작성을 위해 필요한 Hateoas를 기존에 생성한 Page 객체에 적용하는 방법과&lt;span style=&quot;color: #333333;&quot;&gt; PagedResourceAssembler를 Customizing하는 방법을 알아보겠습니다.&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 기존 Page 객체 이슈&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GkTHp/btqGd99ZsLJ/gSOYpOwVC7KS3hyMy87Xo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GkTHp/btqGd99ZsLJ/gSOYpOwVC7KS3hyMy87Xo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GkTHp/btqGd99ZsLJ/gSOYpOwVC7KS3hyMy87Xo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGkTHp%2FbtqGd99ZsLJ%2FgSOYpOwVC7KS3hyMy87Xo0%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 이전 포스팅에서 예제 프로그램 API 호출 결과입니다. 해당 API는 원하는 요청에 대한 결과를 확인할 수 있습니다. 하지만 데이터를 통해서 어떠한 행위들이 가능한지 혹은 메시지가 정확히 어떠한 의미를 포함하고 있는지는 API 결과를 통해 알 수 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다시말해 해당 API 결과를 보고서는 customer_id를 통해 어떠한 API를 추가적으로 호출할 수 있는지 어떤 상태 변화가 가능한지 알 수 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 Hateoas가 적용된 API의 모습은 어떨까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596255302255&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[
  {
    &quot;id&quot;: 1,
    &quot;node_id&quot;: &quot;MDU6SXNzdWUx&quot;,
    &quot;url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/issues/1347&quot;,
    &quot;repository_url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World&quot;,
    &quot;labels_url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/issues/1347/labels{/name}&quot;,
    &quot;comments_url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/issues/1347/comments&quot;,
    &quot;events_url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/issues/1347/events&quot;,
    &quot;html_url&quot;: &quot;https://github.com/octocat/Hello-World/issues/1347&quot;,
    &quot;number&quot;: 1347,
    &quot;state&quot;: &quot;open&quot;,
    &quot;title&quot;: &quot;Found a bug&quot;,
    &quot;body&quot;: &quot;I'm having a problem with this.&quot;,
    &quot;user&quot;: {
      &quot;login&quot;: &quot;octocat&quot;,
      &quot;id&quot;: 1,
      &quot;node_id&quot;: &quot;MDQ6VXNlcjE=&quot;,
      &quot;avatar_url&quot;: &quot;https://github.com/images/error/octocat_happy.gif&quot;,
      &quot;gravatar_id&quot;: &quot;&quot;,
      &quot;url&quot;: &quot;https://api.github.com/users/octocat&quot;,
      &quot;html_url&quot;: &quot;https://github.com/octocat&quot;,
      &quot;followers_url&quot;: &quot;https://api.github.com/users/octocat/followers&quot;,
      &quot;following_url&quot;: &quot;https://api.github.com/users/octocat/following{/other_user}&quot;,
      &quot;gists_url&quot;: &quot;https://api.github.com/users/octocat/gists{/gist_id}&quot;,
      &quot;starred_url&quot;: &quot;https://api.github.com/users/octocat/starred{/owner}{/repo}&quot;,
      &quot;subscriptions_url&quot;: &quot;https://api.github.com/users/octocat/subscriptions&quot;,
      &quot;organizations_url&quot;: &quot;https://api.github.com/users/octocat/orgs&quot;,
      &quot;repos_url&quot;: &quot;https://api.github.com/users/octocat/repos&quot;,
      &quot;events_url&quot;: &quot;https://api.github.com/users/octocat/events{/privacy}&quot;,
      &quot;received_events_url&quot;: &quot;https://api.github.com/users/octocat/received_events&quot;,
      &quot;type&quot;: &quot;User&quot;,
      &quot;site_admin&quot;: false
    },
    &quot;labels&quot;: [
      {
        &quot;id&quot;: 208045946,
        &quot;node_id&quot;: &quot;MDU6TGFiZWwyMDgwNDU5NDY=&quot;,
        &quot;url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/labels/bug&quot;,
        &quot;name&quot;: &quot;bug&quot;,
        &quot;description&quot;: &quot;Something isn't working&quot;,
        &quot;color&quot;: &quot;f29513&quot;,
        &quot;default&quot;: true
      }
    ],
    &quot;assignee&quot;: {
      &quot;login&quot;: &quot;octocat&quot;,
      &quot;id&quot;: 1,
      &quot;node_id&quot;: &quot;MDQ6VXNlcjE=&quot;,
      &quot;avatar_url&quot;: &quot;https://github.com/images/error/octocat_happy.gif&quot;,
      &quot;gravatar_id&quot;: &quot;&quot;,
      &quot;url&quot;: &quot;https://api.github.com/users/octocat&quot;,
      &quot;html_url&quot;: &quot;https://github.com/octocat&quot;,
      &quot;followers_url&quot;: &quot;https://api.github.com/users/octocat/followers&quot;,
      &quot;following_url&quot;: &quot;https://api.github.com/users/octocat/following{/other_user}&quot;,
      &quot;gists_url&quot;: &quot;https://api.github.com/users/octocat/gists{/gist_id}&quot;,
      &quot;starred_url&quot;: &quot;https://api.github.com/users/octocat/starred{/owner}{/repo}&quot;,
      &quot;subscriptions_url&quot;: &quot;https://api.github.com/users/octocat/subscriptions&quot;,
      &quot;organizations_url&quot;: &quot;https://api.github.com/users/octocat/orgs&quot;,
      &quot;repos_url&quot;: &quot;https://api.github.com/users/octocat/repos&quot;,
      &quot;events_url&quot;: &quot;https://api.github.com/users/octocat/events{/privacy}&quot;,
      &quot;received_events_url&quot;: &quot;https://api.github.com/users/octocat/received_events&quot;,
      &quot;type&quot;: &quot;User&quot;,
      &quot;site_admin&quot;: false
    },
    &quot;assignees&quot;: [
      {
        &quot;login&quot;: &quot;octocat&quot;,
        &quot;id&quot;: 1,
        &quot;node_id&quot;: &quot;MDQ6VXNlcjE=&quot;,
        &quot;avatar_url&quot;: &quot;https://github.com/images/error/octocat_happy.gif&quot;,
        &quot;gravatar_id&quot;: &quot;&quot;,
        &quot;url&quot;: &quot;https://api.github.com/users/octocat&quot;,
        &quot;html_url&quot;: &quot;https://github.com/octocat&quot;,
        &quot;followers_url&quot;: &quot;https://api.github.com/users/octocat/followers&quot;,
        &quot;following_url&quot;: &quot;https://api.github.com/users/octocat/following{/other_user}&quot;,
        &quot;gists_url&quot;: &quot;https://api.github.com/users/octocat/gists{/gist_id}&quot;,
        &quot;starred_url&quot;: &quot;https://api.github.com/users/octocat/starred{/owner}{/repo}&quot;,
        &quot;subscriptions_url&quot;: &quot;https://api.github.com/users/octocat/subscriptions&quot;,
        &quot;organizations_url&quot;: &quot;https://api.github.com/users/octocat/orgs&quot;,
        &quot;repos_url&quot;: &quot;https://api.github.com/users/octocat/repos&quot;,
        &quot;events_url&quot;: &quot;https://api.github.com/users/octocat/events{/privacy}&quot;,
        &quot;received_events_url&quot;: &quot;https://api.github.com/users/octocat/received_events&quot;,
        &quot;type&quot;: &quot;User&quot;,
        &quot;site_admin&quot;: false
      }
    ],
    &quot;milestone&quot;: {
      &quot;url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/milestones/1&quot;,
      &quot;html_url&quot;: &quot;https://github.com/octocat/Hello-World/milestones/v1.0&quot;,
      &quot;labels_url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/milestones/1/labels&quot;,
      &quot;id&quot;: 1002604,
      &quot;node_id&quot;: &quot;MDk6TWlsZXN0b25lMTAwMjYwNA==&quot;,
      &quot;number&quot;: 1,
      &quot;state&quot;: &quot;open&quot;,
      &quot;title&quot;: &quot;v1.0&quot;,
      &quot;description&quot;: &quot;Tracking milestone for version 1.0&quot;,
      &quot;creator&quot;: {
        &quot;login&quot;: &quot;octocat&quot;,
        &quot;id&quot;: 1,
        &quot;node_id&quot;: &quot;MDQ6VXNlcjE=&quot;,
        &quot;avatar_url&quot;: &quot;https://github.com/images/error/octocat_happy.gif&quot;,
        &quot;gravatar_id&quot;: &quot;&quot;,
        &quot;url&quot;: &quot;https://api.github.com/users/octocat&quot;,
        &quot;html_url&quot;: &quot;https://github.com/octocat&quot;,
        &quot;followers_url&quot;: &quot;https://api.github.com/users/octocat/followers&quot;,
        &quot;following_url&quot;: &quot;https://api.github.com/users/octocat/following{/other_user}&quot;,
        &quot;gists_url&quot;: &quot;https://api.github.com/users/octocat/gists{/gist_id}&quot;,
        &quot;starred_url&quot;: &quot;https://api.github.com/users/octocat/starred{/owner}{/repo}&quot;,
        &quot;subscriptions_url&quot;: &quot;https://api.github.com/users/octocat/subscriptions&quot;,
        &quot;organizations_url&quot;: &quot;https://api.github.com/users/octocat/orgs&quot;,
        &quot;repos_url&quot;: &quot;https://api.github.com/users/octocat/repos&quot;,
        &quot;events_url&quot;: &quot;https://api.github.com/users/octocat/events{/privacy}&quot;,
        &quot;received_events_url&quot;: &quot;https://api.github.com/users/octocat/received_events&quot;,
        &quot;type&quot;: &quot;User&quot;,
        &quot;site_admin&quot;: false
      },
      &quot;open_issues&quot;: 4,
      &quot;closed_issues&quot;: 8,
      &quot;created_at&quot;: &quot;2011-04-10T20:09:31Z&quot;,
      &quot;updated_at&quot;: &quot;2014-03-03T18:58:10Z&quot;,
      &quot;closed_at&quot;: &quot;2013-02-12T13:22:01Z&quot;,
      &quot;due_on&quot;: &quot;2012-10-09T23:39:01Z&quot;
    },
    &quot;locked&quot;: true,
    &quot;active_lock_reason&quot;: &quot;too heated&quot;,
    &quot;comments&quot;: 0,
    &quot;pull_request&quot;: {
      &quot;url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World/pulls/1347&quot;,
      &quot;html_url&quot;: &quot;https://github.com/octocat/Hello-World/pull/1347&quot;,
      &quot;diff_url&quot;: &quot;https://github.com/octocat/Hello-World/pull/1347.diff&quot;,
      &quot;patch_url&quot;: &quot;https://github.com/octocat/Hello-World/pull/1347.patch&quot;
    },
    &quot;closed_at&quot;: null,
    &quot;created_at&quot;: &quot;2011-04-22T13:33:48Z&quot;,
    &quot;updated_at&quot;: &quot;2011-04-22T13:33:48Z&quot;,
    &quot;repository&quot;: {
      &quot;id&quot;: 1296269,
      &quot;node_id&quot;: &quot;MDEwOlJlcG9zaXRvcnkxMjk2MjY5&quot;,
      &quot;name&quot;: &quot;Hello-World&quot;,
      &quot;full_name&quot;: &quot;octocat/Hello-World&quot;,
      &quot;owner&quot;: {
        &quot;login&quot;: &quot;octocat&quot;,
        &quot;id&quot;: 1,
        &quot;node_id&quot;: &quot;MDQ6VXNlcjE=&quot;,
        &quot;avatar_url&quot;: &quot;https://github.com/images/error/octocat_happy.gif&quot;,
        &quot;gravatar_id&quot;: &quot;&quot;,
        &quot;url&quot;: &quot;https://api.github.com/users/octocat&quot;,
        &quot;html_url&quot;: &quot;https://github.com/octocat&quot;,
        &quot;followers_url&quot;: &quot;https://api.github.com/users/octocat/followers&quot;,
        &quot;following_url&quot;: &quot;https://api.github.com/users/octocat/following{/other_user}&quot;,
        &quot;gists_url&quot;: &quot;https://api.github.com/users/octocat/gists{/gist_id}&quot;,
        &quot;starred_url&quot;: &quot;https://api.github.com/users/octocat/starred{/owner}{/repo}&quot;,
        &quot;subscriptions_url&quot;: &quot;https://api.github.com/users/octocat/subscriptions&quot;,
        &quot;organizations_url&quot;: &quot;https://api.github.com/users/octocat/orgs&quot;,
        &quot;repos_url&quot;: &quot;https://api.github.com/users/octocat/repos&quot;,
        &quot;events_url&quot;: &quot;https://api.github.com/users/octocat/events{/privacy}&quot;,
        &quot;received_events_url&quot;: &quot;https://api.github.com/users/octocat/received_events&quot;,
        &quot;type&quot;: &quot;User&quot;,
        &quot;site_admin&quot;: false
      },
      &quot;private&quot;: false,
      &quot;html_url&quot;: &quot;https://github.com/octocat/Hello-World&quot;,
      &quot;description&quot;: &quot;This your first repo!&quot;,
      &quot;fork&quot;: false,
      &quot;url&quot;: &quot;https://api.github.com/repos/octocat/Hello-World&quot;,
      &quot;archive_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}&quot;,
      &quot;assignees_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/assignees{/user}&quot;,
      &quot;blobs_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}&quot;,
      &quot;branches_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/branches{/branch}&quot;,
      &quot;collaborators_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}&quot;,
      &quot;comments_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/comments{/number}&quot;,
      &quot;commits_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/commits{/sha}&quot;,
      &quot;compare_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}&quot;,
      &quot;contents_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/contents/{+path}&quot;,
      &quot;contributors_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/contributors&quot;,
      &quot;deployments_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/deployments&quot;,
      &quot;downloads_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/downloads&quot;,
      &quot;events_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/events&quot;,
      &quot;forks_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/forks&quot;,
      &quot;git_commits_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}&quot;,
      &quot;git_refs_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}&quot;,
      &quot;git_tags_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}&quot;,
      &quot;git_url&quot;: &quot;git:github.com/octocat/Hello-World.git&quot;,
      &quot;issue_comment_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}&quot;,
      &quot;issue_events_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/issues/events{/number}&quot;,
      &quot;issues_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/issues{/number}&quot;,
      &quot;keys_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/keys{/key_id}&quot;,
      &quot;labels_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/labels{/name}&quot;,
      &quot;languages_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/languages&quot;,
      &quot;merges_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/merges&quot;,
      &quot;milestones_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/milestones{/number}&quot;,
      &quot;notifications_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}&quot;,
      &quot;pulls_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/pulls{/number}&quot;,
      &quot;releases_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/releases{/id}&quot;,
      &quot;ssh_url&quot;: &quot;git@github.com:octocat/Hello-World.git&quot;,
      &quot;stargazers_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/stargazers&quot;,
      &quot;statuses_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/statuses/{sha}&quot;,
      &quot;subscribers_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/subscribers&quot;,
      &quot;subscription_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/subscription&quot;,
      &quot;tags_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/tags&quot;,
      &quot;teams_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/teams&quot;,
      &quot;trees_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}&quot;,
      &quot;clone_url&quot;: &quot;https://github.com/octocat/Hello-World.git&quot;,
      &quot;mirror_url&quot;: &quot;git:git.example.com/octocat/Hello-World&quot;,
      &quot;hooks_url&quot;: &quot;http://api.github.com/repos/octocat/Hello-World/hooks&quot;,
      &quot;svn_url&quot;: &quot;https://svn.github.com/octocat/Hello-World&quot;,
      &quot;homepage&quot;: &quot;https://github.com&quot;,
      &quot;language&quot;: null,
      &quot;forks_count&quot;: 9,
      &quot;stargazers_count&quot;: 80,
      &quot;watchers_count&quot;: 80,
      &quot;size&quot;: 108,
      &quot;default_branch&quot;: &quot;master&quot;,
      &quot;open_issues_count&quot;: 0,
      &quot;is_template&quot;: true,
      &quot;topics&quot;: [
        &quot;octocat&quot;,
        &quot;atom&quot;,
        &quot;electron&quot;,
        &quot;api&quot;
      ],
      &quot;has_issues&quot;: true,
      &quot;has_projects&quot;: true,
      &quot;has_wiki&quot;: true,
      &quot;has_pages&quot;: false,
      &quot;has_downloads&quot;: true,
      &quot;archived&quot;: false,
      &quot;disabled&quot;: false,
      &quot;visibility&quot;: &quot;public&quot;,
      &quot;pushed_at&quot;: &quot;2011-01-26T19:06:43Z&quot;,
      &quot;created_at&quot;: &quot;2011-01-26T19:01:12Z&quot;,
      &quot;updated_at&quot;: &quot;2011-01-26T19:14:43Z&quot;,
      &quot;permissions&quot;: {
        &quot;admin&quot;: false,
        &quot;push&quot;: false,
        &quot;pull&quot;: true
      },
      &quot;allow_rebase_merge&quot;: true,
      &quot;template_repository&quot;: null,
      &quot;temp_clone_token&quot;: &quot;ABTLWHOULUVAXGTRYU7OC2876QJ2O&quot;,
      &quot;allow_squash_merge&quot;: true,
      &quot;delete_branch_on_merge&quot;: true,
      &quot;allow_merge_commit&quot;: true,
      &quot;subscribers_count&quot;: 42,
      &quot;network_count&quot;: 0
    }
  }
]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 API는 Github의 &lt;span style=&quot;color: #000000;&quot;&gt;Issue API 결과입니다. 주요 데이터에 대하여 자신의 URL과 다음 상태 변이에 대한 URL 링크를 같이 제공하여 Hateoas를 준수하고 있습니다. &lt;/span&gt;&lt;span style=&quot;color: #000000; letter-spacing: 0px;&quot;&gt;따라서 Client는 서버로부터 받은 링크를 통해 상태를 변화할 뿐 Client 소스에 하드코딩되어 있을 필요가 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Spring에서는 Hateoas 기능을 손쉽게 적용가능하기 위한 Spring Hateoas 프로젝트가 있습니다. 해당 프로젝트를 적용하여, 기존 프로그램 예제를 확장해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Hateoas 적용&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;build.gradle&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596256475408&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(...중략...)
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-hateoas'
    (...중략...)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와같이 dependency에 hateoas를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. Controller&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596257141831&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class CustomerController {
    private final CustomerService service;
    private final PagedResourcesAssembler&amp;lt;CustomerDTO&amp;gt; assembler;

    @GetMapping(&quot;/customers&quot;)
    public ResponseEntity&amp;lt;PagedModel&amp;lt;EntityModel&amp;lt;CustomerDTO&amp;gt;&amp;gt;&amp;gt; getCustomer(PageableDTO pageableDTO){
        Page&amp;lt;CustomerDTO&amp;gt; customers = service.getCustomer(pageableDTO);
        PagedModel&amp;lt;EntityModel&amp;lt;CustomerDTO&amp;gt;&amp;gt; entityModels = assembler.toModel(customers);
        return ResponseEntity.ok(entityModels);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Controller에서 Page 모델을 기본적으로 제공되는 PagedResourcesAssembler를 통해 PagedModel 객체로 변환하는 코드를 추가하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-2. 실행 결과&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v80nW/btqGbb2Nyoc/rukK2Yn8CPuYs68dAhZa9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v80nW/btqGbb2Nyoc/rukK2Yn8CPuYs68dAhZa9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v80nW/btqGbb2Nyoc/rukK2Yn8CPuYs68dAhZa9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv80nW%2FbtqGbb2Nyoc%2FrukK2Yn8CPuYs68dAhZa9k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;PagedResourcedAssembler 적용 후 결과를 보면, self 링크를 포함하여 다음 상태 전이를 위한 API가 기본적으로 제공되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 위 API에서는 각각의 customerDTO 각 원소에 대한 상태 변이 URL은 제공되지 않았습니다. 이를 추가해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. PagedModelUtil&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596260519160&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class LinkResource&amp;lt;T&amp;gt; extends EntityModel&amp;lt;T&amp;gt; {
    public static &amp;lt;T&amp;gt; EntityModel&amp;lt;T&amp;gt; of(WebMvcLinkBuilder builder, T model, Function&amp;lt;T,?&amp;gt; resourceFunc){
        return EntityModel.of(model, getSelfLink(builder, model, resourceFunc));
    }

    private static&amp;lt;T&amp;gt; Link getSelfLink(WebMvcLinkBuilder builder, T data, Function&amp;lt;T, ?&amp;gt; resourceFunc){
        WebMvcLinkBuilder slash = builder.slash(resourceFunc.apply(data));
        return slash.withSelfRel();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;EntityModel을 생성할 때, Link를 주입하도록 LinkResource 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596260856641&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PagedModelUtil {
    public static &amp;lt;T&amp;gt; PagedModel&amp;lt;EntityModel&amp;lt;T&amp;gt;&amp;gt; getEntityModels(PagedResourcesAssembler&amp;lt;T&amp;gt; assembler,
                                                                 Page&amp;lt;T&amp;gt; page,
                                                                 Class&amp;lt;?&amp;gt; clazz,
                                                                 Function&amp;lt;T, ?&amp;gt; selfLinkFunc ){
        WebMvcLinkBuilder webMvcLinkBuilder = linkTo(clazz);
        return assembler.toModel(page, model -&amp;gt; LinkResource.of(webMvcLinkBuilder, model, selfLinkFunc::apply));
    }

    public static &amp;lt;T&amp;gt; PagedModel&amp;lt;EntityModel&amp;lt;T&amp;gt;&amp;gt; getEntityModels(PagedResourcesAssembler&amp;lt;T&amp;gt; assembler,
                                                                 Page&amp;lt;T&amp;gt; page,
                                                                 WebMvcLinkBuilder builder,
                                                                 Function&amp;lt;T, ?&amp;gt; selfLinkFunc ){
        return assembler.toModel(page, model -&amp;gt; LinkResource.of(builder, model, selfLinkFunc::apply));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;LinkResource를 기반으로 전체 Page 모델을 순회하면서 Link를 주입하고, 이를 PagedModel 객체로 컨버팅하는 Util 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596260969322&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class CustomerController {
    private final CustomerService service;
    private final PagedResourcesAssembler&amp;lt;CustomerDTO&amp;gt; assembler;

    @GetMapping(&quot;/customers&quot;)
    public ResponseEntity&amp;lt;PagedModel&amp;lt;EntityModel&amp;lt;CustomerDTO&amp;gt;&amp;gt;&amp;gt; getCustomer(PageableDTO pageableDTO){
        Page&amp;lt;CustomerDTO&amp;gt; customers = service.getCustomer(pageableDTO);

        PagedModel&amp;lt;EntityModel&amp;lt;CustomerDTO&amp;gt;&amp;gt; entityModels = PagedModelUtil.getEntityModels(assembler, customers,
                linkTo(methodOn(this.getClass()).getCustomer(null)),
                CustomerDTO::getCustomerId);
        return ResponseEntity.ok(entityModels);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Controller 클래스에서는 해당 Util 클래스 함수 호출을 통해 PagedModel로 변경하도록 코드를 변경헀습니다.&lt;/p&gt;
&lt;p&gt;여기서 두번째 및 세번째 인자가 각 DTO별로 호출할 메소드 및 ID 값에 해당합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 예제에서는 getCustomer 메소드를 두번째 인자로 전달했기 때문에 @GetMapping 어노테이션으로 전달한 customers 가 URL에 추가가 될 것입니다. 또한, customers URL 뒤에올 ID 값으로 customerDTO에 속한 id 값을 넘겨줌으로써 URL이 완성될 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;349&quot; data-origin-height=&quot;903&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MaNid/btqGbaiwqMr/yNn0LVCR9ZlS2gYbzikKf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MaNid/btqGbaiwqMr/yNn0LVCR9ZlS2gYbzikKf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MaNid/btqGbaiwqMr/yNn0LVCR9ZlS2gYbzikKf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMaNid%2FbtqGbaiwqMr%2FyNn0LVCR9ZlS2gYbzikKf0%2Fimg.png&quot; data-origin-width=&quot;349&quot; data-origin-height=&quot;903&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서, 위 코드를 통해서 각 DTO 별로 추가되는 URL은 http://localhost:8080/customers/{id}가 될 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596261842970&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class CustomerController {
    private final CustomerService service;
    private final PagedResourcesAssembler&amp;lt;CustomerDTO&amp;gt; assembler;

    @GetMapping(&quot;/customers&quot;)
    public ResponseEntity&amp;lt;PagedModel&amp;lt;EntityModel&amp;lt;CustomerDTO&amp;gt;&amp;gt;&amp;gt; getCustomer(PageableDTO pageableDTO){
        Page&amp;lt;CustomerDTO&amp;gt; customers = service.getCustomer(pageableDTO);

        PagedModel&amp;lt;EntityModel&amp;lt;CustomerDTO&amp;gt;&amp;gt; entityModels = PagedModelUtil.getEntityModels(assembler, customers,
                linkTo(methodOn(this.getClass()).getCustomer(null)),
                v-&amp;gt; v.getCustomerName() + &quot;/&quot; + v.getCustomerId());
        return ResponseEntity.ok(entityModels);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 URL을 변경하고 싶다면, 위와같이 두번째 인자 및 세번째 Function을 새로 정의하여 변경 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqSx4y/btqF9lLvWBQ/nhbbuj44Y0xoQIMzscj2w0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqSx4y/btqF9lLvWBQ/nhbbuj44Y0xoQIMzscj2w0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqSx4y/btqF9lLvWBQ/nhbbuj44Y0xoQIMzscj2w0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqSx4y%2FbtqF9lLvWBQ%2Fnhbbuj44Y0xoQIMzscj2w0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. PagedResourceAssembler 커스터마이징&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lzhuf/btqGbqk60JB/roLy0KtKBvxjDmjKGah991/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lzhuf/btqGbqk60JB/roLy0KtKBvxjDmjKGah991/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lzhuf/btqGbqk60JB/roLy0KtKBvxjDmjKGah991/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flzhuf%2FbtqGbqk60JB%2FroLy0KtKBvxjDmjKGah991%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금까지 Hateoas 적용과 각 DTO 별로 Self 링크를 적용하는 방법에 대해서 살펴봤습니다. 기본적으로 제공되는 PagedResourceAssembler에는 첫페이지, 이전 페이지, 현재 페이지, 다음 페이지 및 마지막 페이지 정보가 담겨있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tmt7g/btqGbqZFDF2/VbKLMDvHylsaWccNzmKKJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tmt7g/btqGbqZFDF2/VbKLMDvHylsaWccNzmKKJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tmt7g/btqGbqZFDF2/VbKLMDvHylsaWccNzmKKJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftmt7g%2FbtqGbqZFDF2%2FVbKLMDvHylsaWccNzmKKJ1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이는 해당 API가 게시판에 노출되는 API라면 위와 같은 모습에 사용될 수 있는 Link 정보를 담고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CUhpX/btqF9C0E7hv/QAdsuOLYAWdqrZ3Zf5ar70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CUhpX/btqF9C0E7hv/QAdsuOLYAWdqrZ3Zf5ar70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CUhpX/btqF9C0E7hv/QAdsuOLYAWdqrZ3Zf5ar70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCUhpX%2FbtqF9C0E7hv%2FQAdsuOLYAWdqrZ3Zf5ar70%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 사용자가 원하는 게시판의 형태가 위와 같다면, 기본 제공되는 PagedResourcesAssembler는 이를 표현할 수 없습니다. 따라서 Customizing이 필요합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-1. CustomPagedResourceAssembler&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596268716331&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomPagedResourceAssembler&amp;lt;T&amp;gt; extends PagedResourcesAssembler&amp;lt;T&amp;gt; {
    private final HateoasPageableHandlerMethodArgumentResolver pageableResolver;
    private final Optional&amp;lt;UriComponents&amp;gt; baseUri;
    private final int PAGE_SIZE;
    private final int DEFAULT_PAGE_SIZE = 10;

    public CustomPagedResourceAssembler(HateoasPageableHandlerMethodArgumentResolver pageableResolver, @Nullable  Integer pageSize) {
        super(pageableResolver,null);
        this.pageableResolver = pageableResolver;
        baseUri = Optional.empty();
        this.PAGE_SIZE = pageSize == null ? DEFAULT_PAGE_SIZE : pageSize;
    }

    @Override
    public PagedModel&amp;lt;EntityModel&amp;lt;T&amp;gt;&amp;gt; toModel(Page&amp;lt;T&amp;gt; entity) {
        return toModel(entity, EntityModel::of);
    }

    @Override
    public &amp;lt;R extends RepresentationModel&amp;lt;?&amp;gt;&amp;gt; PagedModel&amp;lt;R&amp;gt; toModel(Page&amp;lt;T&amp;gt; page, RepresentationModelAssembler&amp;lt;T, R&amp;gt; assembler) {
        Assert.notNull(page, &quot;Page must not be null!&quot;);
        Assert.notNull(assembler, &quot;ResourceAssembler must not be null!&quot;);

        List&amp;lt;R&amp;gt; resources = new ArrayList&amp;lt;&amp;gt;(page.getNumberOfElements());

        for (T element : page) {
            resources.add(assembler.toModel(element));
        }

        PagedModel&amp;lt;R&amp;gt; resource = createPagedModel(resources, asPageMetadata(page), page);
        return addPaginationLinks(resource, page, Optional.empty());
    }

    private PagedModel.PageMetadata asPageMetadata(Page&amp;lt;?&amp;gt; page) {

        Assert.notNull(page, &quot;Page must not be null!&quot;);
        int number = page.getNumber();
        return new PagedModel.PageMetadata(page.getSize(), number, page.getTotalElements(), page.getTotalPages());
    }

    private &amp;lt;R&amp;gt; PagedModel&amp;lt;R&amp;gt; addPaginationLinks(PagedModel&amp;lt;R&amp;gt; resources, Page&amp;lt;?&amp;gt; page, Optional&amp;lt;Link&amp;gt; link) {

        UriTemplate base = getUriTemplate(link);
        boolean isNavigable = page.hasPrevious() || page.hasNext();

        int currIndex = page.getNumber();
        int lastIndex = page.getTotalPages() == 0 ? 0 : page.getTotalPages() - 1;
        int baseIndex = (currIndex / this.PAGE_SIZE) * this.PAGE_SIZE;
        int prevIndex = baseIndex - this.PAGE_SIZE;
        int nextIndex = baseIndex + this.PAGE_SIZE;
        int size = page.getSize();
        Sort sort = page.getSort();

        if(lastIndex &amp;lt; currIndex)
            throw new IndexOutOfBoundsException(&quot;Page total size must not be less than page size&quot;);

        if (isNavigable) {
            resources.add(createLink(base, PageRequest.of(0, size, sort), IanaLinkRelations.FIRST));
        }

        Link selfLink = link.map(it -&amp;gt; it.withSelfRel()).orElseGet(() -&amp;gt; createLink(base, page.getPageable(), IanaLinkRelations.SELF));

        resources.add(selfLink);

        if (prevIndex &amp;gt;= 0) {
            resources.add(createLink(base,
                    PageRequest.of(prevIndex, size, sort),
                    IanaLinkRelations.PREV));
        }

        for(int i = 0; i &amp;lt; this.PAGE_SIZE; i++){
            if(baseIndex + i &amp;lt;= lastIndex){
                resources.add(createLink(base, PageRequest.of(baseIndex + i, size, sort), LinkRelation.of(&quot;pagination&quot;)));
            }
        }

        if(nextIndex &amp;lt; lastIndex){
            resources.add(createLink(base, PageRequest.of(nextIndex, size, sort), IanaLinkRelations.NEXT));
        }

        if (isNavigable) {
            resources.add(createLink(base, PageRequest.of(lastIndex, size, sort), IanaLinkRelations.LAST));
        }

        return resources;
    }

    private UriTemplate getUriTemplate(Optional&amp;lt;Link&amp;gt; baseLink) {
        return UriTemplate.of(baseLink.map(Link::getHref).orElseGet(this::baseUriOrCurrentRequest));
    }

    private String baseUriOrCurrentRequest() {
        return baseUri.map(Object::toString).orElseGet(CustomPagedResourceAssembler::currentRequest);
    }

    private static String currentRequest() {
        return ServletUriComponentsBuilder.fromCurrentRequest().build().toString();
    }

    private Link createLink(UriTemplate base, Pageable pageable, LinkRelation relation) {
        UriComponentsBuilder builder = fromUri(base.expand());
        pageableResolver.enhance(builder, getMethodParameter(), pageable);
        return Link.of(UriTemplate.of(builder.build().toString()), relation);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드는 기존 &lt;span style=&quot;color: #333333;&quot;&gt;PagedResourcesAssembler를 상속받아 구현하였습니다. 가장 핵심이 되는 부분은 addPaginationLinks 메소드이며, 해당 메소드를 통해 페이지의 인덱스 정보를 pagination 링크아래에 배치하도록 표현했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596268930711&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig implements WebMvcConfigurer {

    ...(중략)...

    @Bean
    public CustomPagedResourceAssembler&amp;lt;?&amp;gt; customPagedResourceAssembler(){
        return new CustomPagedResourceAssembler&amp;lt;&amp;gt;(new HateoasPageableHandlerMethodArgumentResolver(),10);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이후 해당 클래스를 Bean으로 등록시켜 줍니다. 생성자의 두번째 파라미터는 Page 사이즈를 결정하며, 해당 값의 변경을 통해 노출되는 URL 크기가 달라집니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-2 Controller&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596269019391&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class CustomerController {
    private final CustomerService service;
    private final CustomPagedResourceAssembler&amp;lt;CustomerDTO&amp;gt; assembler;

    @GetMapping(&quot;/customers&quot;)
    public ResponseEntity&amp;lt;PagedModel&amp;lt;EntityModel&amp;lt;CustomerDTO&amp;gt;&amp;gt;&amp;gt; getCustomer(PageableDTO pageableDTO){
        Page&amp;lt;CustomerDTO&amp;gt; customers = service.getCustomer(pageableDTO);

        PagedModel&amp;lt;EntityModel&amp;lt;CustomerDTO&amp;gt;&amp;gt; entityModels = PagedModelUtil.getEntityModels(assembler, customers,
                linkTo(methodOn(this.getClass()).getCustomer(null)),
                CustomerDTO::getCustomerId);
        return ResponseEntity.ok(entityModels);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Bean으로 등록한 CustomPagedResourceAssembler로 주입받도록 코드를 수정했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4-3 결과&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv7Lyd/btqGbavZk26/3i7CKRwEEuq6fJpwoBbg1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv7Lyd/btqGbavZk26/3i7CKRwEEuq6fJpwoBbg1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv7Lyd/btqGbavZk26/3i7CKRwEEuq6fJpwoBbg1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv7Lyd%2FbtqGbavZk26%2F3i7CKRwEEuq6fJpwoBbg1k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;애플리케이션 구동 후 해당 URL을 호출하면, 정상적으로 페이지 URL이 표시되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&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 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Page 객체에 Hateoas를 적용하는 방법에 대해서 살펴봤습니다. Rest API 설계를 위해서는 Hateoas 적용뿐만 아니라 Self-descriptive-messages를 적용하기 위해 IANA 등록 혹은 Profile 링크를 추가해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Profile 링크를 적용하기 위해서 RestDocs 혹은 Swagger 등을 활용할 수 있으니 참고 바랍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JAVA/JPA</category>
      <category>EntityModel</category>
      <category>Heatoas</category>
      <category>JPA</category>
      <category>Page</category>
      <category>Spring Data JPA</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/97</guid>
      <comments>https://cla9.tistory.com/97#entry97comment</comments>
      <pubDate>Sat, 1 Aug 2020 17:13:05 +0900</pubDate>
    </item>
    <item>
      <title>2. JPA Sort 조건 개선</title>
      <link>https://cla9.tistory.com/96</link>
      <description>&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cla9.tistory.com/95?category=869583&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;이전 포스팅&lt;/u&gt;&lt;/a&gt;에서 다룬 예제를 확장하여 진행하겠습니다. 이번 포스팅에서는 JPA Sort에 대해서 다루도록 하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;문제상황&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JxJuD/btqF5zW5tEI/SWQk1rSSQaBdVo6fdpDEn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JxJuD/btqF5zW5tEI/SWQk1rSSQaBdVo6fdpDEn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JxJuD/btqF5zW5tEI/SWQk1rSSQaBdVo6fdpDEn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJxJuD%2FbtqF5zW5tEI%2FSWQk1rSSQaBdVo6fdpDEn0%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sas&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://localhost:8080/customers?page=3&amp;amp;size=10&amp;amp;sort=customerId,asc&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예제 프로그램 수행후 다음과 같은 URL을 호출하면 page, size, sort 파라미터가 Pageable 객체로 컨버팅 되어 정상적으로 프로그램이 페이징과 정렬이 수행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sas&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;http://localhost:8080/customers?page=3&amp;amp;size=10&amp;amp;sort=customerId,asc&amp;amp;sort=customerName,desc&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;만약 여러 Column의 정렬을 수행하고 싶다면, 위와 같이 여러개의 sort 파라미터를 추가하면 정상 수행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 위 sort 조건은 @Entity 클래스에 정의한 필드명과 정확히 동일해야합니다. 만약 예제와 같이 동일 컬럼에 대하여 리턴 값은 SnakeCase인 customer_id 일경우 Client 로직에서는 전달받은 값 이외에 @Entitiy 클래스 필드명을 관리해야합니다. 만약 Entitiy에 정의되어있지 않은 필드명을 전달하게된다면, 에러가 발생합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Client 로직에서 도메인 객체 필드명에 의존하는 것은 바람직해 보이지 않습니다. 따라서 Enum과 DTO를 활용해서 Sort 조건이 도메인 객체와 직접연관되지 않도록 설정해봅시다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Pageable 구현&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 파라미터 객체를 Pageable로 Controller 파라미터로 주입받았습니다. 이때 도메인 객체 필드명을 Sort 인자에 정확히 명시해야하는 문제가 있었습니다. 따라서 방법을 바꾸어 별도 PageableDTO에 파라미터 인자를 주입받고 이를 처리하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;style1&quot; /&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;1. Pair 클래스&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596109357223&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Builder(access = AccessLevel.PRIVATE)
@ToString
public class Pair&amp;lt;T, U&amp;gt; {
    private T first;
    private U second;

    public static &amp;lt;T, U&amp;gt; Pair of(T first, U second) {
        return Pair.builder()
                .first(first)
                .second(second)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 컬럼명과 정렬 상태를 담을 Pair 클래스를 생성합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Sort 상태를 표현하는 Enum 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596110325027&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum SortOption {
    ASC,
    DESC
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;3. PageableDTO&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596109427121&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class PageableDTO {
    private Integer page;
    private Integer size;
    private Integer totalElements;
    private List&amp;lt;Pair&amp;lt;String, SortOption&amp;gt;&amp;gt; sorts;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Page 관련 정보와 Sort 정보를 담을 DTO를 선언합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;4.&amp;nbsp; HandlerMethodArgumentResolver 클래스 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596109604952&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class PageableArgumentResolver implements HandlerMethodArgumentResolver {
    private final String PAGE = &quot;page&quot;;
    private final String SIZE = &quot;size&quot;;
    private final String TOTAL_ELEMENTS = &quot;total_elements&quot;;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().equals(PageableDTO.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        List&amp;lt;Pair&amp;lt;String, SortOption&amp;gt;&amp;gt; sorts = new ArrayList&amp;lt;&amp;gt;();
        final var sortArr = webRequest.getParameterMap().get(&quot;sort&quot;);

        if(sortArr != null){
            for(var v : sortArr){
                String[] keywords = v.split(&quot;,&quot;);
                sorts.add(Pair.of(keywords[0], (keywords.length &amp;lt; 2 || keywords[1].equals(&quot;asc&quot;)) ? SortOption.ASC : SortOption.DESC));
            }
        }

        return PageableDTO.builder()
                .page(getValue(webRequest.getParameter(PAGE)))
                .size(getValue(webRequest.getParameter(SIZE)))
                .totalElements(getValue(webRequest.getParameter(TOTAL_ELEMENTS)))
                .sorts(sorts)
                .build();
    }

    private Integer getValue(String param) {
        return param != null ? Integer.parseInt(param) : null;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596109820752&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List&amp;lt;HandlerMethodArgumentResolver&amp;gt; resolvers) {
        resolvers.add(new PageableArgumentResolver());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위 클래스는 Custom HandlerMethodArgumentResolver입니다. 별도로 이를 구현한 이유는 PageableDTO에 정렬 정보를 담기 위해서는 배열형태로 넘어오는 Sort 정보를 @ModelAttribute를 통해 주입받을 수 없기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;따라서, 별도 HandlerMethodArgumentResolver를 구현하여 파라미터 값에서 관심있는 값들을 가져와 가공하여 DTO를 생성받아 주입할 수 있도록 구현하여 기존 resolver 목록에 추가했습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;만약 HandlerMethodArgumentResolver를 사용하고 싶지 않다면, MultiValueMap을 통해 주입받은 후 별도 Factory 메소드를 사용해 DTO를 코드상에서 변경시키는 것도 한 방법입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;5. 도메인 컬럼 매핑 Layer 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596110094992&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MetaModelUtil {
    public static String getColumn(Path&amp;lt;?&amp;gt; path){
        return path.getMetadata().getName();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596110108841&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum CustomerMetaType {
    CUSTOMER_ID(getColumn(customer.customerId)),
    CUSTOMER_NAME(getColumn(customer.customerName));

    private String name;
    CustomerMetaType(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Client에서 존재하는 SnakeCase의 컬럼명과 실제 @Entity 클래스 컬럼명을 매핑하기 위하여 Enum을 사용했습니다. @Entity로 부터 생성되는 컬럼명을 얻어오기 위해서는 QueryDsl의 Q클래스를 활용하거나 JPA Meta모델을 활용하는 방법이 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;대개 JPA를 쓰면서 QueryDsl을 같이 쓰기 때문에 별도 JPA Meta모델을 추출하기 보다는 QueryDsl의 MetaModel을 활용하여 매핑하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;6. DomainSpec 클래스 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;6-1 Sort 전략 클래스 생성&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596353003580&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface SortStrategy&amp;lt;T extends Enum&amp;lt;T&amp;gt;&amp;gt; {
    Sort.Order getSortOrder(T domainKey, SortOption order);
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1596353023993&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CustomerSortStrategy implements SortStrategy&amp;lt;CustomerMetaType&amp;gt; {
    @Override
    public Sort.Order getSortOrder(CustomerMetaType domainKey, SortOption order) {
        Sort.Order sortOrder = null;
        switch (domainKey){
            case CUSTOMER_ID:
            case CUSTOMER_NAME:
                final var column = domainKey.getName();
                sortOrder = (order == SortOption.ASC) ? Sort.Order.asc(column) : Sort.Order.desc(column);
        }
        return sortOrder;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;엔티티별로 정렬을 구현할 전략을 설정할 수 있는 객체를 생성합니다. 위 switch 구문에서 CUSTOMER_ID를 제거하면 Client로부터 넘어오는 customer_id는 해당되지 않으므로 Null이 반환되고 에러가 발생하게 될 것입니다. 따라서 Strategy에 지정한 구문여부에 따라서 정렬 대상을 한정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;6-2. DomainSpec 클래스 생성&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596110642326&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;
public class DomainSpec&amp;lt;T extends Enum&amp;lt;T&amp;gt;, U&amp;gt; {
    private final Class&amp;lt;T&amp;gt; clazz;
    @Getter @Setter
    private int defaultPage = 0;
    @Getter @Setter
    private int defaultSize = 20;
    private SortStrategy&amp;lt;T&amp;gt; sortStrategy;

    public DomainSpec(Class&amp;lt;T&amp;gt; clazz, SortStrategy&amp;lt;T&amp;gt; sortStrategy) {
        this.clazz = clazz;
        this.sortStrategy = sortStrategy;
    }

    public DomainSpec(Class&amp;lt;T&amp;gt; clazz, SortStrategy&amp;lt;T&amp;gt; sortStrategy, int defaultPage, int defaultSize) {
        this.clazz = clazz;
        this.defaultPage = defaultPage;
        this.defaultSize = defaultSize;
        this.sortStrategy = sortStrategy;
    }

    public void changeSortStrategy(SortStrategy&amp;lt;T&amp;gt; sortStrategy) {
        this.sortStrategy = sortStrategy;
    }

    public Pageable getPageable(PageableDTO dto) {
        Integer page = dto.getPage();
        Integer size = dto.getSize();
        Pageable pageable;

        if (dto.getSorts().size() &amp;lt; 1) {
            pageable = PageRequest.of(page == null ? defaultPage : page, size == null ? defaultSize : size);
        } else {
            List&amp;lt;Sort.Order&amp;gt; sorts = getSortOrders(dto.getSorts());
            pageable = PageRequest.of(page == null ? defaultPage : page, size == null ? defaultSize : size, Sort.by(sorts));
        }

        return pageable;
    }

    private List&amp;lt;Sort.Order&amp;gt; getSortOrders(List&amp;lt;Pair&amp;lt;String, SortOption&amp;gt;&amp;gt; sorts) {
        Assert.notNull(this.sortStrategy,&quot;There is no sort strategy&quot;);

        List&amp;lt;Sort.Order&amp;gt; orders = new ArrayList&amp;lt;&amp;gt;();
        for (var o : sorts) {
            T column;
            try {
                column = Enum.valueOf(this.clazz, o.getFirst().toUpperCase());
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException(&quot;Sort option error&quot;);
            }

            final Sort.Order order = sortStrategy.getSortOrder(column, o.getSecond());
            Assert.notNull(order, &quot;sort option error&quot;);

            orders.add(order);
        }
        return orders;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;정렬을 포함한 Pageable 객체를 얻어오기 위한 클래스입니다. SortStrategy를 통해 전달받은 구현체에 정렬 방법을 위임합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;style1&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;text-align: left;&quot; data-ke-size=&quot;size20&quot;&gt;7. Service 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596111259985&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class CustomerService {
    private final CustomerRepository repository;
    private DomainSpec&amp;lt;CustomerMetaType, Customer&amp;gt; spec;

    public CustomerService(CustomerRepository repository) {
        this.repository = repository;
        this.spec = new DomainSpec&amp;lt;&amp;gt;(CustomerMetaType.class, new CustomerSortStrategy());
    }

    public Page&amp;lt;CustomerDTO&amp;gt; getCustomer(PageableDTO pageableDTO) {
        final var pageable = spec.getPageable(pageableDTO);

        final var customers = repository.findAll(pageable, pageableDTO.getTotalElements());
        return customers.map(CustomerDTO::of);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정렬 조건을 포함한 Pageable 객체를 CustomerSpec에서 가져올 수 있도록 구성하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;8. Controller 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596111310810&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class CustomerController {
    private final CustomerService service;

    @GetMapping(&quot;/customers&quot;)
    public ResponseEntity&amp;lt;Page&amp;lt;CustomerDTO&amp;gt;&amp;gt; getCustomer(PageableDTO pageableDTO){
        Page&amp;lt;CustomerDTO&amp;gt; customers = service.getCustomer(pageableDTO);
        return ResponseEntity.ok(customers);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Pageable 객체를 바로 전달받는 것이 아니라, &lt;span style=&quot;color: #333333;&quot;&gt;HandlerMethodArgumentResolver를 통해 &lt;/span&gt;PageableDTO를 주입받아 이를 서비스 객체로 넘기도록 코드를 수정하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;9. 실행 결과&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdgUGk/btqGd1YyBlH/TIVQDLYlKDoAM0qrUAFqNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdgUGk/btqGd1YyBlH/TIVQDLYlKDoAM0qrUAFqNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdgUGk/btqGd1YyBlH/TIVQDLYlKDoAM0qrUAFqNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdgUGk%2FbtqGd1YyBlH%2FTIVQDLYlKDoAM0qrUAFqNk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-filename=&quot;blob&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;프로그램을 실행시키면, 기존과는 다르게 customer_name과 같이 도메인 엔티티 컬럼명에 종속적이지 않고 컬럼명을 지정할 수 있습니다. 또한, 컬럼 정렬 대상을 제한하여 보다 무분별한 정렬이 발생되지 않도록 강제할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&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;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 JPA에서 정렬 조건을 제한하는 방법과 엔티티 컬럼명에 의존하지 않도록 매핑 Layer를 두는 방법에 대해서 알아봤습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 구조를 조금 확장하면, Specification이나 BooleanBuilder 등을 활용하여 SQL Where 조건절에 추가하는 도메인 코드 로직을 구성할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;u&gt;&lt;a href=&quot;https://velog.io/@hellozin/JPA-Specification%EC%9C%BC%EB%A1%9C-%EC%BF%BC%EB%A6%AC-%EC%A1%B0%EA%B1%B4-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;hellozin님 블로그&lt;/a&gt;&lt;/u&gt;님 포스팅에 이와 관련된 내용이 있으니 참고하면 좋을 것 같습니다.&lt;/p&gt;
&lt;figure id=&quot;og_1596115078232&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-og-type=&quot;article&quot; data-og-title=&quot;JPA Specification으로 쿼리 조건 처리하기&quot; data-og-description=&quot;해당 코드는 Github에서 확인할 수 있습니다. Spring Data에서 Specification은 DB 쿼리의 조건을 Spec으로 작성해 Repository method에 적용하거나 몇가지 Spec을 조합해서 사용할 수 있게 도와줍니다. 간단한 예&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@hellozin/JPA-Specification%EC%9C%BC%EB%A1%9C-%EC%BF%BC%EB%A6%AC-%EC%A1%B0%EA%B1%B4-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@hellozin/JPA-Specification으로-쿼리-조건-처리하기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/CMpOG/hyGXI4xzH5/fEufKRL2owsv8F2zbHAVv0/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500,https://scrap.kakaocdn.net/dn/HSlJc/hyGXHR5zWo/YrkPADdGjWopfDpGn7kMx1/img.jpg?width=240&amp;amp;height=240&amp;amp;face=52_85_166_199&quot;&gt;&lt;a href=&quot;https://velog.io/@hellozin/JPA-Specification%EC%9C%BC%EB%A1%9C-%EC%BF%BC%EB%A6%AC-%EC%A1%B0%EA%B1%B4-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@hellozin/JPA-Specification%EC%9C%BC%EB%A1%9C-%EC%BF%BC%EB%A6%AC-%EC%A1%B0%EA%B1%B4-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/CMpOG/hyGXI4xzH5/fEufKRL2owsv8F2zbHAVv0/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500,https://scrap.kakaocdn.net/dn/HSlJc/hyGXHR5zWo/YrkPADdGjWopfDpGn7kMx1/img.jpg?width=240&amp;amp;height=240&amp;amp;face=52_85_166_199');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot;&gt;JPA Specification으로 쿼리 조건 처리하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot;&gt;해당 코드는 Github에서 확인할 수 있습니다. Spring Data에서 Specification은 DB 쿼리의 조건을 Spec으로 작성해 Repository method에 적용하거나 몇가지 Spec을 조합해서 사용할 수 있게 도와줍니다. 간단한 예&lt;/p&gt;
&lt;p class=&quot;og-host&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JAVA/JPA</category>
      <category>JPA</category>
      <category>sort</category>
      <category>Spring Data JPA</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/96</guid>
      <comments>https://cla9.tistory.com/96#entry96comment</comments>
      <pubDate>Thu, 30 Jul 2020 21:25:45 +0900</pubDate>
    </item>
    <item>
      <title>1. JPA 페이징 쿼리 개선</title>
      <link>https://cla9.tistory.com/95</link>
      <description>&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;Pageable 객체를 사용하면 JPA에서 손쉽게 페이징 처리를 할 수 있습니다. 하지만 기본적으로 제공되는 findAll 메소드를 통해 페이징 처리를 요청하면, 매 요청시마다 전체 갯수를 구하기 위한 Count SQL이 수행되는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;조회 조건이 달라지지 않는 이상 매번 동일한 결과 건수가 보장될텐데, 매번 Count SQL이 수행되는 것은 꽤나 비효율 적입니다. 더불어 Count SQL을 수행하면, 페이징되는 데이터 뿐만 아니라 전체 데이터를 DBMS 메모리에 로딩해서 집계 해야하기 때문에 반복 수행시 성능 이슈가 발생할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br /&gt;물론 한번 메모리에 데이터가 로딩되면 그 이후에는 Count SQL 속도가 처음보다는 빠릅니다. 하지만 RDBMS(Oracle 기준)는 DMA(Direct Memorry Access)로 데이터에 즉시 접근할 수 없고 Latch를 획득해야하는 과정이 수반되기 때문에 여전히 Overhead가 존재합니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 CustomRepository 구현을 통해 전체 개수를 알고 있는 상황이라면, Count SQL을 요청하지 않도록 findAll 메소드를 구현하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;1. Entity&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table
@Getter
public class Customer {
    @Id
    @GeneratedValue
    @Column(name = &quot;id&quot;)
    private Long customerId;
    @Column(name = &quot;name&quot;)
    private String customerName;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;예제를 위해 엔티티는 위와같이 간단히 설정하였습니다. 조회를 위해 사전에 해당 테이블에 100건의 데이터 입력 하였다고 가정하고 진행하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;2. 반환 DTO&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Builder(access = AccessLevel.PRIVATE)
@Getter
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
public class CustomerDTO {
    private Long customerId;
    private String customerName;

    public static CustomerDTO of(Customer customer){
        return CustomerDTO.builder()
                .customerId(customer.getCustomerId())
                .customerName(customer.getCustomerName())
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;반환되는 DTO는 위와같이 구현했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;3. Repository 구현&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoRepositoryBean
public interface BaseRepository&amp;lt;T, ID extends Serializable&amp;gt; extends JpaRepository&amp;lt;T,ID&amp;gt; {
    Page&amp;lt;T&amp;gt; findAll(Pageable pageable, Integer totalElements);
    Page&amp;lt;T&amp;gt; findAll(Specification&amp;lt;T&amp;gt; spec, Pageable pageable, Integer totalElements);
    Page&amp;lt;T&amp;gt; findAll(Specification&amp;lt;T&amp;gt; spec, Pageable pageable, Integer totalElements, Sort sort);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;CustomRepository 구현을 위해 JpaRepository를 상속받는 새로운 Repository를 생성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 totalElements는 조건에 해당되는 전체 레코드 갯수로 Client가 전체 레코드 갯수를 파라미터로 전달하게 되면, 해당 값을 Page 객체에 삽입 후 Count SQL을 수행하지 않습니다. 반면 값이 전달되지 않으면 Count SQL을 수행합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;따라서, Count SQL 수행 여부는 파라미터로 전달되는 totalElements의 유무에 따라 결정됩니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BaseRepositoryImpl&amp;lt;T,ID extends Serializable&amp;gt; extends SimpleJpaRepository&amp;lt;T, ID&amp;gt; 
        implements BaseRepository&amp;lt;T,ID&amp;gt; {

    public BaseRepositoryImpl(JpaEntityInformation&amp;lt;T, ?&amp;gt; entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
    }

    @Override
    public Page&amp;lt;T&amp;gt; findAll(Pageable pageable, Integer totalElements) {
        return findAll(null, pageable, totalElements, Sort.unsorted());
    }

    @Override
    public Page&amp;lt;T&amp;gt; findAll(Specification&amp;lt;T&amp;gt; spec, Pageable pageable, Integer totalElements) {
       return findAll(spec, pageable, totalElements, Sort.unsorted());
    }

    @Override
    public Page&amp;lt;T&amp;gt; findAll(Specification&amp;lt;T&amp;gt; spec, Pageable pageable, Integer totalElements, Sort sort) {
        TypedQuery&amp;lt;T&amp;gt; query = getQuery(spec, sort);

        int pageNumber = pageable.getPageNumber();
        int pageSize = pageable.getPageSize();

        if(totalElements == null){
            return findAll(spec, pageable);
        }

        if(pageNumber &amp;lt; 0){
            throw new IllegalArgumentException(&quot;page number must not be less than zero&quot;);
        }

        if(pageSize &amp;lt; 1){
            throw new IllegalArgumentException(&quot;pageSize must not be less than one&quot;);
        }

        if(totalElements &amp;lt; pageNumber * pageSize){
            throw new IllegalArgumentException(&quot;totalElements must not be less than pageNumber * pageSize&quot;);
        }

        int offset = (int) pageable.getOffset();
        query.setFirstResult(offset);
        query.setMaxResults(pageable.getPageSize());

        return  new PageImpl&amp;lt;&amp;gt;(query.getResultList(), PageRequest.of(pageNumber, pageSize), totalElements);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위 코드는 실제 Repository 구현 코드입니다. 전달받은 파라미터에 대하여 유효성 검증 후 로직에 따라 처리합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;만약 totalElements 값이 null이 아니라면 마지막 줄과 같이 Page 객체에 전달받은 값을 그대로 넣습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface CustomerRepository extends BaseRepository&amp;lt;Customer, Long&amp;gt; {}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;실제 사용하는 Repository에는 JpaRepository가 아닌 CustomRepository를 상속받도록 지정합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
@EnableJpaRepositories(repositoryBaseClass = BaseRepositoryImpl.class)
public class PagingDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(PagingDemoApplication.class, args);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;마지막으로 등록한 Repository를 사용하기 위해서는 Configuration 클래스에 @EnableJpaRepositories 어노테이션으로 CustomRepository를 등록합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Service&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;groovy&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class CustomerService {
    private final CustomerRepository repository;

    public Page&amp;lt;CustomerDTO&amp;gt; getCustomer(Pageable pageable, Integer totalElements) {
        Page&amp;lt;Customer&amp;gt; customers = repository.findAll(pageable, totalElements);
        return customers.map(CustomerDTO::of);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;CustomRepository의 메소드를 호출하며, 리턴된 페이지객체에 대해서 DTO로 변환하는 로직을 추가하였습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Controller&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1596102671556&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class CustomerController {
    private final CustomerService service;

    @GetMapping(&quot;/customers&quot;)
    public ResponseEntity&amp;lt;Page&amp;lt;CustomerDTO&amp;gt;&amp;gt; getCustomer(Pageable pageable,
                                                         @RequestParam(required = false, name = &quot;total_elements&quot;)Integer totalElements){
        Page&amp;lt;CustomerDTO&amp;gt; customers = service.getCustomer(pageable, totalElements);
        return ResponseEntity.ok(customers);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;파라미터로 Pageable 객체이외에 totalElements를 전달받아 Service로 전달하는 역할을 수행합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;결과&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;1. totalElements 인자를 입력하지 않았을 경우&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw1OMw/btqF9mhnauS/WDlk6WXWHyJ92BVEoUZJl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw1OMw/btqF9mhnauS/WDlk6WXWHyJ92BVEoUZJl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw1OMw/btqF9mhnauS/WDlk6WXWHyJ92BVEoUZJl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw1OMw%2FbtqF9mhnauS%2FWDlk6WXWHyJ92BVEoUZJl1%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;위와 같이 Count SQL이 수행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;2. totalElements 입력 결과&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d412Tg/btqF63qBiz1/CGry7L69bJCoDBaZqWYCY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d412Tg/btqF63qBiz1/CGry7L69bJCoDBaZqWYCY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d412Tg/btqF63qBiz1/CGry7L69bJCoDBaZqWYCY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd412Tg%2FbtqF63qBiz1%2FCGry7L69bJCoDBaZqWYCY1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;페이징 처리 SQL 수행 후 리턴받은 Page 객체에 전달받은 totalElements 값이 추가되었음을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서 JPA 페이징 수행시 Count SQL을 제거하는 방법에 대해서 살펴보았습니다. 기존에는 매번 Count SQL을 수행하여 Index가 벗어났는지 등을 Server에서 확인하였다면, 지금은 totalElements에 대한 책임이 Client 측에 일부 있습니다. 따라서 조회 조건이 달라진다면, totalElements 값을 전달하지 않는 처리가 Client에서 구현되어야 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;약간의 복잡함은 있지만 비효율적인 Count SQL을 반복수행하는 것을 줄임으로 성능 향상을 꾀할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;다음 포스팅에서는 Sort와 조회조건처리 관련하여 다루도록 하겠습니다.&lt;/p&gt;</description>
      <category>JAVA/JPA</category>
      <category>Count SQL</category>
      <category>JPA</category>
      <category>JPA 페이징</category>
      <category>Pageable</category>
      <category>Spring Data JPA</category>
      <category>페이징</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/95</guid>
      <comments>https://cla9.tistory.com/95#entry95comment</comments>
      <pubDate>Wed, 29 Jul 2020 21:36:42 +0900</pubDate>
    </item>
    <item>
      <title>5. 쿠버네티스 클러스터 구축 - MetalLB 설치</title>
      <link>https://cla9.tistory.com/94</link>
      <description>&lt;h2 style=&quot;text-align: left;&quot; data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;쿠버네티스 클러스터에 존재하는 Pod 서비스를 외부로 노출시키기 위한 가장 원시적인 방법은 NodePort를 이용하는 것입니다. 하지만 NodePort는 인스턴스의 IP가 변경되면 해당 서비스에도 이를 반영해야합니다. 따라서 Cloud 벤더에서는 LoadBalancer나 Ingress 타입을 통해 서비스를 노출할 수 있도록 지원합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 온 프레미스 환경에서 LoadBalancer 타입을 지원하기 위한 MetalLB를 설치하는 방법을 다루고자 합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;문제 상황&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;1. nginx Deployment를 하나 생성합니다. (kubectl create deploy nginx --image=nginx)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bexvCE/btqFNapEBkT/6gHGrAMNwUFmD2mzUFhW31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bexvCE/btqFNapEBkT/6gHGrAMNwUFmD2mzUFhW31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bexvCE/btqFNapEBkT/6gHGrAMNwUFmD2mzUFhW31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbexvCE%2FbtqFNapEBkT%2F6gHGrAMNwUFmD2mzUFhW31%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;2. 생성한 Deployment를 LoadBalancer 타입으로 서비스를 노출시킵니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;( kubectl expose deploy nginx --port 80 --type LoadBalancer)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rb4Wn/btqFOL2PJ5O/YAI5dRWVVSXnlnpk1x5Am1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rb4Wn/btqFOL2PJ5O/YAI5dRWVVSXnlnpk1x5Am1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rb4Wn/btqFOL2PJ5O/YAI5dRWVVSXnlnpk1x5Am1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRb4Wn%2FbtqFOL2PJ5O%2FYAI5dRWVVSXnlnpk1x5Am1%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;3.&amp;nbsp; 서비스의 상태를 모니터링합니다. (watch kubectl get svc)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdUPjF/btqFNscgCbB/6dOHSuDIs1lHcEnkpXneP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdUPjF/btqFNscgCbB/6dOHSuDIs1lHcEnkpXneP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdUPjF/btqFNscgCbB/6dOHSuDIs1lHcEnkpXneP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdUPjF%2FbtqFNscgCbB%2F6dOHSuDIs1lHcEnkpXneP1%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyEx58/btqFNgiRKrQ/3l3jLCI6aced7qwbKwUuok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyEx58/btqFNgiRKrQ/3l3jLCI6aced7qwbKwUuok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyEx58/btqFNgiRKrQ/3l3jLCI6aced7qwbKwUuok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyEx58%2FbtqFNgiRKrQ%2F3l3jLCI6aced7qwbKwUuok%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;아무리 기다려도 해당 Service 오브젝트의 External-IP는 Pending 상태입니다. 이는 LoadBalancer가 존재하지 않기 때문입니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;4.&amp;nbsp; Service와 Deployment 오브젝트를 제거합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- kubectl delete svc nginx&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;- kubectl delete depoly nginx&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btjWER/btqFN3Xb9kl/QWmpmHEB9DH4yfOdYbr83K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btjWER/btqFN3Xb9kl/QWmpmHEB9DH4yfOdYbr83K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btjWER/btqFN3Xb9kl/QWmpmHEB9DH4yfOdYbr83K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtjWER%2FbtqFN3Xb9kl%2FQWmpmHEB9DH4yfOdYbr83K%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; 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;style1&quot; /&gt;
&lt;h3 style=&quot;text-align: left;&quot; data-ke-size=&quot;size23&quot;&gt;Metal LB 설치&lt;/h3&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;a href=&quot;https://metallb.universe.tf/installation/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MetalLB 공식 홈페이지&lt;/a&gt;&lt;/u&gt; 설치 가이드대로 차근차근 설치하겠습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;1. MetalLB를 위한 네임스페이스를 생성합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;(kubectl apply -f &lt;a href=&quot;https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/namespace.yaml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/namespace.yaml&lt;/a&gt;)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NIXom/btqFMHOOFjp/WhEsdkv8VOg4Ukxy3hBkuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NIXom/btqFMHOOFjp/WhEsdkv8VOg4Ukxy3hBkuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NIXom/btqFMHOOFjp/WhEsdkv8VOg4Ukxy3hBkuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNIXom%2FbtqFMHOOFjp%2FWhEsdkv8VOg4Ukxy3hBkuK%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;2. MetalLB Components를 생성합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;(kubectl apply -f &lt;a href=&quot;https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/metallb.yaml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/metallb.yaml&lt;/a&gt;)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lbdEd/btqFNPdQjNl/VeAm1mec4PfgpfyFnLTS0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lbdEd/btqFNPdQjNl/VeAm1mec4PfgpfyFnLTS0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lbdEd/btqFNPdQjNl/VeAm1mec4PfgpfyFnLTS0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlbdEd%2FbtqFNPdQjNl%2FVeAm1mec4PfgpfyFnLTS0k%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;3. Secret을 생성합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;(kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey=&quot;$(openssl rand -base64 128)&quot;)&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wDldF/btqFN2qte4M/qiur3l5AmoLd4Hx0nygQ1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wDldF/btqFN2qte4M/qiur3l5AmoLd4Hx0nygQ1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wDldF/btqFN2qte4M/qiur3l5AmoLd4Hx0nygQ1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwDldF%2FbtqFN2qte4M%2Fqiur3l5AmoLd4Hx0nygQ1k%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;4. 라우팅 처리를 위한 ConfigMap을 생성합니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: default
      protocol: layer2
      addresses:
      - 192.168.72.102-192.168.72.103&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjwHGG/btqFNrR1PTk/M6QrmYmi998GxV24PzxDWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjwHGG/btqFNrR1PTk/M6QrmYmi998GxV24PzxDWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjwHGG/btqFNrR1PTk/M6QrmYmi998GxV24PzxDWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjwHGG%2FbtqFNrR1PTk%2FM6QrmYmi998GxV24PzxDWK%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;5. 기존과 마찬가지로 nginx를 Deploy한 다음 LoadBalancer 타입으로 노출시킨 결과 이전과는 다르게 nginx Service 오브젝트의 External IP가 할당된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpx1II/btqFNPx7y2Q/bodAWqbAeK78MUKfvjfC3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpx1II/btqFNPx7y2Q/bodAWqbAeK78MUKfvjfC3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpx1II/btqFNPx7y2Q/bodAWqbAeK78MUKfvjfC3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbpx1II%2FbtqFNPx7y2Q%2FbodAWqbAeK78MUKfvjfC3K%2Fimg.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Cloud/Kubernetes</category>
      <category>docker</category>
      <category>Kubernetes</category>
      <category>metallb</category>
      <category>쿠버네티스</category>
      <category>쿠버네티스 설치</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/94</guid>
      <comments>https://cla9.tistory.com/94#entry94comment</comments>
      <pubDate>Sat, 18 Jul 2020 21:35:23 +0900</pubDate>
    </item>
    <item>
      <title>4. 쿠버네티스 클러스터 구축 - NFS 설치</title>
      <link>https://cla9.tistory.com/93</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot; style=&quot;text-align: left;&quot;&gt;서론&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;u&gt;&lt;a href=&quot;https://cla9.tistory.com/92?category=814452&quot; target=&quot;_blank&quot;&gt;이전 포스팅&lt;/a&gt;&lt;/u&gt;을 통해 VirtualBox에서 쿠버네티스 클러스터를 구축하는 방법을 살펴봤습니다. 이번 포스팅에서는 클러스터내 Pod 끼리 디스크를 공유해야하는 경우 사용되는 Persistent Volume 유형 중 NFS를 구성하는 방법에 대해서 살펴보겠습니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;쿠버네티스 Persistent Volume 지원 항목&lt;/p&gt;&lt;ul style=&quot;list-style-type: disc;&quot;&gt;&lt;li&gt;GCEPersistentDisk&lt;/li&gt;&lt;li&gt;AWSElasticBlockStore&lt;/li&gt;&lt;li&gt;AzureFile&lt;/li&gt;&lt;li&gt;AzureDisk&lt;/li&gt;&lt;li&gt;CSI&lt;/li&gt;&lt;li&gt;FC (파이버 채널)&lt;/li&gt;&lt;li&gt;FlexVolume&lt;/li&gt;&lt;li&gt;Flocker&lt;/li&gt;&lt;li&gt;&lt;u&gt;&lt;b&gt;NFS&lt;/b&gt;&lt;/u&gt;&lt;/li&gt;&lt;li&gt;iSCSI&lt;/li&gt;&lt;li&gt;RBD (Ceph Block Device)&lt;/li&gt;&lt;li&gt;CephFS&lt;/li&gt;&lt;li&gt;Cinder (OpenStack 블록 스토리지)&lt;/li&gt;&lt;li&gt;Glusterfs&lt;/li&gt;&lt;li&gt;VsphereVolume&lt;/li&gt;&lt;li&gt;Quobyte Volumes&lt;/li&gt;&lt;li&gt;HostPath (단일 노드 테스트 전용 – 로컬 스토리지는 어떤 방식으로도 지원되지 않으며 다중-노드 클러스터에서 작동하지 않음)&lt;/li&gt;&lt;li&gt;Portworx Volumes&lt;/li&gt;&lt;li&gt;ScaleIO Volumes&lt;/li&gt;&lt;li&gt;StorageOS&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h3 data-ke-size=&quot;size23&quot; style=&quot;text-align: left;&quot;&gt;1. NFS 서버 설치&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;NFS 구축을 위해서는 서버를 먼저 설치해야합니다. 보통 별도 서버를 띄운다음에 구축하는 것이 보편적이지만 편의상 Work1 노드에 NFS 서버를 구축하겠습니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;1.&amp;nbsp; Work1 노드에서 root 계정으로 접속합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/dmETPH/btqFOMUWdpj/GJ7HCL66sMQ1D1sikZC5C0/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmETPH/btqFOMUWdpj/GJ7HCL66sMQ1D1sikZC5C0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmETPH/btqFOMUWdpj/GJ7HCL66sMQ1D1sikZC5C0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmETPH/btqFOMUWdpj/GJ7HCL66sMQ1D1sikZC5C0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmETPH%2FbtqFOMUWdpj%2FGJ7HCL66sMQ1D1sikZC5C0%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/dmETPH/btqFOMUWdpj/GJ7HCL66sMQ1D1sikZC5C0/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;2.&amp;nbsp; update를 수행합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/dQaeWi/btqFNPx3sUw/9WGc1hX7om00uFr8h553dk/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQaeWi/btqFNPx3sUw/9WGc1hX7om00uFr8h553dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQaeWi/btqFNPx3sUw/9WGc1hX7om00uFr8h553dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQaeWi/btqFNPx3sUw/9WGc1hX7om00uFr8h553dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQaeWi%2FbtqFNPx3sUw%2F9WGc1hX7om00uFr8h553dk%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/dQaeWi/btqFNPx3sUw/9WGc1hX7om00uFr8h553dk/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;3.&amp;nbsp; NFS 서버를 위한 패키지 프로그램을 설치합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/NfI2Y/btqFMHOKBg0/TPMCQFabF39uKRFjSEToek/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NfI2Y/btqFMHOKBg0/TPMCQFabF39uKRFjSEToek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NfI2Y/btqFMHOKBg0/TPMCQFabF39uKRFjSEToek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NfI2Y/btqFMHOKBg0/TPMCQFabF39uKRFjSEToek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNfI2Y%2FbtqFMHOKBg0%2FTPMCQFabF39uKRFjSEToek%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/NfI2Y/btqFMHOKBg0/TPMCQFabF39uKRFjSEToek/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;4.&amp;nbsp; 공유 폴더를 새로 만듭니다. (mkdir /home/share/nfs -p)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/dTqiej/btqFOht6Nsj/zOqY5l9pd3oYsLfV9QFYsk/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTqiej/btqFOht6Nsj/zOqY5l9pd3oYsLfV9QFYsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTqiej/btqFOht6Nsj/zOqY5l9pd3oYsLfV9QFYsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTqiej/btqFOht6Nsj/zOqY5l9pd3oYsLfV9QFYsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTqiej%2FbtqFOht6Nsj%2FzOqY5l9pd3oYsLfV9QFYsk%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/dTqiej/btqFOht6Nsj/zOqY5l9pd3oYsLfV9QFYsk/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;5. 폴더 권한을 부여합니다. (chmod 777 &lt;span style=&quot;color: rgb(51, 51, 51);&quot;&gt;/home/share/nfs&lt;/span&gt;)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/LMeJF/btqFNE4ACIr/w0OrSnZcFcv3Pov5LbKYHk/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LMeJF/btqFNE4ACIr/w0OrSnZcFcv3Pov5LbKYHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LMeJF/btqFNE4ACIr/w0OrSnZcFcv3Pov5LbKYHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LMeJF/btqFNE4ACIr/w0OrSnZcFcv3Pov5LbKYHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLMeJF%2FbtqFNE4ACIr%2Fw0OrSnZcFcv3Pov5LbKYHk%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/LMeJF/btqFNE4ACIr/w0OrSnZcFcv3Pov5LbKYHk/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;6. exports 파일을 엽니다. (vi /etc/exports)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/cIRn9K/btqFNswxIjG/pQVS8pvSiyaPErEETWhCg0/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIRn9K/btqFNswxIjG/pQVS8pvSiyaPErEETWhCg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIRn9K/btqFNswxIjG/pQVS8pvSiyaPErEETWhCg0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIRn9K/btqFNswxIjG/pQVS8pvSiyaPErEETWhCg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIRn9K%2FbtqFNswxIjG%2FpQVS8pvSiyaPErEETWhCg0%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/cIRn9K/btqFNswxIjG/pQVS8pvSiyaPErEETWhCg0/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;7. 공유폴더의 허용 host 및 권한을 설정합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;( &lt;span style=&quot;color: rgb(51, 51, 51);&quot;&gt;/home/share/nfs&lt;/span&gt; &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;192.168.72.101(rw,sync,no_subtree_check) 192.168.72.102(rw,sync,no_subtree_check) 192.168.72.103(rw,sync,no_subtree_check) )&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/cBbFka/btqFNQDKh0K/9n2kQwx0AVXP63fcxLdyW1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cBbFka/btqFNQDKh0K/9n2kQwx0AVXP63fcxLdyW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cBbFka/btqFNQDKh0K/9n2kQwx0AVXP63fcxLdyW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cBbFka/btqFNQDKh0K/9n2kQwx0AVXP63fcxLdyW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcBbFka%2FbtqFNQDKh0K%2F9n2kQwx0AVXP63fcxLdyW1%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/cBbFka/btqFNQDKh0K/9n2kQwx0AVXP63fcxLdyW1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;8. nfs-server를 재기동합니다.(service nfs-server restart)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/cengPi/btqFMIz50N5/UGBHawqBgYVZKuBKnykOek/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cengPi/btqFMIz50N5/UGBHawqBgYVZKuBKnykOek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cengPi/btqFMIz50N5/UGBHawqBgYVZKuBKnykOek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cengPi/btqFMIz50N5/UGBHawqBgYVZKuBKnykOek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcengPi%2FbtqFMIz50N5%2FUGBHawqBgYVZKuBKnykOek%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/cengPi/btqFMIz50N5/UGBHawqBgYVZKuBKnykOek/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;9. nfs-server 상태를 확인합니다.(&lt;span style=&quot;color: rgb(51, 51, 51);&quot;&gt;systemctl status nfs-server.service)&lt;/span&gt;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/b2G9le/btqFMHBdGrk/WZVy8TNFadZgWDUIkwlnIk/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2G9le/btqFMHBdGrk/WZVy8TNFadZgWDUIkwlnIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2G9le/btqFMHBdGrk/WZVy8TNFadZgWDUIkwlnIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2G9le/btqFMHBdGrk/WZVy8TNFadZgWDUIkwlnIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2G9le%2FbtqFMHBdGrk%2FWZVy8TNFadZgWDUIkwlnIk%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/b2G9le/btqFMHBdGrk/WZVy8TNFadZgWDUIkwlnIk/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;10. mount 목록을 확인하여 정상 반영되었는지 체크합니다. (showmount -e 127.0.0.1)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/mrfHz/btqFMHBdWci/yZGdmkKGs5JQXLIY5gb4f1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mrfHz/btqFMHBdWci/yZGdmkKGs5JQXLIY5gb4f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mrfHz/btqFMHBdWci/yZGdmkKGs5JQXLIY5gb4f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mrfHz/btqFMHBdWci/yZGdmkKGs5JQXLIY5gb4f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmrfHz%2FbtqFMHBdWci%2FyZGdmkKGs5JQXLIY5gb4f1%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/mrfHz/btqFMHBdWci/yZGdmkKGs5JQXLIY5gb4f1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;11.&amp;nbsp; 공유 폴더를 /mnt로 마운트 시킵니다. (mount -t nfs 192.168.72.102:/home/share/nfs /mnt)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/dfdkQ5/btqFMHVwna5/jeL017CoNXObkOenJXgiR1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfdkQ5/btqFMHVwna5/jeL017CoNXObkOenJXgiR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfdkQ5/btqFMHVwna5/jeL017CoNXObkOenJXgiR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfdkQ5/btqFMHVwna5/jeL017CoNXObkOenJXgiR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfdkQ5%2FbtqFMHVwna5%2FjeL017CoNXObkOenJXgiR1%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/dfdkQ5/btqFMHVwna5/jeL017CoNXObkOenJXgiR1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;12. 정상적으로 마운트되었는지 확인하기 위해 공유 폴더에 테스트 파일을 만듭니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;(echo test &amp;gt;&amp;gt; /home/share/nfs/test.txt)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/dtZYGT/btqFOXPufVS/U2qm6JzMjlakcyVFpBwUw1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtZYGT/btqFOXPufVS/U2qm6JzMjlakcyVFpBwUw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtZYGT/btqFOXPufVS/U2qm6JzMjlakcyVFpBwUw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtZYGT/btqFOXPufVS/U2qm6JzMjlakcyVFpBwUw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtZYGT%2FbtqFOXPufVS%2FU2qm6JzMjlakcyVFpBwUw1%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/dtZYGT/btqFOXPufVS/U2qm6JzMjlakcyVFpBwUw1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;13.&amp;nbsp; 마운트 위치에있는 파일을 읽어 정상 동작 여부를 확인합니다. (cat /mnt/test.txt)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/HLQSR/btqFOWJOlEr/ORqrTBfPExpjLuK3K0vqU1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HLQSR/btqFOWJOlEr/ORqrTBfPExpjLuK3K0vqU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HLQSR/btqFOWJOlEr/ORqrTBfPExpjLuK3K0vqU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HLQSR/btqFOWJOlEr/ORqrTBfPExpjLuK3K0vqU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHLQSR%2FbtqFOWJOlEr%2FORqrTBfPExpjLuK3K0vqU1%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/HLQSR/btqFOWJOlEr/ORqrTBfPExpjLuK3K0vqU1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;14. 임시 파일을 삭제합니다. (rm /mnt/test.txt)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/b7tvKl/btqFOXopSm2/2su7RqvQlMKLjwkJq8QET0/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7tvKl/btqFOXopSm2/2su7RqvQlMKLjwkJq8QET0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7tvKl/btqFOXopSm2/2su7RqvQlMKLjwkJq8QET0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7tvKl/btqFOXopSm2/2su7RqvQlMKLjwkJq8QET0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7tvKl%2FbtqFOXopSm2%2F2su7RqvQlMKLjwkJq8QET0%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/b7tvKl/btqFOXopSm2/2su7RqvQlMKLjwkJq8QET0/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;15. Master 노드의 root 계정으로 접속합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/IoAM8/btqFOYARtMi/rILF3ff9lLG4KkNI0WOjG0/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IoAM8/btqFOYARtMi/rILF3ff9lLG4KkNI0WOjG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IoAM8/btqFOYARtMi/rILF3ff9lLG4KkNI0WOjG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IoAM8/btqFOYARtMi/rILF3ff9lLG4KkNI0WOjG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIoAM8%2FbtqFOYARtMi%2FrILF3ff9lLG4KkNI0WOjG0%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/IoAM8/btqFOYARtMi/rILF3ff9lLG4KkNI0WOjG0/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;16. update를 수행합니다. (apt update)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/Rec5j/btqFNFP4BxI/aeXP9ObNjFBVOChiGbxynK/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Rec5j/btqFNFP4BxI/aeXP9ObNjFBVOChiGbxynK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Rec5j/btqFNFP4BxI/aeXP9ObNjFBVOChiGbxynK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Rec5j/btqFNFP4BxI/aeXP9ObNjFBVOChiGbxynK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRec5j%2FbtqFNFP4BxI%2FaeXP9ObNjFBVOChiGbxynK%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/Rec5j/btqFNFP4BxI/aeXP9ObNjFBVOChiGbxynK/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;17. NFS 관련 패키지 프로그램을 설치합니다.(apt install nfs-common nfs-kernel-server portmap)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/qqBD7/btqFN241rJx/xiCejcjImbMF96v2yPGGU1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qqBD7/btqFN241rJx/xiCejcjImbMF96v2yPGGU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qqBD7/btqFN241rJx/xiCejcjImbMF96v2yPGGU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qqBD7/btqFN241rJx/xiCejcjImbMF96v2yPGGU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqqBD7%2FbtqFN241rJx%2FxiCejcjImbMF96v2yPGGU1%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/qqBD7/btqFN241rJx/xiCejcjImbMF96v2yPGGU1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;18. Work2 노드에서 root 계정으로 접속합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/cwWyAC/btqFOMm7J3u/BVe3aYNODyw0UrRr1yH9V1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwWyAC/btqFOMm7J3u/BVe3aYNODyw0UrRr1yH9V1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwWyAC/btqFOMm7J3u/BVe3aYNODyw0UrRr1yH9V1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwWyAC/btqFOMm7J3u/BVe3aYNODyw0UrRr1yH9V1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwWyAC%2FbtqFOMm7J3u%2FBVe3aYNODyw0UrRr1yH9V1%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/cwWyAC/btqFOMm7J3u/BVe3aYNODyw0UrRr1yH9V1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;19. update를 수행합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/cgFCgd/btqFNrqUtj9/sWAkbd1k1qGZbShOLy2WQk/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgFCgd/btqFNrqUtj9/sWAkbd1k1qGZbShOLy2WQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgFCgd/btqFNrqUtj9/sWAkbd1k1qGZbShOLy2WQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgFCgd/btqFNrqUtj9/sWAkbd1k1qGZbShOLy2WQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgFCgd%2FbtqFNrqUtj9%2FsWAkbd1k1qGZbShOLy2WQk%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/cgFCgd/btqFNrqUtj9/sWAkbd1k1qGZbShOLy2WQk/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;20. NFS 관련 패키지 프로그램을 설치합니다.(apt install nfs-common nfs-kernel-server portmap)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/GbXDN/btqFOis3lSz/x3o8fAfrnhxGA6dkDYxn20/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GbXDN/btqFOis3lSz/x3o8fAfrnhxGA6dkDYxn20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GbXDN/btqFOis3lSz/x3o8fAfrnhxGA6dkDYxn20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GbXDN/btqFOis3lSz/x3o8fAfrnhxGA6dkDYxn20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGbXDN%2FbtqFOis3lSz%2Fx3o8fAfrnhxGA6dkDYxn20%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/GbXDN/btqFOis3lSz/x3o8fAfrnhxGA6dkDYxn20/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot; style=&quot;text-align: left;&quot;&gt;2. Persistent Volume 테스트&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;u&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistent-volumes&quot; target=&quot;_blank&quot;&gt;쿠버네티스 공식 홈페이지&lt;/a&gt;&lt;/u&gt;에 있는 예제를 활용하여 NFS를 활용한 공유디스크 설정 테스트를 진행합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;1. PersistentVolume 테스트를 위한 yaml 파일을 생성합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/wXXWj/btqFOjeteUB/OQ5s8NA4V4vEHXf4UkUp00/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wXXWj/btqFOjeteUB/OQ5s8NA4V4vEHXf4UkUp00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wXXWj/btqFOjeteUB/OQ5s8NA4V4vEHXf4UkUp00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wXXWj/btqFOjeteUB/OQ5s8NA4V4vEHXf4UkUp00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwXXWj%2FbtqFOjeteUB%2FOQ5s8NA4V4vEHXf4UkUp00%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/wXXWj/btqFOjeteUB/OQ5s8NA4V4vEHXf4UkUp00/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;2.&amp;nbsp; 아래 내용을 복사 &amp;amp; 붙여넣기 합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;pre data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;apiVersion: v1
kind: PersistentVolume
metadata:
  name: my-pv
spec:
  capacity:
    storage: 10Mi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    path: /home/share/nfs
    server: 192.168.72.102&lt;/pre&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;3. PersistentVolume을 생성합니다. (kubectl create -f pv.yaml)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/bBeMOW/btqFMHuuQ6u/9QW43MiE1XRXNKaX0OhoP1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBeMOW/btqFMHuuQ6u/9QW43MiE1XRXNKaX0OhoP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBeMOW/btqFMHuuQ6u/9QW43MiE1XRXNKaX0OhoP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBeMOW/btqFMHuuQ6u/9QW43MiE1XRXNKaX0OhoP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBeMOW%2FbtqFMHuuQ6u%2F9QW43MiE1XRXNKaX0OhoP1%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/bBeMOW/btqFMHuuQ6u/9QW43MiE1XRXNKaX0OhoP1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;4. PersistentVolume 상태를 확인합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/byhzxC/btqFN2D0lhI/lSbeMKqs938MCQ9J9GGrA1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byhzxC/btqFN2D0lhI/lSbeMKqs938MCQ9J9GGrA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byhzxC/btqFN2D0lhI/lSbeMKqs938MCQ9J9GGrA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byhzxC/btqFN2D0lhI/lSbeMKqs938MCQ9J9GGrA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyhzxC%2FbtqFN2D0lhI%2FlSbeMKqs938MCQ9J9GGrA1%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/byhzxC/btqFN2D0lhI/lSbeMKqs938MCQ9J9GGrA1/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;위와같이 Status가 Available이면 정상입니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;5. 초기화를 위해 PersistentVolume을 삭제합니다. (kubectl delete -f pv.yaml)&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/bCZ2Qp/btqFNPx5R0t/mHBR0yYW5quq3436sXcVVK/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCZ2Qp/btqFNPx5R0t/mHBR0yYW5quq3436sXcVVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCZ2Qp/btqFNPx5R0t/mHBR0yYW5quq3436sXcVVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCZ2Qp/btqFNPx5R0t/mHBR0yYW5quq3436sXcVVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCZ2Qp%2FbtqFNPx5R0t%2FmHBR0yYW5quq3436sXcVVK%2Fimg.png&quot; data-image-src=&quot;https://k.kakaocdn.net/dn/bCZ2Qp/btqFNPx5R0t/mHBR0yYW5quq3436sXcVVK/img.png&quot; data-origin-width=&quot;0.0&quot; data-origin-height=&quot;0.0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot; style=&quot;text-align: left;&quot;&gt;마치며&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;Stateful 애플리케이션을 사용하기 위해서는 Volume 설정이 필요합니다. NFS는 Persistent Volume을 지원하는 한 종류로서 공유 디스크 할당이 가능합니다.&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;다음 포스팅에서는 LoadBalancer를 위한 MetalLB 설치과정을 다루겠습니다.&lt;/p&gt;</description>
      <category>Cloud/Kubernetes</category>
      <category>docker</category>
      <category>k8s</category>
      <category>Kubernetes</category>
      <category>Kubernetes 설치</category>
      <category>NFS 설치</category>
      <category>Persistent Volume</category>
      <category>쿠버네티스</category>
      <category>쿠버네티스 설치</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/93</guid>
      <comments>https://cla9.tistory.com/93#entry93comment</comments>
      <pubDate>Sat, 18 Jul 2020 20:32:54 +0900</pubDate>
    </item>
    <item>
      <title>3. 쿠버네티스 클러스터 구축 - Docker &amp;amp; 쿠버네티스 클러스터 설치</title>
      <link>https://cla9.tistory.com/92</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cla9.tistory.com/91?category=814452&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;이전 포스팅&lt;/u&gt;&lt;/a&gt;에서 Ubuntu 설치 및 클러스터 구성을 위한 환경 설정을 진행했습니다. 이번 포스팅에서는 본격적으로 Docker 및 쿠버네티스를 설치하는 과정을 다루겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Docker 설치&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. root 계정으로 접속합니다. (sudo -i)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKCHtr/btqFN3pdmG9/llZS9pbap7BfMrfACJ5b31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKCHtr/btqFN3pdmG9/llZS9pbap7BfMrfACJ5b31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKCHtr/btqFN3pdmG9/llZS9pbap7BfMrfACJ5b31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKCHtr%2FbtqFN3pdmG9%2FllZS9pbap7BfMrfACJ5b31%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. Docker를 설치합니다. (apt install docker.io)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uznrp/btqFMItjUWR/volRdfp5oeY8sVGO7TW5Wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uznrp/btqFMItjUWR/volRdfp5oeY8sVGO7TW5Wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uznrp/btqFMItjUWR/volRdfp5oeY8sVGO7TW5Wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuznrp%2FbtqFMItjUWR%2FvolRdfp5oeY8sVGO7TW5Wk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3. Docker 상태를 확인합니다. (docker ps)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nhyAr/btqFNaC5xNB/9dsX6pwsD4K4hoGMY9atEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nhyAr/btqFNaC5xNB/9dsX6pwsD4K4hoGMY9atEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nhyAr/btqFNaC5xNB/9dsX6pwsD4K4hoGMY9atEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnhyAr%2FbtqFNaC5xNB%2F9dsX6pwsD4K4hoGMY9atEK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4. nginx 이미지를 실행해서 정상적으로 동작하는지 확인해봅니다.&lt;/p&gt;
&lt;p&gt;(docker run -d -p 80:80 --rm --name nginx nginx)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cceNE0/btqFNEcoDRM/LG8Z6MLmptL6237akDIF9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cceNE0/btqFNEcoDRM/LG8Z6MLmptL6237akDIF9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cceNE0/btqFNEcoDRM/LG8Z6MLmptL6237akDIF9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcceNE0%2FbtqFNEcoDRM%2FLG8Z6MLmptL6237akDIF9K%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;5.&amp;nbsp; Safari를 실행하여 nginx 도커 이미지가 정상적으로 기동되었는지 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uwJRf/btqFOiTZDUt/PssBfULkF6diC8acKCAvt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uwJRf/btqFOiTZDUt/PssBfULkF6diC8acKCAvt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uwJRf/btqFOiTZDUt/PssBfULkF6diC8acKCAvt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuwJRf%2FbtqFOiTZDUt%2FPssBfULkF6diC8acKCAvt1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;6. 생성한 Docker 컨테이너를 정리합니다. (docker stop nginx)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w5THU/btqFOil9R96/3din50fWYcQZ3cK9H7U95k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w5THU/btqFOil9R96/3din50fWYcQZ3cK9H7U95k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w5THU/btqFOil9R96/3din50fWYcQZ3cK9H7U95k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw5THU%2FbtqFOil9R96%2F3din50fWYcQZ3cK9H7U95k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;7. nginx 이미지를 삭제합니다. (docker rmi nginx)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xJjDu/btqFOht0oGI/XLj05RopvUgZrPM1x22dC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xJjDu/btqFOht0oGI/XLj05RopvUgZrPM1x22dC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xJjDu/btqFOht0oGI/XLj05RopvUgZrPM1x22dC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxJjDu%2FbtqFOht0oGI%2FXLj05RopvUgZrPM1x22dC1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 쿠버네티스 설치&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;쿠버네티스 공식 홈페이지&lt;/u&gt;&lt;/a&gt;에서 제공하는 방법으로 설치를 진행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;설치에 앞서 공식홈페이지에서 요구하는 요구사항을 살펴봅시다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2 GB or more of RAM per machine (any less will leave little room for your apps)&lt;/li&gt;
&lt;li&gt;2 CPUs or more&lt;/li&gt;
&lt;li&gt;Full network connectivity between all machines in the cluster (public or private network is fine)&lt;/li&gt;
&lt;li&gt;Unique hostname, MAC address, and product_uuid for every node. See&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#verify-mac-address&quot;&gt;here&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;for more details.&lt;/li&gt;
&lt;li&gt;Certain ports are open on your machines. See&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#check-required-ports&quot;&gt;here&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;for more details.&lt;/li&gt;
&lt;li&gt;&lt;u&gt;Swap disabled. You&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MUST&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;disable swap in order for the kubelet to work properly&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기서 마지막 줄을 보면, Swap을 비활성화 해야됩니다. 그 이유는 Swap 기능은 본래 가용된 메모리보다 더 큰 메모리 할당을 가능하도록 하기 위함인데, 쿠버네티스 철학은 주어진 인스턴스의 자원을 100% 가깝게 사용하는 것이 목표이기 때문에 부합되지 않습니다. 따라서 성능을 제대로 사용하기 위해서 Swap 기능을 비활성화 해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Swap 중지하기 (swapoff -a)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBlSGd/btqFMIfIwZu/2hXIYzNR7IKiYcURA88wQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBlSGd/btqFMIfIwZu/2hXIYzNR7IKiYcURA88wQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBlSGd/btqFMIfIwZu/2hXIYzNR7IKiYcURA88wQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBlSGd%2FbtqFMIfIwZu%2F2hXIYzNR7IKiYcURA88wQK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. 재부팅되면 초기화 되므로 완전하게 중지시킵니다. (sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c9UTyP/btqFNapxIXh/1yPp5v2JJ5KitqbkdiPOAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c9UTyP/btqFNapxIXh/1yPp5v2JJ5KitqbkdiPOAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c9UTyP/btqFNapxIXh/1yPp5v2JJ5KitqbkdiPOAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc9UTyP%2FbtqFNapxIXh%2F1yPp5v2JJ5KitqbkdiPOAK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3. 공식 홈페이지에 있는 Kubeadm 설치 명령어를 복사합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;명령어&lt;/p&gt;
&lt;pre id=&quot;code_1595078430811&quot; class=&quot;html xml&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; sudo apt-get install -y apt-transport-https curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
cat &amp;lt;&amp;lt;EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4.&amp;nbsp; 명령어 실행을 위하여 shell 파일을 만듭니다. (vi kubeadm-install.sh)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/neg3Y/btqFOh1RR6I/M3I5DbKCykIphvQ5hJf4ZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/neg3Y/btqFOh1RR6I/M3I5DbKCykIphvQ5hJf4ZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/neg3Y/btqFOh1RR6I/M3I5DbKCykIphvQ5hJf4ZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fneg3Y%2FbtqFOh1RR6I%2FM3I5DbKCykIphvQ5hJf4ZK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;5. 공식 홈페이지에서 복사한 명령어를 붙여넣기 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVsQ7d/btqFNaC7rbE/7qNXYhfTpBATnjCjd0Wqkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVsQ7d/btqFNaC7rbE/7qNXYhfTpBATnjCjd0Wqkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVsQ7d/btqFNaC7rbE/7qNXYhfTpBATnjCjd0Wqkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVsQ7d%2FbtqFNaC7rbE%2F7qNXYhfTpBATnjCjd0Wqkk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 명령어를 통해서 kubeadm kubelect kubectl이 설치됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;6. Shell 파일을 실행합니다. (sh kubeadm-install.sh)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o8T7f/btqFLuB4r5t/nQgrNbackI7kAp2yq9q2Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o8T7f/btqFLuB4r5t/nQgrNbackI7kAp2yq9q2Gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o8T7f/btqFLuB4r5t/nQgrNbackI7kAp2yq9q2Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo8T7f%2FbtqFLuB4r5t%2FnQgrNbackI7kAp2yq9q2Gk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;7. kubeadm이 정상적으로 설치되었는지 확인합니다. (kubeadm version)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bI2OoM/btqFN3pftPI/LMgqBionEFsXz9df5JKnuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bI2OoM/btqFN3pftPI/LMgqBionEFsXz9df5JKnuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bI2OoM/btqFN3pftPI/LMgqBionEFsXz9df5JKnuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbI2OoM%2FbtqFN3pftPI%2FLMgqBionEFsXz9df5JKnuK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;8. 마스터 노드를 중지합니다. (halt)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRa3vr/btqFNgC54g3/UhKoaPIVP2mU2D73uRrMi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRa3vr/btqFNgC54g3/UhKoaPIVP2mU2D73uRrMi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRa3vr/btqFNgC54g3/UhKoaPIVP2mU2D73uRrMi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRa3vr%2FbtqFNgC54g3%2FUhKoaPIVP2mU2D73uRrMi0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 클러스터 구축&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. 쿠버네티스 VM 복사 전에 스냅샷을 찍습니다.( 나중에 문제생길 시 롤백 용도)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vyub5/btqFN2cNCAA/z2Pcu8fCuVepCkVcHcRvSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vyub5/btqFN2cNCAA/z2Pcu8fCuVepCkVcHcRvSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vyub5/btqFN2cNCAA/z2Pcu8fCuVepCkVcHcRvSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvyub5%2FbtqFN2cNCAA%2Fz2Pcu8fCuVepCkVcHcRvSk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. Worker 노드 복사를 위해 Master VM 선택 후 마우스 오른쪽 클릭 &amp;gt; 복제 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LFZgO/btqFLtpB1wk/yweKcFEaxNrDcAKZa1SZWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LFZgO/btqFLtpB1wk/yweKcFEaxNrDcAKZa1SZWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LFZgO/btqFLtpB1wk/yweKcFEaxNrDcAKZa1SZWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLFZgO%2FbtqFLtpB1wk%2FyweKcFEaxNrDcAKZa1SZWK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3.&amp;nbsp; 복제할 가상머신의 이름과 저장 경로를 설정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2FadX/btqFOimcjb3/7QpJ9K9Y8QHPcno3RBLVBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2FadX/btqFOimcjb3/7QpJ9K9Y8QHPcno3RBLVBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2FadX/btqFOimcjb3/7QpJ9K9Y8QHPcno3RBLVBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2FadX%2FbtqFOimcjb3%2F7QpJ9K9Y8QHPcno3RBLVBk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4. MAC 주소 정책에서 모든 네트워크 어댑터의 새 MAC 주소 생성 버튼을 클릭합니다. 이후 다음 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blSVIW/btqFONlW8vH/khLW8dmv0mBgCPhL8ah1C1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blSVIW/btqFONlW8vH/khLW8dmv0mBgCPhL8ah1C1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blSVIW/btqFONlW8vH/khLW8dmv0mBgCPhL8ah1C1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblSVIW%2FbtqFONlW8vH%2FkhLW8dmv0mBgCPhL8ah1C1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;5. 완전한 복제를 선택후 다음 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rYTda/btqFLt4d2xS/SlGQmdtSJGPcsIa5EiVhf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rYTda/btqFLt4d2xS/SlGQmdtSJGPcsIa5EiVhf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rYTda/btqFLt4d2xS/SlGQmdtSJGPcsIa5EiVhf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrYTda%2FbtqFLt4d2xS%2FSlGQmdtSJGPcsIa5EiVhf0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;6. 복제 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bz0gVV/btqFNapyjwo/aOSkdugK1N1wH9aqgAbNbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bz0gVV/btqFNapyjwo/aOSkdugK1N1wH9aqgAbNbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bz0gVV/btqFNapyjwo/aOSkdugK1N1wH9aqgAbNbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbz0gVV%2FbtqFNapyjwo%2FaOSkdugK1N1wH9aqgAbNbK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;7. 1~6 번과정을 동일하게 반복하여 Work2 가상 머신을 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRqMqC/btqFNrxBXT1/ArDfk0lzzKXI9mLlId8UF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRqMqC/btqFNrxBXT1/ArDfk0lzzKXI9mLlId8UF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRqMqC/btqFNrxBXT1/ArDfk0lzzKXI9mLlId8UF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRqMqC%2FbtqFNrxBXT1%2FArDfk0lzzKXI9mLlId8UF0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;8. work1 가상 머신을 기동 후 터미널을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dd9x9x/btqFNQjolE6/rBHfTL21489s6I4VPo0WPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dd9x9x/btqFNQjolE6/rBHfTL21489s6I4VPo0WPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dd9x9x/btqFNQjolE6/rBHfTL21489s6I4VPo0WPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdd9x9x%2FbtqFNQjolE6%2FrBHfTL21489s6I4VPo0WPK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;9. root로 접속합니다. (sudo -i)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjQSeS/btqFN2DVyrY/smCJhcKYX7lIzSj9uHywhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjQSeS/btqFN2DVyrY/smCJhcKYX7lIzSj9uHywhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjQSeS/btqFN2DVyrY/smCJhcKYX7lIzSj9uHywhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjQSeS%2FbtqFN2DVyrY%2FsmCJhcKYX7lIzSj9uHywhK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;10. hostname을 변경합니다. (hostnamectl set-hostname node01)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dyhax5/btqFNaQFabt/RwVknK5a6oX1o3RoBToKu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dyhax5/btqFNaQFabt/RwVknK5a6oX1o3RoBToKu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dyhax5/btqFNaQFabt/RwVknK5a6oX1o3RoBToKu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdyhax5%2FbtqFNaQFabt%2FRwVknK5a6oX1o3RoBToKu0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;11.&amp;nbsp; 고정 IP 설정을 위해 설정 파일 위치로 이동합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqD2YV/btqFNPSjT9w/QGyWoVQdfO8qzWWKmqutVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqD2YV/btqFNPSjT9w/QGyWoVQdfO8qzWWKmqutVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqD2YV/btqFNPSjT9w/QGyWoVQdfO8qzWWKmqutVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqD2YV%2FbtqFNPSjT9w%2FQGyWoVQdfO8qzWWKmqutVk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;12.&amp;nbsp; 설정 파일을 오픈합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lE0gd/btqFM9YwazF/LhKRbOQbLhhCkiQQh4AidK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lE0gd/btqFM9YwazF/LhKRbOQbLhhCkiQQh4AidK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lE0gd/btqFM9YwazF/LhKRbOQbLhhCkiQQh4AidK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlE0gd%2FbtqFM9YwazF%2FLhKRbOQbLhhCkiQQh4AidK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;13. node01의 IP를 변경합니다. (&lt;b&gt;AS-IS&lt;/b&gt; 192.168.72.101, &lt;b&gt;TO-BE&lt;/b&gt; : 192.168.72.102)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfLIJs/btqFNrdk5Cx/P4oS0gOxTUO5WD65S8SuD1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfLIJs/btqFNrdk5Cx/P4oS0gOxTUO5WD65S8SuD1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfLIJs/btqFNrdk5Cx/P4oS0gOxTUO5WD65S8SuD1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfLIJs%2FbtqFNrdk5Cx%2FP4oS0gOxTUO5WD65S8SuD1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;14. reboot을 수행합니다. (reboot)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R7Whx/btqFN151GN9/tRO1Dt34IEPi1jPAXQ3zmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R7Whx/btqFN151GN9/tRO1Dt34IEPi1jPAXQ3zmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R7Whx/btqFN151GN9/tRO1Dt34IEPi1jPAXQ3zmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR7Whx%2FbtqFN151GN9%2FtRO1Dt34IEPi1jPAXQ3zmk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;15. reboot 이후 hostname이 변경되었음을 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yUiol/btqFOhHzVhH/6q203hOXR4l6XNS1fRkbjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yUiol/btqFOhHzVhH/6q203hOXR4l6XNS1fRkbjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yUiol/btqFOhHzVhH/6q203hOXR4l6XNS1fRkbjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyUiol%2FbtqFOhHzVhH%2F6q203hOXR4l6XNS1fRkbjk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;16. work1 가상머신과 동일한 작업을 위해 work2 머신을 기동합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWXoGW/btqFN3pgu67/fylMuF4XkLVCpxtNf0oiEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWXoGW/btqFN3pgu67/fylMuF4XkLVCpxtNf0oiEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWXoGW/btqFN3pgu67/fylMuF4XkLVCpxtNf0oiEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWXoGW%2FbtqFN3pgu67%2FfylMuF4XkLVCpxtNf0oiEK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;17. &lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;터미널을 엽니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dd9x9x/btqFNQjolE6/rBHfTL21489s6I4VPo0WPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dd9x9x/btqFNQjolE6/rBHfTL21489s6I4VPo0WPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dd9x9x/btqFNQjolE6/rBHfTL21489s6I4VPo0WPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdd9x9x%2FbtqFNQjolE6%2FrBHfTL21489s6I4VPo0WPK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;18. root로 접속합니다. (sudo -i)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjQSeS/btqFN2DVyrY/smCJhcKYX7lIzSj9uHywhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjQSeS/btqFN2DVyrY/smCJhcKYX7lIzSj9uHywhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjQSeS/btqFN2DVyrY/smCJhcKYX7lIzSj9uHywhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjQSeS%2FbtqFN2DVyrY%2FsmCJhcKYX7lIzSj9uHywhK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;19. hostname을 변경합니다. &lt;span style=&quot;color: #333333;&quot;&gt;(hostnamectl set-hostname node02)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lfiJH/btqFOh1S4GJ/LCbaNWNFA1064HxelsdR5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lfiJH/btqFOh1S4GJ/LCbaNWNFA1064HxelsdR5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lfiJH/btqFOh1S4GJ/LCbaNWNFA1064HxelsdR5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlfiJH%2FbtqFOh1S4GJ%2FLCbaNWNFA1064HxelsdR5K%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;20. &lt;span style=&quot;color: #333333;&quot;&gt;고정 IP 설정을 위해 설정 파일 위치로 이동합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqD2YV/btqFNPSjT9w/QGyWoVQdfO8qzWWKmqutVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqD2YV/btqFNPSjT9w/QGyWoVQdfO8qzWWKmqutVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqD2YV/btqFNPSjT9w/QGyWoVQdfO8qzWWKmqutVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqD2YV%2FbtqFNPSjT9w%2FQGyWoVQdfO8qzWWKmqutVk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;21. 설정 파일을 오픈합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lE0gd/btqFM9YwazF/LhKRbOQbLhhCkiQQh4AidK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lE0gd/btqFM9YwazF/LhKRbOQbLhhCkiQQh4AidK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lE0gd/btqFM9YwazF/LhKRbOQbLhhCkiQQh4AidK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlE0gd%2FbtqFM9YwazF%2FLhKRbOQbLhhCkiQQh4AidK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;22. node02의 IP를 변경합니다. (&lt;/span&gt;&lt;b&gt;AS-IS&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;192.168.72.101,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;TO-BE&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 192.168.72.103)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ASXEC/btqFNhPyMt6/rzPzMqklcaKh0ysr4Nz4z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ASXEC/btqFNhPyMt6/rzPzMqklcaKh0ysr4Nz4z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ASXEC/btqFNhPyMt6/rzPzMqklcaKh0ysr4Nz4z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FASXEC%2FbtqFNhPyMt6%2FrzPzMqklcaKh0ysr4Nz4z1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;23. 재부팅을 수행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R7Whx/btqFN151GN9/tRO1Dt34IEPi1jPAXQ3zmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R7Whx/btqFN151GN9/tRO1Dt34IEPi1jPAXQ3zmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R7Whx/btqFN151GN9/tRO1Dt34IEPi1jPAXQ3zmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR7Whx%2FbtqFN151GN9%2FtRO1Dt34IEPi1jPAXQ3zmk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;24. &lt;span style=&quot;color: #333333;&quot;&gt;reboot 이후 hostname이 변경되었음을 확인합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tfVIY/btqFOg9KBuq/gvlLQkKrmt1nLlT1tzc4Q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tfVIY/btqFOg9KBuq/gvlLQkKrmt1nLlT1tzc4Q0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tfVIY/btqFOg9KBuq/gvlLQkKrmt1nLlT1tzc4Q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtfVIY%2FbtqFOg9KBuq%2FgvlLQkKrmt1nLlT1tzc4Q0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;25. Master 가상 머신을 기동합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x3agi/btqFNPx1a30/Ba5JoyDUU8j82d6a2KK1f1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x3agi/btqFNPx1a30/Ba5JoyDUU8j82d6a2KK1f1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x3agi/btqFNPx1a30/Ba5JoyDUU8j82d6a2KK1f1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx3agi%2FbtqFNPx1a30%2FBa5JoyDUU8j82d6a2KK1f1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;26. Ping을 수행하여 정상적으로 수행되는지 확인합니다.&lt;/p&gt;
&lt;p&gt;(※ Master 뿐만 아니라 Work1, Work2 가상 머신에서도 Ping을 수행하여 패킷이 정상 수신되는지 확인합니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m2EzF/btqFOiNihL6/QjXE4SLLQvxflssLA8qOR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m2EzF/btqFOiNihL6/QjXE4SLLQvxflssLA8qOR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m2EzF/btqFOiNihL6/QjXE4SLLQvxflssLA8qOR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm2EzF%2FbtqFOiNihL6%2FQjXE4SLLQvxflssLA8qOR0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oU68S/btqFNEpZfWy/o2Xi412YAHVC1xkuMypiE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oU68S/btqFNEpZfWy/o2Xi412YAHVC1xkuMypiE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oU68S/btqFNEpZfWy/o2Xi412YAHVC1xkuMypiE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoU68S%2FbtqFNEpZfWy%2Fo2Xi412YAHVC1xkuMypiE0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pv20p/btqFNEjfioE/waK1l0UaObSEfsxOZkwzEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pv20p/btqFNEjfioE/waK1l0UaObSEfsxOZkwzEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pv20p/btqFNEjfioE/waK1l0UaObSEfsxOZkwzEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpv20p%2FbtqFNEjfioE%2FwaK1l0UaObSEfsxOZkwzEK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;27. Master 노드에서 root 계정으로 접속합니다.&lt;/p&gt;
&lt;p&gt;(※ 반드시 Master에서만 수행해야합니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Lu6xB/btqFNawmU2q/wTK5rkfBqzS0hV7sQZYSa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Lu6xB/btqFNawmU2q/wTK5rkfBqzS0hV7sQZYSa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Lu6xB/btqFNawmU2q/wTK5rkfBqzS0hV7sQZYSa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLu6xB%2FbtqFNawmU2q%2FwTK5rkfBqzS0hV7sQZYSa0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;28. kubeadm init 명령어를 수행합니다.&lt;/p&gt;
&lt;p&gt;(※ 해당 명령어를 통해 kube-apiserver, scheduler 등이 설치됩니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beqriE/btqFOimdKCa/BSYhnRX1TeYVZGYuEa2QQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beqriE/btqFOimdKCa/BSYhnRX1TeYVZGYuEa2QQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beqriE/btqFOimdKCa/BSYhnRX1TeYVZGYuEa2QQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeqriE%2FbtqFOimdKCa%2FBSYhnRX1TeYVZGYuEa2QQK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;29. 정상적으로 설치되었으면 아래와 같은 화면이 표시됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOcR6j/btqFNapzIKN/ScaPvb8Wyk8YdllUcIR3HK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOcR6j/btqFNapzIKN/ScaPvb8Wyk8YdllUcIR3HK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOcR6j/btqFNapzIKN/ScaPvb8Wyk8YdllUcIR3HK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOcR6j%2FbtqFNapzIKN%2FScaPvb8Wyk8YdllUcIR3HK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 화면에서 두가지 작업이 추가됩니다.&lt;/p&gt;
&lt;p&gt;먼저 mkdir 부터 시작하는 영역은 kubectl을 통해 Kube API Server와 통신하기 위한 클라이언트 설정입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아래 kubeadm join에 해당하는 영역은 Worker 노드에서 Master 노드 클러스터에 합류하기 위한 명령어입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;30. 아래 영역을 드래그하여 복사합니다. 이후 터미널 상단에 위치한 새탭 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/E7dmI/btqFMG3m0ey/2rko5y6iCTko2vYkkHIr2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/E7dmI/btqFMG3m0ey/2rko5y6iCTko2vYkkHIr2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/E7dmI/btqFMG3m0ey/2rko5y6iCTko2vYkkHIr2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FE7dmI%2FbtqFMG3m0ey%2F2rko5y6iCTko2vYkkHIr2K%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;31.&amp;nbsp; 새탭에서 복사한 명령어를 수행합니다.&lt;/p&gt;
&lt;p&gt;(※ root가 아닌 admin 계정으로 kubectl을 수행할 것이므로 계정이 올바른지 확인합니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JOaQU/btqFOM1Fhil/ygEZfi7x2DMX6affK0rN11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JOaQU/btqFOM1Fhil/ygEZfi7x2DMX6affK0rN11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JOaQU/btqFOM1Fhil/ygEZfi7x2DMX6affK0rN11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJOaQU%2FbtqFOM1Fhil%2FygEZfi7x2DMX6affK0rN11%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;32. 이전 탭에서 클러스터 조인 명령어를 복사합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwkZ87/btqFNQcGDK5/xZWA5Csnmw2JK3v7FtoQYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwkZ87/btqFNQcGDK5/xZWA5Csnmw2JK3v7FtoQYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwkZ87/btqFNQcGDK5/xZWA5Csnmw2JK3v7FtoQYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwkZ87%2FbtqFNQcGDK5%2FxZWA5Csnmw2JK3v7FtoQYk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;33. Work1 노드, Work2 노드에서 root 계정으로 접속합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEuil5/btqFNapzWLB/R3zmobmBkS9Cx0WKFLb1Z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEuil5/btqFNapzWLB/R3zmobmBkS9Cx0WKFLb1Z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEuil5/btqFNapzWLB/R3zmobmBkS9Cx0WKFLb1Z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEuil5%2FbtqFNapzWLB%2FR3zmobmBkS9Cx0WKFLb1Z1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;34. Work1 노드에서 복사한 명령어를 실행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTguMG/btqFMIGQ4q6/znvRfgUsIZVm9gM3995W0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTguMG/btqFMIGQ4q6/znvRfgUsIZVm9gM3995W0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTguMG/btqFMIGQ4q6/znvRfgUsIZVm9gM3995W0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTguMG%2FbtqFMIGQ4q6%2FznvRfgUsIZVm9gM3995W0k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;35. 아래와 같은 메시지가 나오면 정상적으로 등록된 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eixuzI/btqFMHnBSNw/jxNNOhGuvbbgO9kClCur20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eixuzI/btqFMHnBSNw/jxNNOhGuvbbgO9kClCur20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eixuzI/btqFMHnBSNw/jxNNOhGuvbbgO9kClCur20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeixuzI%2FbtqFMHnBSNw%2FjxNNOhGuvbbgO9kClCur20%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;36. Work2 노드에서도 동일한 명령어를 수행하여 클러스터 Join을 수행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blliG5/btqFNQKtAX6/92ddnKg6tfxoZPvdLEjYVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blliG5/btqFNQKtAX6/92ddnKg6tfxoZPvdLEjYVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blliG5/btqFNQKtAX6/92ddnKg6tfxoZPvdLEjYVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblliG5%2FbtqFNQKtAX6%2F92ddnKg6tfxoZPvdLEjYVk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;37. Work2 노드에서 Join이 정상적으로 등록되었는지 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Nj7hZ/btqFNhIMbFf/S2jJfzFbY4I1HtTKzv52GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Nj7hZ/btqFNhIMbFf/S2jJfzFbY4I1HtTKzv52GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Nj7hZ/btqFNhIMbFf/S2jJfzFbY4I1HtTKzv52GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNj7hZ%2FbtqFNhIMbFf%2FS2jJfzFbY4I1HtTKzv52GK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;38. Master 노드 kadmin 계정에서 cluster가 정상적으로 구축되었는지 확인합니다. (kubectl get nodes)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzuGtw/btqFN3CPCUl/1YBHlaCTsX6nk0Zqx0x2o0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzuGtw/btqFN3CPCUl/1YBHlaCTsX6nk0Zqx0x2o0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzuGtw/btqFN3CPCUl/1YBHlaCTsX6nk0Zqx0x2o0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzuGtw%2FbtqFN3CPCUl%2F1YBHlaCTsX6nk0Zqx0x2o0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같이 3개 노드와 메시지가 표시된다면 정상입니다.&lt;/p&gt;
&lt;p&gt;다만 STATUS가 NotReady 상태인 이유는 Pod Network가 구성되지 않았기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Overlay Network 설치&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#pod-network&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;쿠버네티스 공식 홈페이지&lt;/u&gt;&lt;/a&gt;에 있는 CNI 중 Weave net을 설치하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. 위 공식 문서 링크를 클릭하여 Weave Net 탭을 선택하여 명령어를 복사합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rzTUo/btqFMGWz7nC/agDeI0mcSZho24SKxGMwg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rzTUo/btqFMGWz7nC/agDeI0mcSZho24SKxGMwg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rzTUo/btqFMGWz7nC/agDeI0mcSZho24SKxGMwg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrzTUo%2FbtqFMGWz7nC%2FagDeI0mcSZho24SKxGMwg1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. Master 노드에서 해당 명령어를 실행시킵니다.&lt;/p&gt;
&lt;p&gt;(kubectl apply -f &lt;span&gt;&quot;&lt;a href=&quot;https://cloud.weave.works/k8s/net?k8s-version=&quot;&gt;https://cloud.weave.works/k8s/net?k8s-version=&lt;/a&gt;&lt;/span&gt;&lt;span&gt;$(&lt;/span&gt;kubectl version | base64 | tr -d &lt;span&gt;'\n'&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;&quot;)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oR9xK/btqFNspOnaU/e78r510kYmY27kmvHFKfO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oR9xK/btqFNspOnaU/e78r510kYmY27kmvHFKfO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oR9xK/btqFNspOnaU/e78r510kYmY27kmvHFKfO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoR9xK%2FbtqFNspOnaU%2Fe78r510kYmY27kmvHFKfO0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3. node 상태를 살펴보면 STATUS가 Ready로 변경된 것을 확인할 수 있습니다. (kubectl get nodes)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cslV4C/btqFN3QnCoi/WvkCNqekIFLVw6UZ86eOj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cslV4C/btqFN3QnCoi/WvkCNqekIFLVw6UZ86eOj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cslV4C/btqFN3QnCoi/WvkCNqekIFLVw6UZ86eOj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcslV4C%2FbtqFN3QnCoi%2FWvkCNqekIFLVw6UZ86eOj1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4. 쿠버네티스 클러스터가 정상적으로 설치되었는지 확인하기 위해 nginx Deployment를 실행해봅니다.&lt;/p&gt;
&lt;p&gt;(kubectl create deploy nginx-deploy --image=nginx)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dipgt4/btqFOhHBkKg/O5gcfXwLpv8EQ01ub4qvK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dipgt4/btqFOhHBkKg/O5gcfXwLpv8EQ01ub4qvK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dipgt4/btqFOhHBkKg/O5gcfXwLpv8EQ01ub4qvK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdipgt4%2FbtqFOhHBkKg%2FO5gcfXwLpv8EQ01ub4qvK0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;5. Pod 상태를 확인하여 STATUS Running으로 되어있으면 정상적으로 동작하는 것입니다.&lt;/p&gt;
&lt;p&gt;(kubectl get pods -o wide)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xZWir/btqFOhgyhCt/7GIiPHeYFGbPkxTU2j5N4k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xZWir/btqFOhgyhCt/7GIiPHeYFGbPkxTU2j5N4k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xZWir/btqFOhgyhCt/7GIiPHeYFGbPkxTU2j5N4k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxZWir%2FbtqFOhgyhCt%2F7GIiPHeYFGbPkxTU2j5N4k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;6.&amp;nbsp; 실행시킨 Deployment를 종료시킵니다.&lt;/p&gt;
&lt;p&gt;(kubectl delete deploy nginx-deploy)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sZ0Ln/btqFOh8GOLQ/1TRrkf7KqWvjoCoLnuf1wk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sZ0Ln/btqFOh8GOLQ/1TRrkf7KqWvjoCoLnuf1wk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sZ0Ln/btqFOh8GOLQ/1TRrkf7KqWvjoCoLnuf1wk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsZ0Ln%2FbtqFOh8GOLQ%2F1TRrkf7KqWvjoCoLnuf1wk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금까지 VirtualBox에서 쿠버네티스 클러스터를 구축하는 방법에 대해서 살펴봤습니다. 다음 포스팅에서는 공유 디스크 환경을 구성하는 NFS를 설정하여 PersistentVolume을 구성할 수 있도록 환경을 구성하는 방법을 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Cloud/Kubernetes</category>
      <category>docker</category>
      <category>K8S 설치</category>
      <category>VirtualBox</category>
      <category>VirtualBox kubernetes 설치</category>
      <category>VirtualBox 쿠버네티스 설치</category>
      <category>쿠버네티스</category>
      <category>쿠버네티스 설치</category>
      <category>쿠버네티스 클러스터 설치</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/92</guid>
      <comments>https://cla9.tistory.com/92#entry92comment</comments>
      <pubDate>Sat, 18 Jul 2020 17:39:25 +0900</pubDate>
    </item>
    <item>
      <title>2. 쿠버네티스 클러스터 구축 - Ubuntu 설치 및 환경 구성</title>
      <link>https://cla9.tistory.com/91</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서론&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cla9.tistory.com/90?category=814452&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;이전 포스팅&lt;/u&gt;&lt;/a&gt;을 통해 Ubuntu 이미지 다운로드 및 VirtualBox 기본 환경 설정을 구성하였습니다. 이번 포스팅에서는 K8S 클러스터 설치를 위한 기본 구성을 진행하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Ubuntu 설치&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. VM 기동을 위해 머신 선택 후 시작 버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I6B3D/btqFMG3gWR5/6Bl2khi36uKkfATl4F1by1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I6B3D/btqFMG3gWR5/6Bl2khi36uKkfATl4F1by1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I6B3D/btqFMG3gWR5/6Bl2khi36uKkfATl4F1by1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI6B3D%2FbtqFMG3gWR5%2F6Bl2khi36uKkfATl4F1by1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. 언어 설정 이후에 Ubuntu 설치 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OKwiM/btqFNgXiWKk/WrVv08Jv6F47y5iPdip1Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OKwiM/btqFNgXiWKk/WrVv08Jv6F47y5iPdip1Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OKwiM/btqFNgXiWKk/WrVv08Jv6F47y5iPdip1Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOKwiM%2FbtqFNgXiWKk%2FWrVv08Jv6F47y5iPdip1Dk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3. 별다른 설정없이 계속하기 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mSith/btqFMH2aqIv/oGb5eOUamwQ6Jv0orYIuxk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mSith/btqFMH2aqIv/oGb5eOUamwQ6Jv0orYIuxk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mSith/btqFMH2aqIv/oGb5eOUamwQ6Jv0orYIuxk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmSith%2FbtqFMH2aqIv%2FoGb5eOUamwQ6Jv0orYIuxk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4. 계속하기 버튼을 선택합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1w7rY/btqFMH8ZmNn/S84l4K7UyypsI7Ee3JkGn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1w7rY/btqFMH8ZmNn/S84l4K7UyypsI7Ee3JkGn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1w7rY/btqFMH8ZmNn/S84l4K7UyypsI7Ee3JkGn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1w7rY%2FbtqFMH8ZmNn%2FS84l4K7UyypsI7Ee3JkGn1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;5. 지금 설치 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6IHbX/btqFNDYO9Cj/0AFPIEBX3lHKVSmEBQiNl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6IHbX/btqFNDYO9Cj/0AFPIEBX3lHKVSmEBQiNl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6IHbX/btqFNDYO9Cj/0AFPIEBX3lHKVSmEBQiNl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6IHbX%2FbtqFNDYO9Cj%2F0AFPIEBX3lHKVSmEBQiNl0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;6. 계속 하기 버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FHrLP/btqFOi7vihP/ze9XKmywLqWXiKhtnhhFj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FHrLP/btqFOi7vihP/ze9XKmywLqWXiKhtnhhFj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FHrLP/btqFOi7vihP/ze9XKmywLqWXiKhtnhhFj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFHrLP%2FbtqFOi7vihP%2Fze9XKmywLqWXiKhtnhhFj0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;7. 지역 선택 후 계속 하기 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0epPm/btqFNEXJh2E/FELM3FKiDBYwRVOtdgMHck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0epPm/btqFNEXJh2E/FELM3FKiDBYwRVOtdgMHck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0epPm/btqFNEXJh2E/FELM3FKiDBYwRVOtdgMHck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0epPm%2FbtqFNEXJh2E%2FFELM3FKiDBYwRVOtdgMHck%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;8. 컴퓨터 정보 입력 후 계속하기 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8sgyM/btqFNPxWasO/Sd2JQkRMJWy6gTJcZTzRo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8sgyM/btqFNPxWasO/Sd2JQkRMJWy6gTJcZTzRo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8sgyM/btqFNPxWasO/Sd2JQkRMJWy6gTJcZTzRo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8sgyM%2FbtqFNPxWasO%2FSd2JQkRMJWy6gTJcZTzRo0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;9. 설치가 완료될 때 까지 기다립니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qdI36/btqFLtQDGrE/uQb43OebWbhluuS1uVht2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qdI36/btqFLtQDGrE/uQb43OebWbhluuS1uVht2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qdI36/btqFLtQDGrE/uQb43OebWbhluuS1uVht2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqdI36%2FbtqFLtQDGrE%2FuQb43OebWbhluuS1uVht2K%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;10. 설치가 완료되면 지금 다시 시작 버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc7wvX/btqFN2ju3b3/3FrsVu2npsKl2cdht0usfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc7wvX/btqFN2ju3b3/3FrsVu2npsKl2cdht0usfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc7wvX/btqFN2ju3b3/3FrsVu2npsKl2cdht0usfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc7wvX%2FbtqFN2ju3b3%2F3FrsVu2npsKl2cdht0usfk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;11. 아래와 같은 화면이 출력된다면, Enter키를 누르면 서버가 재시작됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dzAGkE/btqFNEQWLy1/RtzQBgKRZgNu8oiZidunCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dzAGkE/btqFNEQWLy1/RtzQBgKRZgNu8oiZidunCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dzAGkE/btqFNEQWLy1/RtzQBgKRZgNu8oiZidunCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdzAGkE%2FbtqFNEQWLy1%2FRtzQBgKRZgNu8oiZidunCK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ubuntu 환경 설정&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. VM 재시작 이후 로그인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dfs8s/btqFMHHUc6g/K3FednK3AkMTIKyI16KDfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dfs8s/btqFMHHUc6g/K3FednK3AkMTIKyI16KDfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dfs8s/btqFMHHUc6g/K3FednK3AkMTIKyI16KDfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDfs8s%2FbtqFMHHUc6g%2FK3FednK3AkMTIKyI16KDfk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. 건너뛰기 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcGchi/btqFN2KC7mE/0CgT4itC8cBERerj2Q5v10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcGchi/btqFN2KC7mE/0CgT4itC8cBERerj2Q5v10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcGchi/btqFN2KC7mE/0CgT4itC8cBERerj2Q5v10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcGchi%2FbtqFN2KC7mE%2F0CgT4itC8cBERerj2Q5v10%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3. 다음 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnGkLK/btqFLsYtTIY/uJUfgUlYqrMAWSyckKqnsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnGkLK/btqFLsYtTIY/uJUfgUlYqrMAWSyckKqnsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnGkLK/btqFLsYtTIY/uJUfgUlYqrMAWSyckKqnsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnGkLK%2FbtqFLsYtTIY%2FuJUfgUlYqrMAWSyckKqnsK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCVHIP/btqFN3JvKoL/aLwWrg9qskanhgh4iMusTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCVHIP/btqFN3JvKoL/aLwWrg9qskanhgh4iMusTK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCVHIP/btqFN3JvKoL/aLwWrg9qskanhgh4iMusTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCVHIP%2FbtqFN3JvKoL%2FaLwWrg9qskanhgh4iMusTK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4. 완료 버튼을 눌러 최초 시작 환경 구성을 종료합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dLB187/btqFNaJPhXs/zcEcMjTyhGYecTClkkhPm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dLB187/btqFNaJPhXs/zcEcMjTyhGYecTClkkhPm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dLB187/btqFNaJPhXs/zcEcMjTyhGYecTClkkhPm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdLB187%2FbtqFNaJPhXs%2FzcEcMjTyhGYecTClkkhPm0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;5. 소프트웨어 업데이터 창이 뜬다면 지금 설치를 눌러 업데이트를 진행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eczkBv/btqFN3pb3EL/YuQhBv8hIo9kPVuyXLOgbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eczkBv/btqFN3pb3EL/YuQhBv8hIo9kPVuyXLOgbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eczkBv/btqFN3pb3EL/YuQhBv8hIo9kPVuyXLOgbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeczkBv%2FbtqFN3pb3EL%2FYuQhBv8hIo9kPVuyXLOgbK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;6. 환경 구성을 위해 목록창을 열고 터미널을 입력하여 터미널 창을 띄웁니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b59wK4/btqFNf5bsAr/5k2LWzPIRt7KnuUrWp1or1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b59wK4/btqFNf5bsAr/5k2LWzPIRt7KnuUrWp1or1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b59wK4/btqFNf5bsAr/5k2LWzPIRt7KnuUrWp1or1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb59wK4%2FbtqFNf5bsAr%2F5k2LWzPIRt7KnuUrWp1or1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;7. root 계정으로 작업하기 위해 sudo -i 명령어를 입력합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTFc6P/btqFNfRFGK4/daS20O54lD3siD0j0fbky0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTFc6P/btqFNfRFGK4/daS20O54lD3siD0j0fbky0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTFc6P/btqFNfRFGK4/daS20O54lD3siD0j0fbky0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTFc6P%2FbtqFNfRFGK4%2FdaS20O54lD3siD0j0fbky0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;8. root 계정의 비밀번호를 변경합니다. (passwd)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEyWyD/btqFOil8IJH/nd1UD5RDfvU8He9GnwRDF1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEyWyD/btqFOil8IJH/nd1UD5RDfvU8He9GnwRDF1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEyWyD/btqFOil8IJH/nd1UD5RDfvU8He9GnwRDF1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEyWyD%2FbtqFOil8IJH%2Fnd1UD5RDfvU8He9GnwRDF1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;9. update를 진행합니다. (apt update)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/I3aXG/btqFONe8XTe/kDmJRkbNiU0fziSLkESolk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/I3aXG/btqFONe8XTe/kDmJRkbNiU0fziSLkESolk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/I3aXG/btqFONe8XTe/kDmJRkbNiU0fziSLkESolk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FI3aXG%2FbtqFONe8XTe%2FkDmJRkbNiU0fziSLkESolk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;10.&amp;nbsp; openssh-server를 설치합니다. (apt install openssh-server)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beg0sL/btqFMItiTC4/z1h0bnVekRfSEnnepYWn1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beg0sL/btqFMItiTC4/z1h0bnVekRfSEnnepYWn1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beg0sL/btqFMItiTC4/z1h0bnVekRfSEnnepYWn1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbeg0sL%2FbtqFMItiTC4%2Fz1h0bnVekRfSEnnepYWn1k%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;11. ifconfig 명령어를 보기 위해 net-tools를 설치합니다. (apt install net-tools)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/33432/btqFOMHiank/aNO6jFyVP9PW6UlFTX9De1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/33432/btqFOMHiank/aNO6jFyVP9PW6UlFTX9De1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/33432/btqFOMHiank/aNO6jFyVP9PW6UlFTX9De1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F33432%2FbtqFOMHiank%2FaNO6jFyVP9PW6UlFTX9De1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;12. 학습 목적이므로 모든 방화벽을 해제합니다. (ufw disable)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccXMJF/btqFN3is5kT/V4Y34lR3KOkUiaT4PVXb60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccXMJF/btqFN3is5kT/V4Y34lR3KOkUiaT4PVXb60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccXMJF/btqFN3is5kT/V4Y34lR3KOkUiaT4PVXb60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccXMJF%2FbtqFN3is5kT%2FV4Y34lR3KOkUiaT4PVXb60%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;13. 방화벽 상태를 확인합니다. (ufw status verbose)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOw83Z/btqFNDR3coD/fFhf5RKkyB9yveQPbDp5Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOw83Z/btqFNDR3coD/fFhf5RKkyB9yveQPbDp5Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOw83Z/btqFNDR3coD/fFhf5RKkyB9yveQPbDp5Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOw83Z%2FbtqFNDR3coD%2FfFhf5RKkyB9yveQPbDp5Kk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;14. Vim을 설치합니다. (apt-get install vim)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGCqng/btqFNFI76fX/ASlIxsC6eutBkWuqSlCt4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGCqng/btqFNFI76fX/ASlIxsC6eutBkWuqSlCt4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGCqng/btqFNFI76fX/ASlIxsC6eutBkWuqSlCt4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGCqng%2FbtqFNFI76fX%2FASlIxsC6eutBkWuqSlCt4K%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;15. 편의 기능인 클립보드 및 공유폴더 기능을 사용하기 위해 장치 &amp;gt; 게스트 확장 CD 이미지 삽입 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dAIgbQ/btqFN3W4gk4/wNlKMf6jU9anzk1K8Qiux0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dAIgbQ/btqFN3W4gk4/wNlKMf6jU9anzk1K8Qiux0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dAIgbQ/btqFN3W4gk4/wNlKMf6jU9anzk1K8Qiux0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdAIgbQ%2FbtqFN3W4gk4%2FwNlKMf6jU9anzk1K8Qiux0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;16. 실행 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ntc7U/btqFNa3995o/0Zsk2vgRjTQgtHSBoQJ840/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ntc7U/btqFNa3995o/0Zsk2vgRjTQgtHSBoQJ840/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ntc7U/btqFNa3995o/0Zsk2vgRjTQgtHSBoQJ840/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fntc7U%2FbtqFNa3995o%2F0Zsk2vgRjTQgtHSBoQJ840%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;17.&amp;nbsp; 인증 필요시 비밀번호 입력 후 인증 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqoRqT/btqFMG918Yn/WEnYUxXkOUaIqqmEQcjZG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqoRqT/btqFMG918Yn/WEnYUxXkOUaIqqmEQcjZG0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqoRqT/btqFMG918Yn/WEnYUxXkOUaIqqmEQcjZG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqoRqT%2FbtqFMG918Yn%2FWEnYUxXkOUaIqqmEQcjZG0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;18. 클립보드 공유를 위해 장치 &amp;gt; 클립보드 공유 &amp;gt; 양방향 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOQfz7/btqFNaC5Ngp/dqKjh3ZLPZdg9YwoDlzjCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOQfz7/btqFNaC5Ngp/dqKjh3ZLPZdg9YwoDlzjCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOQfz7/btqFNaC5Ngp/dqKjh3ZLPZdg9YwoDlzjCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOQfz7%2FbtqFNaC5Ngp%2FdqKjh3ZLPZdg9YwoDlzjCk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;19. 지금부터 네트워크 설정을 하겠습니다. 먼저 hosts 파일을 열어 master 노드와 worker 노드의 IP 목록을 입력한 다음 저장합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTuZgd/btqFNEcnGKR/LrT2LEE7CzTNNcZ2SarvT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTuZgd/btqFNEcnGKR/LrT2LEE7CzTNNcZ2SarvT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTuZgd/btqFNEcnGKR/LrT2LEE7CzTNNcZ2SarvT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTuZgd%2FbtqFNEcnGKR%2FLrT2LEE7CzTNNcZ2SarvT0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzFvHn/btqFNEQ0Qyd/r2AzZys6AE9TOihjMoQRQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzFvHn/btqFNEQ0Qyd/r2AzZys6AE9TOihjMoQRQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzFvHn/btqFNEQ0Qyd/r2AzZys6AE9TOihjMoQRQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzFvHn%2FbtqFNEQ0Qyd%2Fr2AzZys6AE9TOihjMoQRQK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;20. hostname을 변경합니다. (hostnamectl set-hostname master)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4w61E/btqFNsb7SuC/ZVRckTl1mkpYV1SCnxb0S0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4w61E/btqFNsb7SuC/ZVRckTl1mkpYV1SCnxb0S0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4w61E/btqFNsb7SuC/ZVRckTl1mkpYV1SCnxb0S0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4w61E%2FbtqFNsb7SuC%2FZVRckTl1mkpYV1SCnxb0S0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;21. ip 목록을 확인합니다. (ifconfig)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjvt2Q/btqFNhBWJX6/Bx2Inl9iCvy2zJuTYRi2Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjvt2Q/btqFNhBWJX6/Bx2Inl9iCvy2zJuTYRi2Qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjvt2Q/btqFNhBWJX6/Bx2Inl9iCvy2zJuTYRi2Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcjvt2Q%2FbtqFNhBWJX6%2FBx2Inl9iCvy2zJuTYRi2Qk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림에서 enp0s3에 등록된 ip는 Bridge Adaptor로 등록된 IP이며, PC에서 SSH를 통해 접속 가능합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;확인을 위해 개인 PC에서 Putty를 통해 해당 IP로 접속시도하면 정상적으로 커넥션이 맺어지는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;22. Host Only Ethernet 설정을 위해 설정 파일이 있는 폴더로 이동합니다. (cd /etc/netplan/)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0F9pg/btqFOizGCsg/WhMz86hTZGeJJjmbQ2iB9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0F9pg/btqFOizGCsg/WhMz86hTZGeJJjmbQ2iB9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0F9pg/btqFOizGCsg/WhMz86hTZGeJJjmbQ2iB9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0F9pg%2FbtqFOizGCsg%2FWhMz86hTZGeJJjmbQ2iB9K%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;23. 설정 파일을 열어 고정 IP 설정을 진행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tgTyQ/btqFNDR3FYE/GDyGPNncrbEFoP3hCQ4AnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tgTyQ/btqFNDR3FYE/GDyGPNncrbEFoP3hCQ4AnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tgTyQ/btqFNDR3FYE/GDyGPNncrbEFoP3hCQ4AnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtgTyQ%2FbtqFNDR3FYE%2FGDyGPNncrbEFoP3hCQ4AnK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZsH3O/btqFOhU5vKZ/wILQawkVLsGAl1PvR8kGDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZsH3O/btqFOhU5vKZ/wILQawkVLsGAl1PvR8kGDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZsH3O/btqFOhU5vKZ/wILQawkVLsGAl1PvR8kGDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZsH3O%2FbtqFOhU5vKZ%2FwILQawkVLsGAl1PvR8kGDk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;24. 고정 IP 설정을 적용합니다. (netplan apply)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MLH2O/btqFNswrFNk/kTxsrNOYxLlwIj7Wf7RhKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MLH2O/btqFNswrFNk/kTxsrNOYxLlwIj7Wf7RhKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MLH2O/btqFNswrFNk/kTxsrNOYxLlwIj7Wf7RhKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMLH2O%2FbtqFNswrFNk%2FkTxsrNOYxLlwIj7Wf7RhKK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;25. IP 설정이 정상적으로 되었는지 확인합니다. (hostname -I)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3Qke3/btqFNboqmmB/uu5iKeCHqEu6EKSmfLSnz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3Qke3/btqFNboqmmB/uu5iKeCHqEu6EKSmfLSnz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3Qke3/btqFNboqmmB/uu5iKeCHqEu6EKSmfLSnz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3Qke3%2FbtqFNboqmmB%2Fuu5iKeCHqEu6EKSmfLSnz0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;26. hostname 변경 적용을 위해 reboot을 진행합니다.(reboot)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8qFxw/btqFMHA7cC8/nA0egbKPTrdOI5aiSuxAiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8qFxw/btqFMHA7cC8/nA0egbKPTrdOI5aiSuxAiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8qFxw/btqFMHA7cC8/nA0egbKPTrdOI5aiSuxAiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8qFxw%2FbtqFMHA7cC8%2FnA0egbKPTrdOI5aiSuxAiK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;27. reboot 이후에 hostname이 변경되었음을 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPyl6N/btqFN3W3Os3/kWnLkLhqXXC4Jv0L7e6yp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPyl6N/btqFN3W3Os3/kWnLkLhqXXC4Jv0L7e6yp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPyl6N/btqFN3W3Os3/kWnLkLhqXXC4Jv0L7e6yp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPyl6N%2FbtqFN3W3Os3%2FkWnLkLhqXXC4Jv0L7e6yp0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Docker 및 쿠버네티스 설치를 위한 기초 설정 작업을 마쳤습니다. 다음 포스팅에서는 Docker &amp;amp; Kubernetes 설치 과정을 다루어보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Cloud/Kubernetes</category>
      <category>docker 설치</category>
      <category>K8S 설치</category>
      <category>Ubuntu 설치</category>
      <category>쿠버네티스 설치</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/91</guid>
      <comments>https://cla9.tistory.com/91#entry91comment</comments>
      <pubDate>Sat, 18 Jul 2020 16:28:03 +0900</pubDate>
    </item>
    <item>
      <title>1. 쿠버네티스 클러스터 구축 - Virtual Box 환경 설정</title>
      <link>https://cla9.tistory.com/90</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;서론&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;쿠버네티스를 처음 공부할 때 난해한 부분이 클러스터 설치라고 생각합니다. 물론 GCP, AWS와 같은 클라우드에서 제공하는 쿠버네티스 클러스터를 활용하거나 Katakoda와 같은 웹사이트를 통해 접할 수도 있지만, 직접 VM으로 클러스터를 구축하는 경험도 중요하다고 생각합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;향후 몇개의 포스팅을 통해 VirtualBox를 통해 3개(1 Master, 2 Worker)의 K8S 클러스터를 구축방법을 다루어보고자 합니다.&amp;nbsp;쿠버네티스는 Kubeadm을 활용하여 설치할 계획이며, 학습 목적이므로 Ansible이나 기타 자동화 툴 도입이나 상세 설정 없이 설치하는 과정을 설명합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsDRPT/btqFNQRhCq0/KH4EFzRkKlQJBjjnAUCKgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsDRPT/btqFNQRhCq0/KH4EFzRkKlQJBjjnAUCKgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsDRPT/btqFNQRhCq0/KH4EFzRkKlQJBjjnAUCKgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsDRPT%2FbtqFNQRhCq0%2FKH4EFzRkKlQJBjjnAUCKgK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;PC 환경은 램 &lt;span style=&quot;color: #333333;&quot;&gt;16GB&lt;span&gt; &lt;/span&gt;&lt;/span&gt;이상을 권장드리며, 공유기가 있다는 전제하에 네트워크 구성은 Bridge Adaptor를 통해 구성하오니 내용 참고 바랍니다.&lt;/p&gt;
&lt;p&gt;(※ 공유기가 없는 환경이라면 NAT 네트워크를 구성하여 진행 가능합니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;앞으로 다룰 내용&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. VirtualBox Ubuntu 설치&lt;/p&gt;
&lt;p&gt;2. Docker 설치&lt;/p&gt;
&lt;p&gt;3. 쿠버네티스 클러스터 구성&lt;/p&gt;
&lt;p&gt;4. NFS 구성&lt;/p&gt;
&lt;p&gt;5. metallb 설치&lt;/p&gt;
&lt;p&gt;6. ingress-nginx 설치&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅은 첫 번째로 VirtualBox를 사용하여 이미지 설치를 위한 기초 설정을 다루겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. VirtualBox 설치&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLzIf9/btqFNEJ4183/7lqzFe8eCuf93RSRP95Qgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLzIf9/btqFNEJ4183/7lqzFe8eCuf93RSRP95Qgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLzIf9/btqFNEJ4183/7lqzFe8eCuf93RSRP95Qgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLzIf9%2FbtqFNEJ4183%2F7lqzFe8eCuf93RSRP95Qgk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.virtualbox.org/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;VirtualBox 공식 홈페이지&lt;/u&gt;&lt;/a&gt;에 접속하여 프로그램을 다운로드 후 설치를 진행합니다.&lt;/p&gt;
&lt;p&gt;설치시 특별한 설정 없이 다음버튼을 눌러 진행하면 되므로 설치 과정은 생략하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Ubuntu 이미지 다운로드&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. &lt;a href=&quot;https://ubuntu.com/#download&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Ubuntu 공식 홈페이지&lt;/u&gt;&lt;/a&gt;에 접속하여 Desktop 다운로드 버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btDKMU/btqFMHgI3qb/yR5CXrnTqtSlpM9yMi0cVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btDKMU/btqFMHgI3qb/yR5CXrnTqtSlpM9yMi0cVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btDKMU/btqFMHgI3qb/yR5CXrnTqtSlpM9yMi0cVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtDKMU%2FbtqFMHgI3qb%2FyR5CXrnTqtSlpM9yMi0cVk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. 이미지를 다운로드 받습니다. 만약 이미지 다운로드가 되지 않는다면, Download Now 링크를 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTCUb4/btqFN1Y8aPI/jqTkhPIXWfy5sYwNmMMnXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTCUb4/btqFN1Y8aPI/jqTkhPIXWfy5sYwNmMMnXK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTCUb4/btqFN1Y8aPI/jqTkhPIXWfy5sYwNmMMnXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTCUb4%2FbtqFN1Y8aPI%2FjqTkhPIXWfy5sYwNmMMnXK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. VirtualBox 설정&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. VirtualBox를 실행시킵니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pd4lk/btqFOg9AbMe/0FpDWKmwm9ZzVCUWMUcG91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pd4lk/btqFOg9AbMe/0FpDWKmwm9ZzVCUWMUcG91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pd4lk/btqFOg9AbMe/0FpDWKmwm9ZzVCUWMUcG91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPd4lk%2FbtqFOg9AbMe%2F0FpDWKmwm9ZzVCUWMUcG91%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. 상단의 머신 &amp;gt; 새로 만들기 버튼을 선택합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eD7zQg/btqFNEwyFfu/xmyrloNLKkboBXrOb5aiOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eD7zQg/btqFNEwyFfu/xmyrloNLKkboBXrOb5aiOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eD7zQg/btqFNEwyFfu/xmyrloNLKkboBXrOb5aiOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeD7zQg%2FbtqFNEwyFfu%2FxmyrloNLKkboBXrOb5aiOK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3. 이미지의 이름을 지정합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qwcXm/btqFLuaRdBg/N2MKgPsRQjvHtHpFFJrWWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qwcXm/btqFLuaRdBg/N2MKgPsRQjvHtHpFFJrWWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qwcXm/btqFLuaRdBg/N2MKgPsRQjvHtHpFFJrWWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqwcXm%2FbtqFLuaRdBg%2FN2MKgPsRQjvHtHpFFJrWWK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4. VM 이미지 설치 위치를 저장하기 위해 머신폴더 &amp;gt; 기타를 선택합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nocHy/btqFLuBWewM/BVXojk4xw4deBKuckeBd61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nocHy/btqFLuBWewM/BVXojk4xw4deBKuckeBd61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nocHy/btqFLuBWewM/BVXojk4xw4deBKuckeBd61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnocHy%2FbtqFLuBWewM%2FBVXojk4xw4deBKuckeBd61%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;5. 설치 위치를 선택한 다음 폴더 선택 버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ce4HfT/btqFOiTR9Vb/4OEObGHEv6qO8REp5U9N3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ce4HfT/btqFOiTR9Vb/4OEObGHEv6qO8REp5U9N3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ce4HfT/btqFOiTR9Vb/4OEObGHEv6qO8REp5U9N3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fce4HfT%2FbtqFOiTR9Vb%2F4OEObGHEv6qO8REp5U9N3K%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;6. 종류와 버전을 선택한 다음 다음 버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OGHeS/btqFOil23Ea/Y4X7Tn3xf2JPFWE2UUWOqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OGHeS/btqFOil23Ea/Y4X7Tn3xf2JPFWE2UUWOqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OGHeS/btqFOil23Ea/Y4X7Tn3xf2JPFWE2UUWOqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOGHeS%2FbtqFOil23Ea%2FY4X7Tn3xf2JPFWE2UUWOqK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;7. 컴퓨터 사양을 고려하여 메모리를 할당(2GB 이상) 후 다음버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Gpphw/btqFNaiDX4G/uDCKF94nOHK29CKvCvJXR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Gpphw/btqFNaiDX4G/uDCKF94nOHK29CKvCvJXR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Gpphw/btqFNaiDX4G/uDCKF94nOHK29CKvCvJXR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGpphw%2FbtqFNaiDX4G%2FuDCKF94nOHK29CKvCvJXR1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;8. 가상디스크를 만들기 위해 만들기 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfga7P/btqFOh1JIu3/p9o3yyWCKkK4vNpDk6qZGK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfga7P/btqFOh1JIu3/p9o3yyWCKkK4vNpDk6qZGK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfga7P/btqFOh1JIu3/p9o3yyWCKkK4vNpDk6qZGK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbfga7P%2FbtqFOh1JIu3%2Fp9o3yyWCKkK4vNpDk6qZGK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;9. 기본 설정을 유지한채 다음 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0YmXq/btqFOhnaFbv/fRvNuuweKdT01P8LaLd1P1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0YmXq/btqFOhnaFbv/fRvNuuweKdT01P8LaLd1P1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0YmXq/btqFOhnaFbv/fRvNuuweKdT01P8LaLd1P1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0YmXq%2FbtqFOhnaFbv%2FfRvNuuweKdT01P8LaLd1P1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;10. 동적 할당을 선택 후 다음 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/u3ZJU/btqFNqZAASk/nODyKIdk8WkbYrWoTUkFB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/u3ZJU/btqFNqZAASk/nODyKIdk8WkbYrWoTUkFB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/u3ZJU/btqFNqZAASk/nODyKIdk8WkbYrWoTUkFB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fu3ZJU%2FbtqFNqZAASk%2FnODyKIdk8WkbYrWoTUkFB0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;11.&amp;nbsp; PC 사양을 고려하여 이미지 할당 크기를 지정한 다음 저장 위치를 지정합니다. 완료 후 만들기 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ycqRC/btqFNEJ7O1W/Vb5rQHVG3MI95RXKvhw110/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ycqRC/btqFNEJ7O1W/Vb5rQHVG3MI95RXKvhw110/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ycqRC/btqFNEJ7O1W/Vb5rQHVG3MI95RXKvhw110/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FycqRC%2FbtqFNEJ7O1W%2FVb5rQHVG3MI95RXKvhw110%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;12. Network 및 기타 설정을 위해 생성된 머신 선택 &amp;gt; 마우스 오른쪽 클릭 &amp;gt; 설정 버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOBh0K/btqFLtQyVca/OSH83RdT7q0ia6ICO5a6V0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOBh0K/btqFLtQyVca/OSH83RdT7q0ia6ICO5a6V0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOBh0K/btqFLtQyVca/OSH83RdT7q0ia6ICO5a6V0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOBh0K%2FbtqFLtQyVca%2FOSH83RdT7q0ia6ICO5a6V0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;13. CPU 설정을 위해 시스템 &amp;gt; 프로세서 탭에서 CPU 개수를 2개로 설정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJefN3/btqFNgQyfoL/iFMkbrQHQn0qWe2k3f8zd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJefN3/btqFNgQyfoL/iFMkbrQHQn0qWe2k3f8zd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJefN3/btqFNgQyfoL/iFMkbrQHQn0qWe2k3f8zd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJefN3%2FbtqFNgQyfoL%2FiFMkbrQHQn0qWe2k3f8zd0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;14. Ubuntu 이미지를 설정하기 위해서 저장소 &amp;gt; 비어 있음 &amp;gt; 디스크 버튼 &amp;gt; Choose a disk file... 버튼을 클릭합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLTLzE/btqFN3inTAh/obo0EtGA6qv7ogO5acE0LK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLTLzE/btqFN3inTAh/obo0EtGA6qv7ogO5acE0LK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLTLzE/btqFN3inTAh/obo0EtGA6qv7ogO5acE0LK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLTLzE%2FbtqFN3inTAh%2Fobo0EtGA6qv7ogO5acE0LK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;15. 다운로드 받은 Ubuntu 이미지를 선택한 다음 열기 버튼을 누릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bghvuf/btqFNrKYjhl/2nwvQP5XgFhOTwFDVog8O1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bghvuf/btqFNrKYjhl/2nwvQP5XgFhOTwFDVog8O1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bghvuf/btqFNrKYjhl/2nwvQP5XgFhOTwFDVog8O1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbghvuf%2FbtqFNrKYjhl%2F2nwvQP5XgFhOTwFDVog8O1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;16. Ubuntu 이미지로 설정된 것을 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC8Yt1/btqFMHVm1Eh/Fg2kmcymfJblbPCgY8KZlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC8Yt1/btqFMHVm1Eh/Fg2kmcymfJblbPCgY8KZlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC8Yt1/btqFMHVm1Eh/Fg2kmcymfJblbPCgY8KZlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC8Yt1%2FbtqFMHVm1Eh%2FFg2kmcymfJblbPCgY8KZlK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;17. 네트워크 &amp;gt; 어댑터1에서 이름을 어댑터 브리지로 선택합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAOTZG/btqFNsQHUdg/8SMLNOKTQzLSq6NLCFB8o1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAOTZG/btqFNsQHUdg/8SMLNOKTQzLSq6NLCFB8o1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAOTZG/btqFNsQHUdg/8SMLNOKTQzLSq6NLCFB8o1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAOTZG%2FbtqFNsQHUdg%2F8SMLNOKTQzLSq6NLCFB8o1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;18. Host 네트워크 설정을 위해 어댑터 2 &amp;gt; 네트워크 어댑터 사용하기 클릭 &amp;gt; 호스트 전용 어댑터를 선택합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cq6kQ0/btqFLsYsMGJ/lJRJSYzNgBZaXonBKAfoB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cq6kQ0/btqFLsYsMGJ/lJRJSYzNgBZaXonBKAfoB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cq6kQ0/btqFLsYsMGJ/lJRJSYzNgBZaXonBKAfoB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcq6kQ0%2FbtqFLsYsMGJ%2FlJRJSYzNgBZaXonBKAfoB0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;확인 버튼을 눌러 종료합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Ubuntu 설치를 위한 기본적인 설정은 마무리 되었습니다.&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 Ubuntu 설치 및 기본적인 환경 구성을 진행하겠습니다.&lt;/p&gt;</description>
      <category>Cloud/Kubernetes</category>
      <category>docker</category>
      <category>docker 설치</category>
      <category>k8s</category>
      <category>Kubernetes</category>
      <category>ubuntu</category>
      <category>Ubuntu 설치</category>
      <category>VirtualBox</category>
      <category>쿠버네티스</category>
      <category>쿠버네티스 설치</category>
      <category>쿠버네티스 클러스터</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/90</guid>
      <comments>https://cla9.tistory.com/90#entry90comment</comments>
      <pubDate>Sat, 18 Jul 2020 13:15:13 +0900</pubDate>
    </item>
    <item>
      <title>7. Reification</title>
      <link>https://cla9.tistory.com/52</link>
      <description>&lt;p&gt;지금까지 Generic 기초 및 동작 원리에 대해서 살펴봤습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Generic을 활용한 Type erasure 동작 방법을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;경계가 표시된 Type은 경계로 컴파일시에 치환된다&lt;/li&gt;
&lt;li&gt;Unbounded Wildcard(?) 타입은 Object 타입으로 치환된다&lt;/li&gt;
&lt;li&gt;타입 안정성 보장을 위해서 캐스팅 연산을 수행한다.&lt;/li&gt;
&lt;li&gt;Generic Type에 대한 다형성 지원을 위해서 Bridge 메소드를 추가한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;개인적으로 Generic이 도입은 Java를 사용함에 있어 많은 편의성을 가져다 줬지만, 이에 못지않은 불편함 또한 주었다고 생각합니다. 즉 Type erasure로 인하여 하위 호환성을 유지할 순 있었지만, 한참이 지난 지금도 런타임시 타입을 알지 못하는 불편함으로 인해 개발자가 코드 작성시에 주의를 기울여야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에는 Type erasure에 의해서 런타임시 표현되는 Unknown Type 불일치로 인해 주의해야할 점을 다루도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reifiable Type&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Java에서 Runtime시에 완전하게 오브젝트 정보를 표현할 수 있는 타입을 가르켜 Reifiable 하다고 합니다.&lt;/p&gt;
&lt;p&gt;즉 Compile 단계에서 Type erasure에 의해서 지워지지 않는 타입 정보를 말합니다.&lt;/p&gt;
&lt;p&gt;Reifiable 가능한 타입 정보는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;원시 타입(int, double, float, byte 등등)&lt;/li&gt;
&lt;li&gt;Number, Integer와 같은 일반 클래스와 인터페이스 타입&lt;/li&gt;
&lt;li&gt;Unbounded Wildcard가 포함된 Parameterized Type(List&amp;lt;?&amp;gt;, ArrayList&amp;lt;?&amp;gt; 등)&lt;/li&gt;
&lt;li&gt;Raw Type(List, ArrayList, Map 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Unbounded Wildcard는 애초에 타입 정보를 전혀 명시하지 않았기 때문에, 컴파일시에 Type erasure 한다고 해도 잃을 정보가 없습니다. 따라서 Reifiable 하다고 볼 수 있으며, 컴파일시점에 Object로 치환됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그외 나머지는 Generic을 사용하지 않았기 때문에 타입 정보가 그대로 남습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;반면 아래 타입의 경우에는 Reifiable 하지 않습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Generic Type(T)&lt;/li&gt;
&lt;li&gt;Parameterized Type(List&amp;lt;Number, ArrayList&amp;lt;String&amp;gt; 등)&lt;/li&gt;
&lt;li&gt;경계가 포함된 Parameterized Type(List&amp;lt;? extends Number&amp;gt;, List&amp;lt;? super String&amp;gt; 등)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Reifiable 하지 않은 경우에 대해서 차근차근 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;우선 Generic Type은 Type erasure에 의하여 삭제되는 것은 이전의 포스팅들을 통해서 이해되셨을 것으로 생각합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Parameterized Type의 경우에는 Type erasure에 의하여 컴파일시에 Raw Type으로 변경됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;가령 List&amp;lt;String&amp;gt;, List&amp;lt;Integer&amp;gt;, List&amp;lt;List&amp;lt;String&amp;gt;&amp;gt;의 타입정보는 컴파일 시에 타입 안정성 검증 용도로 사용될 뿐 컴파일이 완료되면 Raw Type인 List로 치환됩니다. 따라서 Reifiable 하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;경계가 포함된 Parameterized Type 또한, 컴파일 시 타입 안정성 검증 용도로만 사용되므로 동일합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 위와같은 특성으로 인하여 주의해야할 점이 무엇이 있을까요?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Instance Test&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583846886998&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Integer  a = 3;
        System.out.println(isIntegerType(a));
    }
    public static boolean isIntegerType(Object b){
        return b instanceof Integer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;전달받은 타입이 Integer인지 확인하는 메소드를 구현한다고 가정합시다. 가장 심플한 방법은 instanceof 문법을 사용해서 Integer 임을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그럼 해당 메소드를 Generic 메소드로 변경해도 문제 없을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583847056676&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Integer  a = 3;
        System.out.println(isIntegerType(a));
    }
    public static &amp;lt;T&amp;gt; boolean isIntegerType(T b){
        return b instanceof Integer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드를 수행하면 문제없이 동작합니다. 그 이유는 Integer 타입은 런타임시에도 Integer 타입이기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583847286710&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        List&amp;lt;Integer&amp;gt; ints = Arrays.asList(1,2,3);
        System.out.println(isIntegerList(ints));
    }
    public static &amp;lt;T&amp;gt; boolean isIntegerList(T b){
        return b instanceof List&amp;lt;Integer&amp;gt;; //Compile 에러 발생
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 List&amp;lt;Integer&amp;gt; 타입임을 확인하는 메소드를 생성한다고 가정해봅시다. 위와같이 만들었을 때 정상적으로 동작할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;우리는 런타임시에 List의 Type Argument가 Integer임을 확인하고 싶으나 컴파일 이후에는 Raw Type으로 변경됩니다. 따라서 List&amp;lt;Integer&amp;gt; 임을 확인할 수 없기 때문에 애초에 컴파일 에러가 발생합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583847452522&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        List&amp;lt;Integer&amp;gt; ints = Arrays.asList(1,2,3);
        System.out.println(isList(ints));
    }
    public static &amp;lt;T&amp;gt; boolean isList(T b){
        return b instanceof List;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;물론 Raw Type 자체는 Reifiable 하기 때문에, 위와 같이 Raw type으로 명시하면 컴파일 에러도 발생하지 않고 정상적으로 동작합니다. 하지만 여전히 해당 List의 Element 타입이 Integer임은 알 수 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583848120082&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        List&amp;lt;Integer&amp;gt; ints = Arrays.asList(1,2,3);
        System.out.println(isList(ints));
    }
    public static &amp;lt;T&amp;gt; boolean isList(T b){
        return b instanceof List&amp;lt;?&amp;gt;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;가급적이면 Raw type을 사용하는 것은 지양하는 것이 좋으므로 Reifiable한 Wildcard를 사용하여 동일한 구문을 표현할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;일반적으로 instanceof 문법을 사용한 코드는 code smell로 표현하여 잘 사용하지는 않지만, 만약 사용한다 할지라도 반드시 Reifiable한 타입에만 사용할 수 있다는점을 유의합시다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐스팅&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583847825104&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        List objs = Arrays.asList(1, 2, 3);
        List&amp;lt;Integer&amp;gt; ints = (List&amp;lt;Integer&amp;gt;)objs;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같이 Raw Type의 List에 Integer 타입 원소만 있다고 가정할 때, 이를 List&amp;lt;Integer&amp;gt; 타입으로 캐스팅할 수 있을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;실제로 어플리케이션으로 실행하면, 정상적으로 수행되는 것으로 보입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RFgdv/btqCz5ZedaA/1w6t1wMbvtx2NhOdwVGUs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RFgdv/btqCz5ZedaA/1w6t1wMbvtx2NhOdwVGUs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RFgdv/btqCz5ZedaA/1w6t1wMbvtx2NhOdwVGUs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRFgdv%2FbtqCz5ZedaA%2F1w6t1wMbvtx2NhOdwVGUs0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 자세히 보면, cast 관련 Warning이 발생된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그 이유는 마찬가지로 컴파일이 완료된 이후에는 타입정보가 없어지는데, List&amp;lt;Integer&amp;gt; 타입으로의 안전한 캐스팅을 보장할 수 없기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 Instance Test에서는 에러를 발생시켰는데, 캐스팅에서는 Warning을 발생시킨 것일까요?&lt;/p&gt;
&lt;p&gt;다음 예제코드를 보며, 그 이유를 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583849086044&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static List&amp;lt;Integer&amp;gt; getAsIntegerList(List&amp;lt;?&amp;gt; objs){
    objs.forEach(o-&amp;gt;{
        if(!(o instanceof Integer))
            throw new ClassCastException();
    });
    return (List&amp;lt;Integer&amp;gt;)objs;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;List 오브젝트를 전달받아, 해당 List안에 있는 원소가 모두 Integer 일경우에는 List&amp;lt;Integer&amp;gt;로 캐스팅하여 반환하는 메소드를 구현한다고 가정해봅시다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드에는 이미 List 원소를 탐색하면서 Integer가 아닌 원소가 하나라도 존재한다면 ClassCastException이 발생합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;즉 List&amp;lt;Integer&amp;gt;로 캐스팅하는 코드에 도달한다면 이미 objs 리스트에는 모든 원소가 Integer 임이 반드시 보장됩니다.&lt;/p&gt;
&lt;p&gt;따라서 java에서는 개발자가 의도적으로 위 코드와 같은 캐스팅을 수행할 수 있을 수도 있기때문에 에러가 아닌 Warning을 발생시킵니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서, 만약 위와같이 의도적으로 Non-Reifiable 타입에 대해서 캐스팅을 수행했다면 &lt;span&gt;@SuppressWarnings&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;unchecked&quot;&lt;/span&gt;&lt;span&gt;) 어노테이션을 붙여서 컴파일 시점에 Warning이 발생하지 않도록 처리해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Exception&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583849727398&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class MyException&amp;lt;T&amp;gt; extends Exception{}


try {
   ....
}catch (MyException&amp;lt;Integer&amp;gt; e){
   ....   
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Throwable을 상속받는다면 Generic 클래스를 생성할 수 없습니다. 또한 catch 구문에서 Parameterized Type을 사용할 수 없습니다. 이는 이전의 설명과 마찬가지로 런타임 시점에 타입확인이 불가하기 때문입니다.&lt;/p&gt;</description>
      <category>JAVA/Generic</category>
      <category>generic</category>
      <category>reification</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/52</guid>
      <comments>https://cla9.tistory.com/52#entry52comment</comments>
      <pubDate>Tue, 10 Mar 2020 23:18:16 +0900</pubDate>
    </item>
    <item>
      <title>6. Generic 메소드</title>
      <link>https://cla9.tistory.com/51</link>
      <description>&lt;p&gt;Integer 값이 들어있는 List 원소중 가장 큰 값을 리턴하는 함수를 만든다고 가정해봅시다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583757929496&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static int max(Collection&amp;lt;Integer&amp;gt; cols){
    return cols.stream().max((o1, o2) -&amp;gt; o1.compareTo(o2)).get();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;대략 위와같이 구현할 수 있습니다. 그럼 Integer 타입이아닌 비교가 가능한 모든 클래스 타입이 사용될 수 있도록 하려면 어떻게 해야할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그동안 학습한 내용을 적용하면서 이번 포스팅 주제를 다루어보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Step 1. Generic 사용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1583758140800&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; T max(Collection&amp;lt;T&amp;gt; cols){
    return cols.stream().max((o1, o2) -&amp;gt; o1.compareTo(o2)).get(); //Compile 에러 발생
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Get and Put 원리 고민없이 작성한다면 위와같이 작성할 수 있습니다. 여기서 첫번째 등장하는 &amp;lt;T&amp;gt;는 T 타입의 경계를 지정합니다. 이는 &lt;a href=&quot;https://cla9.tistory.com/44&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;첫번째 포스팅&lt;/u&gt;&lt;/a&gt;에서 클래스 멤버 변수 타입의 경계 설정과 동일한 개념입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 이를 실행하면 두 원소의 대소 비교부분인 compareTo 메소드에서 컴파일 에러가 발생하게됩니다.&lt;/p&gt;
&lt;p&gt;과연 무엇이 문제일까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;첫번째 등장하는 &amp;lt;T&amp;gt; 구문은 T 타입의 경계를 지정한다고 했습니다.&lt;/p&gt;
&lt;p&gt;위 코드는 Generic 타입이 존재한다는 것만 지정할 뿐, 해당 타입에 대한 경계를 지정하지 않았습니다.&lt;/p&gt;
&lt;p&gt;이는 어떠한 타입이 지정되어도 상관없다는 의미입니다. 문제는 여기서 발생합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;compareTo 메소드를 사용하기 위해서는 해당 클래스에 Comparable 인터페이스가 구현되어있어야 합니다.&lt;/p&gt;
&lt;p&gt;하지만 T 타입에는 어떠한 타입도 허용가능하므로 문제가 발생할 수 있습니다. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;따라서 허용되지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이와 같은 문제를 해결하기 위해서는 Comparable 인터페이스가 구현된 타입만 지정되도록 경계를 정의해야합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Step2. 타입 경계 적용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1583760702478&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt; public static &amp;lt;T extends Comparable&amp;lt;T&amp;gt;&amp;gt; T max(Collection&amp;lt;T&amp;gt; cols){
    return cols.stream().max((o1, o2) -&amp;gt; o1.compareTo(o2)).get();
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;메소드 레벨에서 타입 경계를 Comparable로 지정하면, Comparable Subtype 만 지정될 수 있습니다. 따라서 메소드 내부에서 compareTo 사용이 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Step3. Get and Put 원리 적용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1583760774314&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;T extends Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(Collection&amp;lt;? extends T&amp;gt; cols){
    return cols.stream().max((o1, o2) -&amp;gt; o1.compareTo(o2)).get();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Get and Put 원리를 적용하면 위와같이 변경 가능합니다. 그 이유는 Collection 내부에서 바깥으로 T의 Subtype 값을 꺼내며, Comparable 인터페이스 compareTo 메소드안으로 값을 넣기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Step4. Method Reference 적용&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1583760884481&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;T extends Comparable&amp;lt;? super T&amp;gt;&amp;gt; T max(Collection&amp;lt;? extends T&amp;gt; cols){
    return cols.stream().max(T::compareTo).get();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;추가로 Generic 타입에 메소드 레퍼런스를 적용하면 위와같이 작성할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Bridge 메소드&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583763282333&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Comparable{
	public int compareTo(Object o);
}    &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 Bridge 메소드에 대해서 알아보겠습니다. Java 1.4 버전 이하에서는 Generic 지원이 없었기 때문에 Comparable 인터페이스 구조는 위와 같습니다. 이때 파라미터는 모든 타입을 허용해야하므로 Object 타입인 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583763240922&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Integer implements Comparable{
    private final int value;
    public Integer(int value) { this.value = value; }
    public int compareTo(Integer o){
        return (this.value &amp;lt; o.value) ? -1 : (value == o.value ? 0 : 1 );
    }
    @Override
    public int compareTo(Object o) {
        return compareTo((Integer)o);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 구현해야하는 Integer, Double 등의 클래스에서는 올바른 비교 처리를 위해 Object를 자기 자신의 클래스 타입으로 치환하여 처리하도록 부가적인 메소드 구현이 필요했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583763557095&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Comparable&amp;lt;T&amp;gt;{
	public int compareTo(T o);
}    &lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1583763582326&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Integer implements Comparable&amp;lt;Integer&amp;gt;{
    private final int value;
    public Integer(int value) { this.value = value; }
    @Override
    public int compareTo(Integer o){
        return (this.value &amp;lt; o.value) ? -1 : (value == o.value ? 0 : 1 );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Generic 등장 이후 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;compareTo 메소드에 Object가 아닌 명확한 타입 지정이 가능해졌습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;따라서 이를 구현하는 클래스에서도 Object 타입이 아닌 자신이 지정한 타입으로의 메소드 구현이 가능해졌습니다.&lt;/p&gt;
&lt;p&gt;그 결과 Bridge 메소드 없이 1개의 메소드만 구현하면 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;하지만 여기서 한가지 의문이 발생합니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;'&lt;i&gt;Java는 하위 호환성을 중요시 여기는 언어인데, 위와 예시와 같이 Generic 도입에 따라서 메소드 수가 줄어들게 되면, 이는 Integer 클래스 구조가 변경되므로 하위 호환성을 유지할 수 있을까?&lt;/i&gt;'&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;실제 구조가 변경되었는지 확인을 위해 Reflection을 활용해서 런타임시 compareTo 메소드 정보를 출력해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583762871699&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Stream.of(Integer.class.getDeclaredMethods())
                .filter(m -&amp;gt; &quot;compareTo&quot;.equals(m.getName()))
                .map(Method::toGenericString)
                .forEach(System.out::println);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;public int &lt;a href=&quot;java.lang.Integer.compareTo(java.lang.Object)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java.lang.Integer.compareTo(java.lang.Object)&lt;/a&gt; &lt;br /&gt;public&amp;nbsp;int&amp;nbsp;&lt;a href=&quot;java.lang.Integer.compareTo(java.lang.Integer)&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java.lang.Integer.compareTo(java.lang.Integer)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;출력 결과 2개의 메소드가 존재함을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이를 통해 알 수 있는 사실은 Generic 타입 파라미터가 포함된 메소드를 오버로딩 할경우 Bridge 메소드를 자동으로 추가하여 레거시 코드와 호환성을 유지한다는 점입니다.&lt;/p&gt;</description>
      <category>JAVA/Generic</category>
      <category>Bridge 메소드</category>
      <category>generic</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/51</guid>
      <comments>https://cla9.tistory.com/51#entry51comment</comments>
      <pubDate>Tue, 10 Mar 2020 00:01:49 +0900</pubDate>
    </item>
    <item>
      <title>5. Wildcard</title>
      <link>https://cla9.tistory.com/50</link>
      <description>&lt;h3&gt;Unbounded Wildcard&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Collections 클래스에는 reverse API가 있습니다. 해당 API는 주어진 Collection내 원소들을 역순으로 뒤집어주는 기능을 수행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583500410202&quot; class=&quot;java&quot; style=&quot;overflow: auto; padding: 15px; margin: 20px auto 0px; max-width: 100%; border: 1px solid #dddddd; font: 400 14px / 25.2px Menlo, Consolas, Monaco, monospace; background: #f6f7f8; border-radius: 3px; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; display: block; color: #383a42; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Collections {
    ....
    public static void reverse(List&amp;lt;?&amp;gt; list){}
    ....
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 파라미터 변수를 살펴보면 wildcard가 사용되었지만 그 어떠한 경계도 지정되지 않은 형태로 사용되었습니다.&lt;/p&gt;
&lt;p&gt;이는 말 그대로 타입 변수가 어떠한 것이든 상관이 없음을 의미합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자세히 보면 List&amp;lt;Object&amp;gt; 와 의미가 비슷하다고 생각할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 List&amp;lt;?&amp;gt;와 List&amp;lt;Object&amp;gt;는 다릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583500410203&quot; class=&quot;java&quot; style=&quot;overflow: auto; padding: 15px; margin: 20px auto 0px; max-width: 100%; border: 1px solid #dddddd; font: 400 14px / 25.2px Menlo, Consolas, Monaco, monospace; background: #f6f7f8; border-radius: 3px; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; display: block; color: #383a42; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main{
   public void static main(int argc, String[] argv){
       List&amp;lt;Object&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();
       ints.add(1);
       ints.add(2);
       List&amp;lt;?&amp;gt; objs;
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;List&amp;lt;Object&amp;gt;에는 Object 의 Subtype이 할당될 수 있지만, List&amp;lt;?&amp;gt;에는 오직 null 만이 할당될 수 있습니다. null만 할당이 가능하다는 점에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;List&amp;lt;? extends Object&amp;gt;&lt;/b&gt;&lt;/span&gt;와 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Unbounded Wildcard 의미는 전달되는 변수의 타입이 무엇인지 정확히는 모르나 타입은 존재하며, 관심대상은 파라미터 타입이 아닌 객체가 제공하는 기능입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;즉 위 예시의 경우에는 관심대상은 List 인터페이스가 제공하는 기능이지 그 안에서 제공되는 타입들이 제공되는 기능은 관심대상은 아닙니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Unbounded Wildcard 내용을 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Object 클래스에 의하여 제공되는 기본적인 기능을 사용할 경우&lt;/li&gt;
&lt;li&gt;타입 파라미터에 의존적이지 않은 Generic Class에서 제공되는 기능을 사용하는 경우&lt;/li&gt;
&lt;li&gt;매개 변수로 전달되는 실제 타입은 관심 대상이 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3&gt;Wildcard Capture&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Get and Put 원리를 적용하여 reverse API를 직접 구현해봅시다. 메소드 수행 동작상에서 List의 원소가 제공하는 기능은 관심대상이 아니기에 다음과 같이 Spec을 정의할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583502023863&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void reverse(List&amp;lt;?&amp;gt; list)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;메소드를 구현하면 대략 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583502282245&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void reverse(List&amp;lt;?&amp;gt; list){
    List&amp;lt;Object&amp;gt; tmp = new ArrayList&amp;lt;&amp;gt;(list);
    for(int i =0;i &amp;lt; tmp.size();i++){
        list.set(i, tmp.get(i));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;Error:(12,&amp;nbsp;32)&amp;nbsp;java:&amp;nbsp;incompatible&amp;nbsp;types:&amp;nbsp;&lt;a href=&quot;java.lang.Object&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java.lang.Object&lt;/a&gt;&amp;nbsp;cannot&amp;nbsp;be&amp;nbsp;converted&amp;nbsp;to&amp;nbsp;capture#1&amp;nbsp;of&amp;nbsp;?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 메소드에서 tmp 리스트의 타입 인자가 Object인 이유는 전달받는 타입 정보를 모르기 때문에 모든 타입을 수용할 수 있도록 Object로 지정하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 메소드를 수행하면 에러가 발생하는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;무엇이 문제였을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583501454668&quot; class=&quot;java&quot; style=&quot;overflow: auto; padding: 15px; margin: 20px auto 0px; max-width: 100%; border: 1px solid #dddddd; font: 400 14px / 25.2px Menlo, Consolas, Monaco, monospace; background: #f6f7f8; border-radius: 3px; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; display: block; color: #383a42; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface List&amp;lt;?&amp;gt; extends Collection&amp;lt;?&amp;gt;{
    ...
    ? get(int index);
    ? set(int index, ? element)
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cla9.tistory.com/46?category=814455&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;상위 경계 포스팅&lt;/u&gt;&lt;/a&gt;에서 다루었던 내용을 잠시 복습해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Wildcard로 타입 인자가 지정되었을 때 타입에 대한 정보를 모르기때문에, 위 인터페이스에서 set 메소드를 호출할때마다 이를 지칭하기 위해 capture# of ?와 같은 형태로 컴파일러가 임의의 타입 변수를 할당합니다. 이를 Wildcard Capture라고 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;문제는 알지 못하는 임의의 타입 T를 가르키는 &lt;span style=&quot;color: #333333;&quot;&gt;capture# of ? 변수는 컴파일러가 내부적으로만 사용하고 타입 제약조건을 공식화하는데 사용하지 않는다는 점입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 Object 타입의 값을 알 수 없는 List에 집어넣으려고 하기 때문에 에러가 발생합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583653769191&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void reverse(List&amp;lt;T&amp;gt; list){
    List&amp;lt;T&amp;gt; tmp = new ArrayList&amp;lt;&amp;gt;(list);
    for(int i =0;i &amp;lt; tmp.size();i++){
        list.set(i, tmp.get(i));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;가장 심플한 방법은 reverse API를 위와 같이 타입 파라미터를 명시하는 방법입니다.&lt;/p&gt;
&lt;p&gt;하지만, 관심대상이 아닌 T 타입을 컴파일러 제약때문에 API에 넣는다는 것은 그다지 좋은 디자인이 아닙니다.&lt;/p&gt;
&lt;p&gt;따라서 API 디자인 규칙을 잘 준수하면서도 기능 동작을 구현하기위해&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;일종의 컴파일러 트릭을 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d8LDmy/btqCynZpKTS/awBkNak7KavPL2YDZc5cKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d8LDmy/btqCynZpKTS/awBkNak7KavPL2YDZc5cKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d8LDmy/btqCynZpKTS/awBkNak7KavPL2YDZc5cKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd8LDmy%2FbtqCynZpKTS%2FawBkNak7KavPL2YDZc5cKK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드는 Helper 메소드를 사용하여 문제를 해결하였습니다. 즉 reverse 메소드를 호출할 때는 Wildcard Capture 정보를 타입 제약 조건으로 사용되지 않았습니다. 하지만 내부적으로 reverseHelper 메소드를 호출할때, 파라미터 타입에 T 타입을 명시하였기 때문에, capture# of ?와 같은 정보를 타입으로써 지정할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이후 동일 타입내에서 값을 추출하고 넣음을 보장할 수 있기때문에 값 할당이 허용됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Wildcard 제한&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583658373159&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args)  {
        List&amp;lt;?&amp;gt; list = new ArrayList&amp;lt;?&amp;gt;(); //Compile 에러 발생
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tfjc0/btqCuYsPCE0/icoILxAj2wYDzMyuMcVzlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tfjc0/btqCuYsPCE0/icoILxAj2wYDzMyuMcVzlK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tfjc0/btqCuYsPCE0/icoILxAj2wYDzMyuMcVzlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftfjc0%2FbtqCuYsPCE0%2FicoILxAj2wYDzMyuMcVzlK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Wildcard를 사용하면, 이를 기반으로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;인스턴스 생성이 불가&lt;/b&gt;&lt;/span&gt;합니다.&lt;/p&gt;
&lt;p&gt;만약, 타입 자동 추론을 이용하여 위 그림과 같이 list를 생성 할지라도, add 수행시 알 수 없는 타입이기 때문에 null외에 값 허용이 안됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이러한 제한을 둔 배경에는 Wildcard는 구체적인 타입 파라미터가 지정된 객체를 가르키는 것이기 때문에, 타입을 알 수 없는 Wildcard가 지정된 인스턴스 생성은 바람직하지 않기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 Wildcard가 쓰여있다고 해서 모든 객체가 생성 불가능한 것은 아닙니다.&lt;/p&gt;
&lt;p&gt;Nested 형태로 Wildcard가 명시된 구조에서는 생성 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583658605071&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args)  {
        List&amp;lt;List&amp;lt;?&amp;gt;&amp;gt; lists = new ArrayList&amp;lt;List&amp;lt;?&amp;gt;&amp;gt;();
        List&amp;lt;Integer&amp;gt; ints = Arrays.asList(1, 2, 3);
        List&amp;lt;Double&amp;gt; dbls = Arrays.asList(1.0, 2.0);
        lists.add(ints);
        lists.add(dbls);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다만, 위에서도 볼 수 있듯이 Nested 형태 Wildcard가 포함된 인스턴스 또한 다른 List의 타입 파라미터를 가르키는 용도로 사용될 뿐 Wildcard 자체가 타입으로써 사용되지는 않습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JAVA/Generic</category>
      <category>generic</category>
      <category>wildcard</category>
      <category>wildcard capture</category>
      <category>제약</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/50</guid>
      <comments>https://cla9.tistory.com/50#entry50comment</comments>
      <pubDate>Sun, 8 Mar 2020 18:35:37 +0900</pubDate>
    </item>
    <item>
      <title>4. Wildcard with super(하위 경계)</title>
      <link>https://cla9.tistory.com/48</link>
      <description>&lt;pre id=&quot;code_1583497460423&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Collections {
    ....
    public static &amp;lt;T&amp;gt; void copy(List&amp;lt;? super T&amp;gt; dest, List&amp;lt;? extends T&amp;gt; src){}
    ....
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Collections 클래스는 List와 관련된 여러 유용한 기능을 제공합니다. 그 중 copy API를 통해서 이번절 내용을 살펴보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;copy는 src 변수가 가르키는 List의 데이터 값을 dest 변수가 가르키는 List에 저장하는 기능을 제공합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;여기서 우리는 지금까지 보지 못한 구문을 또 발견하게됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;u&gt;? super T&lt;/u&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&amp;lt;T&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2번째 내용은 다음 포스팅에서 다루기로하며, 이번 절에서는 ? super T 구문에 대해서 살펴보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;? super T는 무엇을 의미할까요?&lt;/p&gt;
&lt;p&gt;이는 T 타입 뿐만 아니라 T 타입의 모든 Supertype까지 허용하겠다는 의미입니다.&lt;/p&gt;
&lt;p&gt;즉 T 타입이 Integer라면 List&amp;lt;Integer&amp;gt; 외에 List&amp;lt;Number&amp;gt; 및 List&amp;lt;Object&amp;gt; 형태 할당을 허용하겠다는 의미입니다.&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이러한 특성을 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Contravariant&lt;/b&gt; &lt;/span&gt;라고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;또한, Parameter Type을 T 타입 이상으로 제한한다고해서 하위 경계(Lower bound)라고 말합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;예제를 통해서 개념을 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583497460426&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args)  {
        List&amp;lt;Object&amp;gt; objs = Arrays.asList(2, 3.14, &quot;Penguin&quot;);
        List&amp;lt;Integer&amp;gt; ints = Arrays.asList(1, 2);
        Collections.copy(objs, ints);
        System.out.println(objs);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드는 Object List에 Integer List 값을 추가하는 코드입니다.&lt;/p&gt;
&lt;p&gt;여기에서 Collections.copy 메소드는 objs 객체, ints 객체에도 속하지 않습니다. 따라서 메소드 호출 시에 어떠한 타입으로 호출될지 컴파일러가 추론하거나 개발자가 명시적로 호출시 타입을 지정해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이때 지정될 수 있는 타입의 종류는 Integer, Number(Integer의 Supertype 이므로), Object입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot;&gt;
&lt;li&gt;Collections.copy(objs, ints);&lt;/li&gt;
&lt;li&gt;Collections.&amp;lt;Integer&amp;gt;copy(objs, ints);&lt;/li&gt;
&lt;li&gt;Collections.&amp;lt;Number&amp;gt;copy(objs,ints);&lt;/li&gt;
&lt;li&gt;Collections.&amp;lt;Object&amp;gt;copy(objs,ints);&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583497460427&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; void copy(List&amp;lt;T&amp;gt; dest, List&amp;lt;? extends T&amp;gt; src){}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이러한 상황에서 만약 copy 메소드의 dst 변수 타입이 하위 경계가 아닌 T 타입으로 설계되어 있었다면 어떠한 타입으로 추론될까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583497460427&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;Object&amp;gt; void copy(List&amp;lt;Object&amp;gt; dest, List&amp;lt;? extends Object&amp;gt; src){}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드를 만족시키기 위해서는 추론되는 타입이 Object 타입만 허용 가능합니다. 그 이유는 dest에 할당되는 Parameterized Type이 List&amp;lt;Object&amp;gt;이기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 위의 4가지 경우 중 1번과 4번 메소드 호출만 성공하고, 나머지 2개의 타입은 컴파일 오류가 발생합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583497460427&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; void copy(List&amp;lt;? super T&amp;gt; dest, List&amp;lt;? extends T&amp;gt; src){}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 원래의 copy 메소드처럼 상위 경계와 하위 경계를 같이 쓰게되는 경우에는 어떻게 될까요? 결과는 위 4가지 어떠한 타입으로 추론되더라도 문제가 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;타입을 직접 명시하는 경우를 기준으로 하나하나씩 살펴볼까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;1.&amp;nbsp; Collections.&amp;lt;Integer&amp;gt;copy()&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1583497460428&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;Integer&amp;gt; void copy(List&amp;lt;? super Integer&amp;gt; dest, List&amp;lt;? extends Integer&amp;gt; src){}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Object는 Integer의 SuperType이고, Integer는 자기 자신에 대한 Subtype이므로 위와 같이 추론될 경우 할당 가능합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 20px auto 0px; border: none; cursor: pointer !important; z-index: 1; font-size: 0px; line-height: 0; background: url('../image/divider-line.svg') center 0px / 200px 420px no-repeat; width: 64px; height: 4px; padding: 20px;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;b&gt;2. Collections.&amp;lt;Number&amp;gt;copy()&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1583497460428&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;Number&amp;gt; void copy(List&amp;lt;? super Number&amp;gt; dest, List&amp;lt;? extends Number&amp;gt; src){}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;Object는 Number Class의 SuperType이고, Integer &amp;lt;: Number 이므로 할당 가능합니다.&lt;/p&gt;
&lt;hr style=&quot;margin: 20px auto 0px; border: none; cursor: pointer !important; z-index: 1; font-size: 0px; line-height: 0; background: url('../image/divider-line.svg') center 0px / 200px 420px no-repeat; width: 64px; height: 4px; padding: 20px;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;b&gt;3. Collections.&amp;lt;Object&amp;gt;copy()&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1583497460428&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;Object&amp;gt; void copy(List&amp;lt;? super Object&amp;gt; dest, List&amp;lt;? extends Object&amp;gt; src){}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Object는 모든 Class의 SuperType이므로 할당 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 사실들을 기반으로 사용될 수 있는 타입 파라미터 종류를 조합해보면 다음과 같은 결과를 얻을 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;1. copy(List&amp;lt;T&amp;gt; dst, List&amp;lt;T&amp;gt; src)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;- 동일 타입만이 가능하므로, 위 예제에서는 해당 메소드를 사용할 수 없습니다.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;b&gt;2. copy(List&amp;lt;T&amp;gt; dst, List&amp;lt;? extends T&amp;gt; src)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;- dst가 Object 타입이므로 Object 타입 추론될 경우에만 허용 가능합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;3. copy(List&amp;lt;? super T&amp;gt; dst, List&amp;lt;T&amp;gt; src)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;- src가 Integer 이므로 Integer 타입 추론될 경우에만 허용 가능합니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;b&gt;4.&amp;nbsp;copy(List&amp;lt;?&amp;nbsp;suepr&amp;nbsp;T&amp;gt;&amp;nbsp;dst,&amp;nbsp;List&amp;lt;?&amp;nbsp;extends&amp;nbsp;T&amp;gt;&amp;nbsp;src)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&amp;nbsp;&amp;nbsp;- Object, Number, Integer 타입 어떤 것이 오든지 허용 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 통해 알 수 있는 사실은 적절한 경계를 사용함으로서 메소드의 타입을 추론하는데 있어서 한정된 타입만 사용되지 않도록하여 유연한 API 설계를 가능하도록 지원합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 하위 경계는 언제 주로 활용될까요?&lt;/p&gt;
&lt;p&gt;전달받은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;구조 내부로 T나 T의 Subtype으로 값을 입력하는 경우&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;주로 사용됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583497460430&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args)  {
        List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();
        ints.add(1);
        ints.add(3);
        List&amp;lt;? super Integer&amp;gt; ints2 = ints;
        ints.add(4);
        
        for(Integer i : ints2){   //Compile 에러 발생
           System.out.println(i);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 예제와 같이 ints2 객체에서는 Integer Subtype 값 입력이 가능합니다.&lt;/p&gt;
&lt;p&gt;반면, 해당 구조체로부터 값을 가져오려는 경우에는 컴파일 에러가 발생합니다.&lt;/p&gt;
&lt;p&gt;그 이유는, 컴파일러 입장에서 해당 객체는 Integer의 상위 타입은 모두 올 수 있는데, 그것을 Integer 타입 변수로 할당 받는 것은 적합하지 않기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 하위 경계에도 예외가 존재합니다. 즉 하위 경계는 주로 값 입력을 위해 사용되나, 특정 타입에 경우에는 값을 가져올 수 있습니다. 그것은 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Object&lt;/b&gt;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;타입입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583497460431&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args)  {
        List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();
        ints.add(1);
        ints.add(3);
        List&amp;lt;? super Integer&amp;gt; ints2 = ints;
        ints.add(4);
        
        for(Object i : ints2){  
           System.out.println(i);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그 이유는 Object는 모든 클래스의 부모 클래스이기 때문입니다.&lt;/p&gt;
&lt;p&gt;아래 예시를 통해서 자세히 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;List&amp;lt;? super Integer&amp;gt; 변수 사용시, 어떠한 타입이 올지 모르므로 해당 타입을 임의의 타입 변수 X라고 가정해봅시다.&lt;/p&gt;
&lt;p&gt;이때 Integer &amp;lt;: X이며, X &amp;lt;: Object 입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583499721342&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface List&amp;lt;X&amp;gt; extends Collection&amp;lt;X&amp;gt;{
    ...
    X get(int index);
    void add(X element);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 X의 Supertype인 Object의 반환은 허용됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3&gt;Get and Put Principle&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금까지 상위, 하위 경계를 학습하였습니다.&lt;/p&gt;
&lt;p&gt;이전의 예시들에서 확인하였듯이 Wildcard는 사용할 수 있을 때 가능하면 사용하는 것이 좋은 습관입니다.&lt;/p&gt;
&lt;p&gt;그 이유는 WIldcard 사용을 통해서 해당 API 호출하는 입장에서 전달하는 파라미터 내용이 내부적으로 어떻게 사용될지 유추가 가능할 뿐더러 유연한 타입 제공이 가능하기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 언제, 어떤 Wildcard를 사용하는 것이 좋을까요?&lt;/p&gt;
&lt;p&gt;이를 결정하기 위한 간단한 원칙이 있습니다.(Get and Put Principle)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;b&gt;하위 경계(? extends T)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 구조 바깥으로 T나 T의 Supertype으로 값을 읽어야 하는 경우 사용하자&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상위 경계(? super T)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 구조 내부로 T나 T의 Subtype으로 값을 입력해야하는 경우 사용하자&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전에 살펴본 copy 메소드를 통해 해당 원칙이 어떻게 적용되었는지 확인해봅시다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583655422063&quot; class=&quot;java&quot; style=&quot;overflow: auto; padding: 15px; margin: 20px auto 0px; max-width: 100%; border: 1px solid #dddddd; font: 400 14px / 25.2px Menlo, Consolas, Monaco, monospace; background: #f6f7f8; border-radius: 3px; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; display: block; color: #383a42; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static &amp;lt;T&amp;gt; void copy(List&amp;lt;? super T&amp;gt; dst, List&amp;lt;? extends T&amp;gt; src){}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Get and Put 원칙을 인지한 상태에서 위와 같은 API를 보면 다음과 같은 생각을 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;'&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;src 변수에 전달하는 List 객체에서는 내부에서 값을 추출하기 위한 용도로 사용되며, dst 변수에 전달하는 객체에서는 값을 입력하는 용도로 사용되겠구나&lt;/i&gt;&lt;/span&gt;'&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 Number 타입을 지닌 Collection의 합을 구하는 메소드를 직접 디자인한다면 어떻게 타입을 설계하는것이 좋을지 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583655422064&quot; class=&quot;java&quot; style=&quot;overflow: auto; padding: 15px; margin: 20px auto 0px; max-width: 100%; border: 1px solid #dddddd; font: 400 14px / 25.2px Menlo, Consolas, Monaco, monospace; background: #f6f7f8; border-radius: 3px; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; display: block; color: #383a42; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static double sum(Collection&amp;lt;Number&amp;gt; cols){
    double s = 0.0;
    for(Number col : cols)
    	s = s + col.doubleValue();
    return s;
}    &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 위와 같이 Number 타입 지정을 고려해볼 수 있습니다. 하지만 이는 이전에 줄곧 얘기한 List&amp;lt;Number&amp;gt; 만 허용 가능한 메소드이기에 사용의 제약이 있습니다. 또한 Get and Put 원리에 따르면 해당 Collection에 별도 값 입력을 하지 않기 때문에 상위 경계(? extends Number)를 사용하는 것이 적합해보입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 n을 입력하면 0부터 n-1까지 값을 채워주는 함수를 만들어 보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583655422064&quot; class=&quot;java&quot; style=&quot;overflow: auto; padding: 15px; margin: 20px auto 0px; max-width: 100%; border: 1px solid #dddddd; font: 400 14px / 25.2px Menlo, Consolas, Monaco, monospace; background: #f6f7f8; border-radius: 3px; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; display: block; color: #383a42; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public static void assign(Collection&amp;lt;? super Integer&amp;gt; ints, int n){
    for(int i = 0 ; i &amp;lt; n; i++) ints.add(i);
}    &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 경우에는 해당 Collection 안에 값을 채워넣는 작업이 수행되므로 하위 경계를 입력하는 것이 적합해보입니다.&lt;/p&gt;</description>
      <category>JAVA/Generic</category>
      <category>? super T</category>
      <category>generic</category>
      <category>wildcard</category>
      <category>하위 경계</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/48</guid>
      <comments>https://cla9.tistory.com/48#entry48comment</comments>
      <pubDate>Fri, 6 Mar 2020 21:24:25 +0900</pubDate>
    </item>
    <item>
      <title>3. Wildcard with extends(상위 경계)</title>
      <link>https://cla9.tistory.com/46</link>
      <description>&lt;p&gt;만약 Number 의 Subtype인 모든 List의 원소를 저장하는 메소드를 디자인 하려면 어떻게 해야할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583492096751&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Data{
    private List&amp;lt;Number&amp;gt; list;
    public void addAll(List&amp;lt;Number&amp;gt; cols){
        this.list.addAll(cols);
    }
}

public class Main {
    public static void main(String[] args)  {
        Data data = new Data();

        List&amp;lt;Integer&amp;gt; ints = Arrays.asList(1, 2, 3);
        List&amp;lt;Double&amp;gt; dbls = Arrays.asList(1.0, 2.8);
        List&amp;lt;Number&amp;gt; nums = Arrays.asList(5,4.5);
        
        data.addAll(ints); //Compile 에러 발생
        data.addAll(dbls); //Compile 에러 발생
        data.addAll(nums);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금까지 학습한 내용으로는 위와 같이 Data 클래스를 디자인 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;main 함수를 보면 Integer, Double, Number 관련 List 인스턴스를 생성했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;과연 우리의 의도대로 Data 객체내 addAll 함수를 호출할 수 있을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전 포스팅에서 살펴보았듯이 Generic은 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;invariant&lt;/span&gt; &lt;/b&gt;하기 때문에, 동일 타입의 Number 타입의 List를 제외하고 두 리스트에서의 메소드 호출은 컴파일 에러를 발생시킵니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 이 경우에는 어떻게 메소드 디자인을 디자인할 수 있을까요?&lt;/p&gt;
&lt;p&gt;Java에서는 이러한 요구를 충족되는 다양한 Wildcard 기법을 제공합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 상위 경계 타입 파라미터(Upper-bound Type Parameter)를 알아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583493017187&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Data{
    private List&amp;lt;Number&amp;gt; list;
    public void addAll(List&amp;lt;? extends Number&amp;gt; cols){
        this.list.addAll(cols);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드에서 addAll 메소드는 상위 경계 타입 파라미터를 적용하였습니다.&lt;/p&gt;
&lt;p&gt;이로써 이전에 발생했던 List&amp;lt;Integer&amp;gt;, List&amp;lt;Double&amp;gt; 변수를 addAll 메소드의 cols 변수에 할당할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 List&amp;lt;? extends Number&amp;gt;는 무엇을 의미할까요?&lt;/p&gt;
&lt;p&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;전달되는 인자가 어떤 타입인지는 모르겠다. 이를 X 라고 하자&lt;/li&gt;
&lt;li&gt;&lt;u&gt;X &amp;lt;: Number 라면 List&amp;lt;X&amp;gt; &amp;lt;:&amp;nbsp; List&amp;lt;? extends Number&amp;gt;이다.&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같은 의미로 인하여 List&amp;lt;Integer&amp;gt; &amp;lt;:&amp;nbsp; List&amp;lt;? extends Number&amp;gt; 됩니다. 따라서 변수 할당이 가능합니다.&lt;/p&gt;
&lt;p&gt;마찬가지로 List&amp;lt;Double&amp;gt; &amp;lt;:&amp;nbsp; List&amp;lt;? extends Number&amp;gt;이므로 변수 할당 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이로써 일반 클래스 처럼 타입 파라미터 또한 Subtitution Principle 적용 가능해졌습니다. 위와 같은 특성을&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; Covariant&lt;/b&gt;&lt;/span&gt; 하다고 합니다. Covariant는 자기 자신 뿐만 아니라 자기 자신의 Subtype으로 타입 변환이 가능함을 말합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;
&lt;p&gt;Integer &amp;lt;: Number 일때, List&amp;lt;Integer&amp;gt;는 List&amp;lt;Number&amp;gt;의 Subtype이 아니다.&lt;/p&gt;
&lt;p&gt;Integer &amp;lt;: Number 일때, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;List&amp;lt;? extends Integer&amp;gt; &lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;lt;:&lt;/span&gt;&lt;b&gt; List&amp;lt;? extends Number&amp;gt; &lt;/b&gt;&lt;/span&gt;이다!&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583498241979&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args)  {
        List&amp;lt;? extends Integer&amp;gt; ints = Arrays.asList(1, 2);
        List&amp;lt;? extends Number&amp;gt; nums = ints;
        nums.forEach(System.out::println);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 Corvariant 특성을 이용하면 위와 같은 사용 또한 가능합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Heap Polution 이슈 방지&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583491513155&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main{
   public void static main(int argc, String[] argv){
       List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();
       ints.add(1);
       ints.add(2);
       List&amp;lt;Number&amp;gt; nums = ints; //Compile 에러 발생
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1583491513155&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main{
   public void static main(int argc, String[] argv){
       List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();
       ints.add(1);
       ints.add(2);
       List&amp;lt;? extends Number&amp;gt; nums = ints; //할당 허용
       nums.add(3.14); //Compile 에러 발생
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;640&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvNZf0/btqCy3y2dX5/jVxlkdOlYaXe7C16sOCLKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvNZf0/btqCy3y2dX5/jVxlkdOlYaXe7C16sOCLKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvNZf0/btqCy3y2dX5/jVxlkdOlYaXe7C16sOCLKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvNZf0%2FbtqCy3y2dX5%2FjVxlkdOlYaXe7C16sOCLKk%2Fimg.png&quot; width=&quot;640&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;기존에 문제되었던 코드에 상위 경계 타입 파라미터를 사용하면 변수 할당이 가능함을 확인했습니다. 하지만 이때 상위 경계 타입 적용 변수에 Number의 Subtype 값을 할당하려고 하면 이전에 발생하였던 문제(List&amp;lt;Integer&amp;gt; 타입이 Integer가 아닌 Double 등의 다른 값 참조 현상)가 재발하게됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;u&gt;따라서 상위 경계를 사용한 객체에 대해서는 Subtype 값을 할당하려고 시도하면 컴파일 단계에서 에러&lt;/u&gt;를 발생시킵니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 어떠한 과정으로 컴파일 단계에서 Subtype 값 할당이 실패하게 되는 것일까요?&lt;/p&gt;
&lt;p&gt;Capture 변환 과정을 통해 이를 알아보도록 하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3&gt;Capture 변환&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;List&amp;lt;Integer&amp;gt;와 같이 명시적으로 타입을 정의하면 List에서 제공되는 메소드가 호출되었을 때는 다음과 같이 논리적으로 생각해볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583496195456&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface List&amp;lt;Integer&amp;gt; extends Collection&amp;lt;Ineteger&amp;gt;{
    ...
    Integer get(int index);
    void add(Integer element);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 경우 add에 들어오는 값은 반드시 Integer 타입으로 지정되어있기 때문에 값 할당을 허용합니다.&lt;/p&gt;
&lt;p&gt;그렇다면 List&amp;lt;? extends Number&amp;gt;로 타입이 지정되면 컴파일러는 어떤 타입으로 이를 추론할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583496195456&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface List&amp;lt;?&amp;gt; extends Collection&amp;lt;?&amp;gt;{
    ...
    ? get(int index);
    void add(? element);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;u&gt;전달받는 파라미터 정보를 명시하지 않았기 때문에 타입을 추론할 수 없습니다&lt;/u&gt;. 따라서 컴파일시 내부적으로 임의의 타입 변수(capture of ?)으로 지정합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583496195457&quot; class=&quot;java&quot; style=&quot;display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; margin: 20px auto 0px; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface List&amp;lt;X&amp;gt; extends Collection&amp;lt;X&amp;gt;{
    ...
    X get(int index);
    void add(X element);
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;만약 컴파일러가 지정한 임의의 타입을 X라고 간단하게 정의하면 List 인터페이스는 다음과 같이 추론될 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;그리고 List&amp;lt;? extend Number&amp;gt; 구문을 통해서 컴파일러는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;적어도&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;해당 타입이 Number의 Subtype임을 알 수는 있습니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;결론적으로 위와 과정을 거쳐 컴파일러가 알 수 있는 사실은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;u&gt;Number의 Subtype이지만 무슨 타입인지는 모르겠다.&lt;/u&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이러한 상황에서 만약 add 메소드를 호출하면 어떻게 될까요?&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;예시 코드를 통해 알아보도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583496645632&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args)  {
        List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();
        ints.add(1);
        List&amp;lt;? extends  Number&amp;gt; nums = ints;
        Number number = nums.get(0);
        nums.add(number);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;X &amp;lt;: Number 이지만 무엇인지는 알 수 없습니다.&lt;/p&gt;
&lt;p&gt;여기에 Number 값을 넣기 위해서는 Number &amp;lt;: X 이어야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 이는 성립되지 않습니다. 만약 X의 실제 타입이 Integer 였다면, Number &amp;lt;: Integer 라는 말인데 이는 성립되지 않기 때문입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서, 아무리 입력하고자 하는 값이 Number의 Subtype일지라도 X 타입 변수내에서는 타입 정보를 정확히 알지 못하기 때문에 null 이외에 어떠한 값 입력을 허용하지 않습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583497360460&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main{
   public void static main(int argc, String[] argv){
       List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();
       ints.add(1);
       ints.add(2);
       List&amp;lt;? extends Number&amp;gt; nums = ints;
       nums.add(null);
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;null이 허용 가능한 이유는 &lt;span style=&quot;color: #333333;&quot;&gt;null은 모든 타입과 호환되는 값이기 때문입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 해당 문법은 언제 사용해야 할까요?&lt;/p&gt;
&lt;p&gt;일반적으로 상위 경계 타입 파라미터 타입은 &lt;span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;구&lt;/b&gt;&lt;b&gt;조 바깥으로 T나 T의 Supertype으로 값을 읽어야 하는 경우 &lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;사용&lt;/b&gt;&lt;/span&gt;하며, 추론되는 E의 Subtype 값을 집어 넣을 수 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;List&amp;lt;? extends Number&amp;gt;를 예로 내용 정리하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot;&gt;
&lt;li&gt;관심대상은 List 클래스에서 제공되는 기능이다.(size 메소드, isEmpty 메소드 등)&lt;/li&gt;
&lt;li&gt;관심대상은 Number 클래스에서 제공되는 기능이다.(실제 들어있는 값, equals 메소드 등)&lt;/li&gt;
&lt;li&gt;Number 타입의 Subtype들은 관심대상이 아니다.&lt;/li&gt;
&lt;li&gt;&lt;u&gt;해당 변수에서는 구조체 내부 정보를 가져오기 위한 용도로 주로 사용된다.&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;개인적으로는 마지막 부분이 시사하는 바가 크다고 생각합니다.&lt;/p&gt;
&lt;p&gt;이를 통해 앞으로 라이브러리 메소드를 사용할때 상위 한정자가 쓰여있다면, '&lt;i&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;메소드내에서 내가 전달한 구조에서 값을 꺼내는 용도로 사용하겠구나&lt;/span&gt;'&lt;/i&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 의미를 파악하면 좋을 것 같습니다.&lt;/p&gt;</description>
      <category>JAVA/Generic</category>
      <category>bounded type</category>
      <category>Capture</category>
      <category>generic</category>
      <category>upper bound</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/46</guid>
      <comments>https://cla9.tistory.com/46#entry46comment</comments>
      <pubDate>Fri, 6 Mar 2020 19:50:04 +0900</pubDate>
    </item>
    <item>
      <title>2. Subtyping, Substitution 원리</title>
      <link>https://cla9.tistory.com/45</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Subtyping&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅을 시작하기 앞서 Subtyping개념을 먼저 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzXhLN/btqCsIv7CfB/Z8jfskMmYXvIr2GGZ3u7SK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzXhLN/btqCsIv7CfB/Z8jfskMmYXvIr2GGZ3u7SK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzXhLN/btqCsIv7CfB/Z8jfskMmYXvIr2GGZ3u7SK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbzXhLN%2FbtqCsIv7CfB%2FZ8jfskMmYXvIr2GGZ3u7SK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같이 A 클래스가 가진 Spec이 요구되는 상황에서 B의 클래스가 A 역할을 대체하여 사용할 수 있을 때 B를 A의 Subtype, A는 B의 Supertype이라고 불립니다. 또한 모든 참조타입은 자기자신에 대해서 Subtype입니다.&lt;/p&gt;
&lt;p&gt;&lt;b&gt;&lt;u&gt;(※ Subtype을 기호로 나타내면 &amp;lt;: 로 표현되며 A &amp;lt;: B 일 경우 A는 B의 Subtype 입니다.)&lt;/u&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfpJec/btqCxPt4vQi/YfgZQZHiGJepFBbDdRCCZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfpJec/btqCxPt4vQi/YfgZQZHiGJepFBbDdRCCZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfpJec/btqCxPt4vQi/YfgZQZHiGJepFBbDdRCCZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfpJec%2FbtqCxPt4vQi%2FYfgZQZHiGJepFBbDdRCCZK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;광의적인 의미에서 A 클래스의 Spec을 B 클래스에도 코드상에서 준수하고 있으면 이를 Subtype 관계로 보지만, Java에서는 상속 관계에 놓여있을 때만 교체가 가능하기에 이에 집중해서 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 통해서 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nNHJg/btqCxPOmEmo/66DC9qRakJWjURrX7X5ixk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nNHJg/btqCxPOmEmo/66DC9qRakJWjURrX7X5ixk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nNHJg/btqCxPOmEmo/66DC9qRakJWjURrX7X5ixk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnNHJg%2FbtqCxPOmEmo%2F66DC9qRakJWjURrX7X5ixk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ArrayList는 List의 인터페이스를 구현하였습니다. 따라서 List가 사용될 수 있는 영역에는 ArrayList로 대체가 가능합니다.&lt;/p&gt;
&lt;p&gt;따라서, 이때 ArrayList는 List의 Subtype, List는 Supertype입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Java에서 기본적으로 제공되는 라이브러리 예를 통해 해당 개념을 적용해보겠습니다.&lt;/p&gt;
&lt;p&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;Integer &amp;lt;: Number&lt;/li&gt;
&lt;li&gt;Double &amp;lt;: Number&lt;/li&gt;
&lt;li&gt;ArrayList&amp;lt;E&amp;gt; &amp;lt;:&amp;nbsp; List&amp;lt;E&amp;gt;&lt;/li&gt;
&lt;li&gt;Collection&amp;lt;E&amp;gt; &amp;lt;:&amp;nbsp; Iterable&amp;lt;E&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czVNN6/btqCrPbfGvT/8ivtSgeCH9TNmZaZxWcii1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czVNN6/btqCrPbfGvT/8ivtSgeCH9TNmZaZxWcii1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czVNN6/btqCrPbfGvT/8ivtSgeCH9TNmZaZxWcii1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczVNN6%2FbtqCrPbfGvT%2F8ivtSgeCH9TNmZaZxWcii1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Subtyping은 이행적인 특징을 지니고 있습니다.&lt;/p&gt;
&lt;p&gt;즉 A &amp;lt;: B이고, B &amp;lt;: C 이면 A &amp;lt;: C입니다.&lt;/p&gt;
&lt;p&gt;따라서, 위 예에서 List&amp;lt;E&amp;gt; &amp;lt;:&amp;nbsp; Iterable&amp;lt;E&amp;gt; 라고 볼 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Substitution Principle(치환 원리)&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Wiki에는 해당 원리에 대하여 다음과 같이 정의하고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1583401195641&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if S is a subtype of T, then objects of type T may be replcaed with objects of type S&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;요약하자면, 주어진 타입 변수는 해당 타입 어떠한 Subtype으로도 대체가 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Collection&amp;lt;E&amp;gt; 인터페이스에 정의된 add 메소드 사용예를 통해서 해당 원리을 적용해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1583401412149&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;interface Collection&amp;lt;E&amp;gt;{
  ...
  public boolean add(E elt);
  ...
}

&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1583401490876&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main{
   public void static main(int argc, String[] argv){
       List&amp;lt;Number&amp;gt; nums = new ArrayList&amp;lt;&amp;gt;();
       nums.add(2);
       nums.add(3.14);
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;nums 객체의 Type Argument는 &lt;b&gt;Number&lt;/b&gt; 입니다.&lt;/p&gt;
&lt;p&gt;따라서 add 메소드 파라미터 Type은 Number로 추론되므로 메소드 호출시, &lt;b&gt;Integer&lt;/b&gt;나 &lt;b&gt;Double&lt;/b&gt; 값을 할당할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이것이 가능한 이유는 Integer &amp;lt;: Number, Double &amp;lt;: Number 이기 때문입니다.&lt;/p&gt;
&lt;p&gt;그렇기에 위 코드를 컴파일하면 아무런 문제 없이 완료됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 해당 개념을 확장하여 다음과 같은 생각 또한 해볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;i&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Integer &lt;/b&gt;&lt;/span&gt;&amp;lt;: &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Number&lt;/span&gt;&lt;/b&gt;이니까 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;List&amp;lt;Integer&amp;gt;&lt;/span&gt;&lt;/b&gt; &amp;lt;:&amp;nbsp; &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;List&amp;lt;Number&amp;gt; &lt;/b&gt;&lt;/span&gt;라고 볼 수 있지 않을까?&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 문장만을 봐서는 가능할 것 같습니다.&lt;/p&gt;
&lt;p&gt;예시 코드를 보면서 가능 여부를 확인해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1583402299137&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main{
   public void static main(int argc, String[] argv){
       List&amp;lt;Integer&amp;gt; ints = Arrays.asList(1,2);
       List&amp;lt;Number&amp;gt; nums = ints; //Compile 에러 발생
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드를 실행하면 컴파일 에러가 발생합니다. 과연 무엇이 문제였을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sZyUN/btqCwWUwqdz/MWk2NAX7IOZ3qdfd4yHSEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sZyUN/btqCwWUwqdz/MWk2NAX7IOZ3qdfd4yHSEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sZyUN/btqCwWUwqdz/MWk2NAX7IOZ3qdfd4yHSEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsZyUN%2FbtqCwWUwqdz%2FMWk2NAX7IOZ3qdfd4yHSEK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드가 정상적으로 컴파일된다면, 두 객체는 Heap 같은 메모리 영역을 바라보게 될 것입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금까지는 아무런 이상이 없는 것 처럼 보입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583409344078&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public boolean add(Number elt){}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 List&amp;lt;Number&amp;gt; 타입이 해당 메모리 영역을 가르킬 수 있으면 add 함수의 타입 파라미터는 Number로 추론됩니다. &lt;span style=&quot;color: #333333;&quot;&gt;따라서&lt;span&gt; &lt;/span&gt;&lt;/span&gt;Substitution Principle을 적용한다면 Number의 Subtype인 Double 값을 논리적으로 할당할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583409492832&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main{
   public void static main(int argc, String[] argv){
       List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();
       ints.add(1);
       ints.add(2);
       List&amp;lt;Number&amp;gt; nums = ints //Compile 에러 발생
       nums.add(3.14);
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;실제로 Intellij 에서 위 코드를 입력하면 Compile 에러 발생라인은 컴파일전에 에러가 발생하고 있다고 highlight 표시해주지만 nums 객체의 add 함수호출부는 아무런 표시를 하지 않는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKZoTW/btqCqMzgNX0/DKKhcZKmBWkBaflAtV0QB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKZoTW/btqCqMzgNX0/DKKhcZKmBWkBaflAtV0QB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKZoTW/btqCqMzgNX0/DKKhcZKmBWkBaflAtV0QB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKZoTW%2FbtqCqMzgNX0%2FDKKhcZKmBWkBaflAtV0QB1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 구조에서의 문제는 해당 객체가 저장된 공간은 &lt;span style=&quot;color: #333333;&quot;&gt;Parameterized Type이 List&amp;lt;Integer&amp;gt;인 &lt;/span&gt;ints 객체 또한 가르키는 영역이라는 점입니다.&lt;/p&gt;
&lt;p&gt;만약 nums 객체에 ints를 할당할 수 있다면 자신이 가르키고 있는 영역에 int가 아닌 다른 타입이 존재할 수 있기 때문에 타입 안정성이 깨지게됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이러한 문제를 해결하기 위해서 Java에서는 해당 경우에 Substitution Principle을 적용하지 않습니다.&amp;nbsp;즉 List&amp;lt;Integer&amp;gt;는 List&amp;lt;Number&amp;gt;의 Subtype이 아닙니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 반대로 한번 생각해보겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;List&amp;lt;Number&amp;gt; &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;lt;:&lt;/span&gt;&lt;/span&gt;&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;List&amp;lt;Integer&amp;gt;&lt;/span&gt;&lt;/b&gt;일까?&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583410192616&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main{
   public void static main(int argc, String[] argv){
       List&amp;lt;Number&amp;gt; nums = new ArrayList&amp;lt;&amp;gt;();
       nums.add(2.78);
       nums.add(3.14);
       List&amp;lt;Integer&amp;gt; ints = nums; //Compile 에러 발생
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwzcbE/btqCrPoM5CI/mBVYC4HqkLONqdfU3kegNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwzcbE/btqCrPoM5CI/mBVYC4HqkLONqdfU3kegNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwzcbE/btqCrPoM5CI/mBVYC4HqkLONqdfU3kegNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwzcbE%2FbtqCrPoM5CI%2FmBVYC4HqkLONqdfU3kegNk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;Number 타입은 Integer의 Supertype이지 Subtype이 아닙니다. 따라서 위 경우에 nums 객체 값을 ints에 할당 가능하다면 List&amp;lt;Integer&amp;gt; 타입인 ints 입장에서는 Number 타입의 다른 Subtype이 존재하는 영역을 가르키게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;문제는 ints 객체가 가르키는 메모리 영역으로부터 데이터를 읽고자 한다면 Integer 타입이 아닌 Double 타입을 참조할 수 있습니다. 따라서 이러한 경우에는 타입 안정성이 깨지게 됩니다. 그러한 이유로 이 또한 허용되지 않습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금까지 살펴본 내용을 정리해보겠습니다.&lt;/p&gt;
&lt;p&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;List&amp;lt;Integer&amp;gt;는 List&amp;lt;Number&amp;gt;의 Subtype이 아니다.&lt;/li&gt;
&lt;li&gt;List&amp;lt;Number&amp;gt;는 List&amp;lt;Integer&amp;gt;의 Subtype이 아니다.&lt;/li&gt;
&lt;li&gt;List&amp;lt;Integer&amp;gt;는 자기 자신에 대하여 Subtype이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;u&gt;List&amp;lt;E&amp;gt;는 Collection&amp;lt;E&amp;gt;를 상속받으므로 List&amp;lt;Integer&amp;gt;는 Collection&amp;lt;Integer&amp;gt;의 Subtype이다.&lt;/u&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 정리를 살펴보면, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;주어진 타입과 동일한 타입을 지닌 객체에 대해서만 변수 할당이 가능&lt;/b&gt;&lt;/span&gt;한 것을 확인할 수 있습니다. 일반화를 하면 Generic을 사용하면 타입의 상속 관계와 상관 없이 동일 타입만 가능한 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Invariant&lt;/b&gt;&lt;/span&gt; 특성을 지니고 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 이렇게만 사용한다면, Generic을 사용함에 있어 많은 제약이 따를 수 밖에 없습니다.&lt;/p&gt;
&lt;p&gt;이전에 살펴본 코드를 잠시 다시 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583410591533&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main{
   public void static main(int argc, String[] argv){
       List&amp;lt;Integer&amp;gt; ints = new ArrayList&amp;lt;&amp;gt;();
       ints.add(1);
       ints.add(2);
       List&amp;lt;Number&amp;gt; nums = ints; //Compile 에러 발생
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드는&amp;nbsp; nums 객체 할당이후 Number 타입의 다른 Subtype 할당이 가능했기 때문에 문제가 발생했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VioIr/btqCvy0GqbV/J4aYkDO9AkBdiEecAynNoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VioIr/btqCvy0GqbV/J4aYkDO9AkBdiEecAynNoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VioIr/btqCvy0GqbV/J4aYkDO9AkBdiEecAynNoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVioIr%2FbtqCvy0GqbV%2FJ4aYkDO9AkBdiEecAynNoK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;하지만 해당 nums 객체가 지정하는 타입에 대하여 Subtype 값 할당은 하지 않고 단순히 참조의 목적이라면, List&amp;lt;Number&amp;gt;에 List&amp;lt;Integer&amp;gt; 객체를 할당하는 것은 논리적으로 문제 없어 보입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같은 상황에서 사용할 수 있는 방법이 없을까요?&lt;/p&gt;
&lt;p&gt;다음 포스팅에서 다룰 Wildcard를 통해서 해당 이슈를 다루어보도록 하겠습니다.&lt;/p&gt;</description>
      <category>JAVA/Generic</category>
      <category>generic</category>
      <category>subtype</category>
      <category>subtyping</category>
      <category>supertype</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/45</guid>
      <comments>https://cla9.tistory.com/45#entry45comment</comments>
      <pubDate>Thu, 5 Mar 2020 18:24:43 +0900</pubDate>
    </item>
    <item>
      <title>1. Generic 기초</title>
      <link>https://cla9.tistory.com/44</link>
      <description>&lt;p&gt;Java는 대표적인 정적 타입 언어입니다. 즉 컴파일 시점에 사용하는 변수의 타입이 결정되며, 따라서 명시적으로 변수의 타입을 지정하거나 추론이 가능해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 클래스내 일부 변수에 대하여 int형 또는 String형으로 상황에 따라 각기 다른 데이터를 담고 싶다면 어떻게 해야할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583151158350&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class IntegerData{
    int data;
    double var2;
    public IntegerData(int data) {this.data = data;}
    public int getData() {return data;}
    public void method1(){}
    public void method2(){}
}

class StringData {
    String data;
    double var2;
    public StringData(String data) {this.data = data;}
    public String getData() {return data;}
    public void method1(){}
    public void method2(){}
}

public class Main {
    public static void main(String[] args) {
        IntegerData a = new IntegerData(1);
        StringData b = new StringData(&quot;1&quot;);
        
        int c = a.getData();
        String d = b.getData();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;먼저 타입별 클래스를 만드는 방법을 먼저 떠올릴 수 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 일부 데이터 타입만 다름에도 불구하고 매번 클래스를 만드는 것은 매우 비효율적입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 어떻게 문제를 해결할 수 있을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Idea) 모든 클래스는 Object를 상속받으니, Object 타입으로 변수를 지정하자&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583151754747&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Data{
    Object data;
    public Data(Object data) {this.data = data;}
    public Object getData() {return data;}
}

public class Main {
    public static void main(String[] args) {
        Data a = new Data(1);
        Data b = new Data(&quot;1&quot;);
        
        int c = (int) a.getData();
        String d = (String) b.getData();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Object 타입으로 변수를 생성하면, Object 객체에 모든 변수 값을 저장할 수 있습니다. 따라서, 타입별로 클래스를 만들지 않아도되니 효율적입니다. 하지만 데이터를 가져올 때에는 원래 타입으로 캐스팅이 필요합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;음.. 캐스팅을 개발자가 명시적으로 지정해야한다는 약간의 불편함은 있지만 그런대로 괜찮아 보입니다.&lt;/p&gt;
&lt;p&gt;하지만 정말 이대로 괜찮다고 말할 수 있을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;문제점&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583152294008&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Data a = new Data(1);
        String b = (String) a.getData();
        System.out.println(b);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;개발자의 실수로 Data 객체에 들어있는 값의 타입이 String으로 착각하여 String 변수에 이를 담았다고 가정해봅시다.&lt;/p&gt;
&lt;p&gt;실제로는 담겨져있는 int 타입의 변수에 대해서 String 타입으로 캐스팅을 수행하니 오류가 나야 정상입니다.&lt;/p&gt;
&lt;p&gt;하지만 위 소스를 컴파일을 해보면 정상적으로 컴파일됩니다. 그 이유는 지정된 타입은 Object 이고, Object 타입은 String 타입의 부모 타입이기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;실행 결과 : Exception in thread &quot;main&quot; &lt;a href=&quot;java.lang.ClassCastException:&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java.lang.ClassCastException:&lt;/a&gt;&amp;nbsp;class&amp;nbsp;&lt;a href=&quot;java.lang.Integer&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java.lang.Integer&lt;/a&gt;&amp;nbsp;cannot&amp;nbsp;be&amp;nbsp;cast&amp;nbsp;to&amp;nbsp;class&amp;nbsp;&lt;a href=&quot;java.lang.String&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java.lang.String&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서, 에러가 발생하는 시점은 Application을 구동해서 데이터를 가져오는 코드가 실행되는 시점입니다. 즉 해당 방법은 TypeSafety 하지 않은 방법으로 &lt;b&gt;Runtime 시점에 오류&lt;/b&gt;를 알 수 있는점은 잠재적인 오류를 내포하고 있으므로 좋지 못합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583153679845&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(1);
        String a = (String) list.get(0);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAhHfc/btqCrO27ZIq/PgUUSmO59w0Hr92S0ALfMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAhHfc/btqCrO27ZIq/PgUUSmO59w0Hr92S0ALfMk/img.png&quot; data-alt=&quot;java 1.4 doc ArrayList get 메소드&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAhHfc/btqCrO27ZIq/PgUUSmO59w0Hr92S0ALfMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAhHfc%2FbtqCrO27ZIq%2FPgUUSmO59w0Hr92S0ALfMk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;java 1.4 doc ArrayList get 메소드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Generic이 지원되지 않는 Java 1.4 버전 이하에서는 우리가 흔히 사용하는 ArrayList 에서 여러 데이터 형을 담기 위하여 Object 타입으로 저장하였습니다. 또한 반환시에도 Object 타입으로 반환하였습니다. 따라서 Runtime 오류를 피하기 위해서 개발자가 조심해서 캐스팅을 수행해야했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Generic 등장&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583154680190&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Data&amp;lt;T&amp;gt;{
    T data;
    public Data(T data) { this.data = data; }
    public T getData() {return data; }
}

public class Main {
    public static void main(String[] args) {
        Data&amp;lt;Integer&amp;gt; a = new Data&amp;lt;&amp;gt;(1);
        Data&amp;lt;String&amp;gt; b = new Data&amp;lt;&amp;gt;(&quot;1&quot;);

        int c = a.getData();
        String d = b.getData();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전에 살펴보았듯이 Object 타입으로 저장하는 것은 개발자가 명시적으로 타입을 예측하여 캐스팅을 수행해야했기에 잠재적 코드 결함율이 발생할 수 있었습니다. 따라서 이를 해결하고자 Java 진영에서는 1.5 버전부터 Generic을 지원하였습니다. Generic은 특정 변수에 대하여 객체 생성시 원하는 타입으로 지정할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드와 같이 Data 클래스에 대하여 객체 생성시 &amp;lt;&amp;gt; 안에 원하는 Reference 타입을 명시해주면, 컴파일러가 해당 객체의 data 타입은 명시적으로 지정된 타입이라는 것을 보장해줍니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;기존에는 반환 값이 Object였기 때문에 명시적으로 캐스팅을 수행해야했지만, Generic을 사용하면 컴파일러가 타입을 보장해주기 때문에 명시적 캐스팅이 불필요합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oJch8/btqCuZcJbzl/XIAFoCO2kTYJncSEikEUS0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oJch8/btqCuZcJbzl/XIAFoCO2kTYJncSEikEUS0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oJch8/btqCuZcJbzl/XIAFoCO2kTYJncSEikEUS0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoJch8%2FbtqCuZcJbzl%2FXIAFoCO2kTYJncSEikEUS0%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마치 위 그림과 같이 타입을 지정하게되면 각각의 클래스가 별도로 존재하는 것처럼 논리적으로 생각할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583155236556&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) {
        Data&amp;lt;Integer&amp;gt; a = new Data&amp;lt;&amp;gt;(1);
        String d = (String)a.getData();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 위와 같이 개발자의 실수로 Integer 데이터를 String 변수에 담으려고 해도 컴파일 단계에서 잘못된 타입임을 알고 에러를 표시하기 때문에 TypeSafety합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;도입부에 이해를 돕기 위해 논리적으로 Class 여러개가 생성된 그림을 보여드렸습니다. 그렇다면 실제 내부 구현도 위와 같이 되어있을까요? 지금부터 Generic 내부에 대해서 살펴보면서 원리를 알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;용어&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;학습하기 전 관련 용어 일부를 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/spFKP/btqCtYdaKaz/dRw7YhA1CjOzRP9x9wUWbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/spFKP/btqCtYdaKaz/dRw7YhA1CjOzRP9x9wUWbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/spFKP/btqCtYdaKaz/dRw7YhA1CjOzRP9x9wUWbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FspFKP%2FbtqCtYdaKaz%2FdRw7YhA1CjOzRP9x9wUWbk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Type Parameter&lt;/b&gt; : Generic 타입을 명시하기 위한 Placeholder 입니다. 즉 위 코드에서 Data 클래스의 T가 이에 해당됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;Type Argument&lt;/b&gt; : 실제 Generic 타입에 명시된 타입을 의미합니다. 위 코드에서는 Integer가 해당됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Parameterized Type&lt;/b&gt; : Type argument에 의하여 Type Parameter가 치환된 전체 데이터 타입을 의미합니다. 위 코드에서는 Data&amp;lt;Integer&amp;gt;가 이에 해당됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 세가지 용어가 혼동되지 않도록 주의하며, 본격적으로 Generic에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Type Erasure&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583157483138&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Data&amp;lt;T&amp;gt;{
    T data;
    public Data(T data) { this.data = data; }
    public T getData() {return data; }
}

public class Main {
    public static void main(String[] args) {
        Data&amp;lt;Integer&amp;gt; a = new Data&amp;lt;&amp;gt;(1);
        int c = a.getData();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;동작 원리를 이해하기 위해 컴파일된 바이트 코드를 보는 것이 가장 좋지만, 이해를 돕기위해 위와 같이 Generic 문법을 사용한 자바 코드를 컴파일 한다음 해당 class 파일을 다시 디컴파일 결과를 보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXyxdn/btqCwWOJSL4/Rbs0gUgYSiHVNrkVOrvvh1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXyxdn/btqCwWOJSL4/Rbs0gUgYSiHVNrkVOrvvh1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXyxdn/btqCwWOJSL4/Rbs0gUgYSiHVNrkVOrvvh1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXyxdn%2FbtqCwWOJSL4%2FRbs0gUgYSiHVNrkVOrvvh1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;먼저 Data 클래스를 보겠습니다. 기존에 Type Parameter T로 선언되었던 부분이 전부 &lt;b&gt;Object&lt;/b&gt; 클래스로 타입이 변경되었습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/znvb8/btqCz5ReRMb/1OgkDtUr4nLNhnsvWC3nWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/znvb8/btqCz5ReRMb/1OgkDtUr4nLNhnsvWC3nWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/znvb8/btqCz5ReRMb/1OgkDtUr4nLNhnsvWC3nWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fznvb8%2FbtqCz5ReRMb%2F1OgkDtUr4nLNhnsvWC3nWK%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;main 호출부에서도 Parameterized Type이 Data&amp;lt;T&amp;gt;였던 클래스 정보가 Data와 같이 변경되었고, 값을 가져오는 부분에서 &lt;b&gt;캐스팅&lt;/b&gt;이 발생되었음을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;자세히 보면 이는 Generic이 사용되기 이전에 작성하던 방식과 동일한 결과입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 통해 알 수 있는 사실은 Generic을 사용하더라도 &lt;b&gt;사용자가 지정한 Type은 사라지고&lt;/b&gt; 기존과 같이 Object 타입으로 지정되며, 대신 명시적으로 값을 가져오는 코드에&amp;nbsp;&lt;b&gt;컴파일러가 캐스팅하는 코드를 삽입&lt;/b&gt;하여 Type 안정성을 보장해준다는 점입니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjLUK1/btqCu0bzDiS/thx4dhsdQkOw0fx8KI9RVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjLUK1/btqCu0bzDiS/thx4dhsdQkOw0fx8KI9RVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjLUK1/btqCu0bzDiS/thx4dhsdQkOw0fx8KI9RVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjLUK1%2FbtqCu0bzDiS%2Fthx4dhsdQkOw0fx8KI9RVk%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또한, 컴파일러가 개발자가 지정한 Type을 알고 있기 때문에, 중간에 개발자가 실수로 잘못된 캐스팅을 시도하는 코드가 있다면 잘못 캐스팅 되었음을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583160388959&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args) throws NoSuchFieldException {
        Data&amp;lt;Integer&amp;gt; a = new Data&amp;lt;&amp;gt;(1);
        System.out.println(a.getClass().getDeclaredField(&quot;data&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;&lt;a href=&quot;java.lang.Object&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;실행결과 : java.lang.Object&lt;/a&gt;&amp;nbsp;&lt;a href=&quot;Data.data&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Data.data&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;한 단계 더 나아가자면, 컴파일 시점에는 내가 지정한 Type 정보를 알 수 있지만 컴파일 이후에 생성된 바이트코드에서는 Object로 Type이 지정되므로 &lt;b&gt;런타임 시점에는 Type 확인이 불가&lt;/b&gt;하다는 점입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;실제 위 코드와 같이 Reflection을 활용하여 런타임시 Data 인스턴스의 data 타입을 확인해보면 Object 타입임을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;런타임 시점에 Type 정보를 알 수 없는 것은 꽤나 불편한 제약으로 느껴집니다. 그렇다면 Java는 왜 이러한 제약사항이 존재함에도 불구하고 Type Erasure를 통해서 명시된 Type을 삭제했을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583160919680&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Stub{
    List list;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를들어 기존에 jdk 1.5 미만을 사용하던 라이브러리에서 Java Collection을 사용하려면 위와 같이 사용되었을 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 Generic을 사용한 Collection 클래스에 대해 컴파일 하는 시점에 Type이 사용자가 지정한 Type으로 변경된다면, 그에 따른 바이트 코드 또한 변경될 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 낮은 버전(1.4 이하) Collection을 사용하는 라이브러리에서 상위 버전으로 업그레이드를 위해서는 변경에 따른 코드 수정이 필요합니다. 즉 New Feature 도입으로 인하여 하위 호환성 문제가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Java는 아주 대표적으로 하위 호환성을 중요시 여기는 언어입니다. 따라서 하위 버전의 호환성 유지를 위하여 Type Erasure를 통해 기존 코드와&amp;nbsp;상위 버전 코드간의 호환성이 잘 유지되도록 지원하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 Generic 프로그래밍을 지원하는 모든 언어가 Type erasure를 사용할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정답은 &lt;b&gt;그렇지 않습니다&lt;/b&gt;. 대표적으로 C++가 이에 해당합니다.&lt;/p&gt;
&lt;p&gt;C++은 컴파일시에 실제 지정한 데이터타입에 대하여 코드를 생성합니다. 따라서 때로는 머신에 최적화된 코드를 생성하기 용이하기도 합니다. 하지만 타입만큼 코드가 생성되므로 Java에 비해 상대적으로 코드 크기가 커질 수 있는 단점 또한 존재합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타입 경계&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583647783978&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Data&amp;lt;T&amp;gt;{
    T data;
    public Data(T data) { this.data = data; }
    public T getData() {return data; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전 내용을 통해 Type erasure에 의하여 T 타입이 Object 타입으로 변경됨을 확인하였습니다.&lt;/p&gt;
&lt;p&gt;그렇다면 Generic으로 정의한 모든 타입이 컴파일시 Object로 타입이 변환될까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 예제를 기반으로 T 타입이 Object로 변경된 이유를 다시 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드에서 T 타입이 의미하는바는 우리가 지정한 어느 타입이여도 상관이 없음을 나타냅니다.&lt;/p&gt;
&lt;p&gt;이는 Java를 통해 구현하는 모든 클래스 타입을 지정할 수 있음을 의미합니다. 즉 &lt;span style=&quot;color: #333333;&quot;&gt;타입의 경계가 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;classes-object.gif&quot; data-origin-width=&quot;553&quot; data-origin-height=&quot;287&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZgip1/btqCy5qFWg7/KcpMhLjEEzKT60hcKKZC60/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZgip1/btqCy5qFWg7/KcpMhLjEEzKT60hcKKZC60/img.gif&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://docs.oracle.com/javase/tutorial/java/IandI/subclasses.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZgip1/btqCy5qFWg7/KcpMhLjEEzKT60hcKKZC60/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/dZgip1/btqCy5qFWg7/KcpMhLjEEzKT60hcKKZC60/img.gif&quot; data-filename=&quot;classes-object.gif&quot; data-origin-width=&quot;553&quot; data-origin-height=&quot;287&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://docs.oracle.com/javase/tutorial/java/IandI/subclasses.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서, 런타임시 내가 지정한 어떠한 타입이라도 허용이 가능한 가장 최상위 클래스인 Object로 타입이 변경되는 것입니다.&lt;/p&gt;
&lt;p&gt;만약 다음과 같은 요구사항이 있다고 가정해봅시다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;i&gt;'Data 클래스에서 data 변수는 &lt;b&gt;Integer&lt;/b&gt;, &lt;b&gt;Double&lt;/b&gt;과 같은 Number 타입만 허용 가능하다.'&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이 경우에는 어떻게 해야할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;
&lt;p&gt;방법1. 개발자끼리 명시적으로 합의하여 Number 타입이하로만 사용하게끔 조심한다.&lt;/p&gt;
&lt;p&gt;방법2. T 타입으로 지정할 수 있는 범위를 설정하여, 잘못 지정시 컴파일 에러를 발생시킨다.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;너무나 당연하게도, 방법 2로하는게 가장 안전할 것입니다.&lt;/p&gt;
&lt;p&gt;Java에서는 이를 위해서 &lt;b&gt;타입 경계 &lt;/b&gt;기능을 제공합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583648854318&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Data&amp;lt;T extends Number&amp;gt;{
    T data;
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CiRkZ/btqCAOV1MdX/MyYOk90m6fAW0mf37WBAB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CiRkZ/btqCAOV1MdX/MyYOk90m6fAW0mf37WBAB1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CiRkZ/btqCAOV1MdX/MyYOk90m6fAW0mf37WBAB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCiRkZ%2FbtqCAOV1MdX%2FMyYOk90m6fAW0mf37WBAB1%2Fimg.png&quot; data-origin-width=&quot;0&quot; data-origin-height=&quot;0&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드와 같이 T에 대한 경계(Number 타입 이하로만 설정)를 지정하면, 이를 사용하는 Data 클래스 사용시 타입을 Number 이하로만 설정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583649629721&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Main {
    public static void main(String[] args)  {
        Data&amp;lt;Object&amp;gt; obj = new Data&amp;lt;&amp;gt;();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;
&lt;p&gt;Error:(14,&amp;nbsp;14)&amp;nbsp;java:&amp;nbsp;type&amp;nbsp;argument&amp;nbsp;&lt;a href=&quot;java.lang.Object&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java.lang.Object&lt;/a&gt;&amp;nbsp;is&amp;nbsp;not&amp;nbsp;within&amp;nbsp;bounds&amp;nbsp;of&amp;nbsp;type-variable&amp;nbsp;T&lt;/p&gt;
&lt;p&gt;Error:(14,&amp;nbsp;36)&amp;nbsp;java:&amp;nbsp;incompatible&amp;nbsp;types:&amp;nbsp;cannot&amp;nbsp;infer&amp;nbsp;type&amp;nbsp;arguments&amp;nbsp;for&amp;nbsp;Data&amp;lt;&amp;gt; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;reason:&amp;nbsp;inference&amp;nbsp;variable&amp;nbsp;T&amp;nbsp;has&amp;nbsp;incompatible&amp;nbsp;bounds &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;equality&amp;nbsp;constraints:&amp;nbsp;&lt;a href=&quot;java.lang.Object&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java.lang.Object&lt;/a&gt; &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;lower&amp;nbsp;bounds:&amp;nbsp;&lt;a href=&quot;java.lang.Number&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;java.lang.Number&lt;/a&gt;&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 main 함수에서 Data 클래스 정의를 Number의 하위 타입이 아닌 다른 클래스로 지정하게되면 위와 같은 에러를 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 위와 같이 경계를 지정한 다음 컴파일하게되면 타입은 무엇으로 지정되어있을까요?&lt;/p&gt;
&lt;p&gt;이전과 마찬가지로 컴파일한 내용을 다시 디컴파일해서 확인해보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583649894949&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Data
{

    Data()
    {
    }

    public Number getData()
    {
        return data;
    }

    public void setData(Number number)
    {
        data = number;
    }

    Number data;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;아마 눈치를 채셨겠지만, 디컴파일을 하게되면 Data 클래스의 T 타입이 Object가 아닌 Number 타입으로 변경된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그 이유는 타입 경계로 인하여 Number 이하의 타입만 허용가능하기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정리하면 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;T 타입에 대한 경계를 지정하지 않으면 가장 최상위인 Object로 컴파일시 변경된다.&lt;/li&gt;
&lt;li&gt;T 타입에 대한 경계를 지정하면, 해당 타입으로 컴파일시 변경된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그렇다면 타입의 경계를 여러개 지정이 필요한 경우에는 어떻게 할까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1583651806722&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Data&amp;lt;T extends Number &amp;amp; Comparable&amp;lt;T&amp;gt;&amp;gt;{
    private T data;

    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위와 같이 Number 타입이면서 Comparable 인터페이스를 구현한 타입만 지정하게끔 설정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그리고 이때 컴파일 시에는 지정된 경계중에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;가장 왼쪽&lt;/b&gt;&lt;/span&gt;에 위치한 Number 클래스로 타입이 지정됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이때 주의할 점은 경계를 구체적인 클래스 타입이 가장 첫 번째로 지정되어야합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마치며&lt;/h3&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Generic 기초와 Type erasure에 대해서 살펴보았습니다. 다음 포스팅에서는 Subtyping에 대해서 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>JAVA/Generic</category>
      <category>generic</category>
      <category>java</category>
      <category>type erasure</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/44</guid>
      <comments>https://cla9.tistory.com/44#entry44comment</comments>
      <pubDate>Wed, 4 Mar 2020 20:04:36 +0900</pubDate>
    </item>
    <item>
      <title>20. Saga 패턴을 활용한 트랜잭션 관리 - 3</title>
      <link>https://cla9.tistory.com/24</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅은 AxonFramework 관련 마지막 포스팅입니다. Saga 패턴 보상 트랜잭션 구현을 다루겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Deadline&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;MSA 환경에서는 App이 여러개로 분산되어있으므로 하나의 App이 느려지거나 장애가 발생하면, 장애가 발생한 App을 호출하는 App에도 장애가 전파됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Axon에서 제공하는 Saga 패턴을 사용하면, 요청마다 Saga 인스턴스가 생성됩니다. 따라서 연관된 App의 장애로 인해 전체 트랜잭션에 Hang이 걸리게되면, 요청한 App 또한 안전하지 못합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 이를 완화하기 위해 Axon에서는 Deadline 기능을 제공합니다. Deadline은 App에서 지정한 시간동안 반응이 없으면, 이를 처리할 메소드를 Callback 형식으로 등록할 수 있는 기능입니다. 자세한 내용은 &lt;a href=&quot;https://docs.axoniq.io/reference-guide/configuring-infrastructure-components/deadlines&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;공식 문서&lt;/u&gt;&lt;/a&gt;를 참고 바라며, 데모 프로젝트에서는 &lt;b&gt;CommandGateway&lt;/b&gt; 클래스에서 기본적으로 제공하는 &lt;b&gt;sendAndWait&lt;/b&gt; 메소드를 통해서 일정 시간동안 응답이 없으면, 보상 트랜잭션을 발동하도록 구현하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Command 모듈 Saga 패키지내 있는 Saga 클래스를 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;saga.png&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wxwWZ/btqBtGNbi4Z/kN8DmVsvblEMJtlpqKFcj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wxwWZ/btqBtGNbi4Z/kN8DmVsvblEMJtlpqKFcj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wxwWZ/btqBtGNbi4Z/kN8DmVsvblEMJtlpqKFcj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwxwWZ%2FbtqBtGNbi4Z%2FkN8DmVsvblEMJtlpqKFcj0%2Fimg.png&quot; data-filename=&quot;saga.png&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;501&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. Saga 클래스 코드를 수정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransferManager.java&lt;/p&gt;
&lt;pre id=&quot;code_1579925373790&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Saga
@Slf4j
public class TransferManager {
    (...중략...)
    
    @StartSaga
    @SagaEventHandler(associationProperty = &quot;transferID&quot;)
    protected void on(MoneyTransferEvent event) {
       (...중략...)
        try {
            log.info(&quot;계좌 이체 시작 : {} &quot;, event);
            commandGateway.sendAndWait(comamndFactory.getTransferCommand(), 10, TimeUnit.SECONDS);
        } catch (CommandExecutionException e) {
            log.error(&quot;Failed transfer process. Start cancel transaction&quot;);
            //보상 트랜잭션 구현 로직
        }
    }
    (...중략...)

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;sendAndWait 두 번째, 세 번째 인자를 통해 TimeOut을 지정하며, 해당 기간동안 응답이 없을 경우 Exception을 통해 보상 트랜잭션을 발동할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 트랜잭션 프로세스 설계&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;일반적인 상황&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1431&quot; data-origin-height=&quot;837&quot; width=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vNp0o/btqBuPbal4m/yk9XUWWep1u2OpCuRiHk0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vNp0o/btqBuPbal4m/yk9XUWWep1u2OpCuRiHk0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vNp0o/btqBuPbal4m/yk9XUWWep1u2OpCuRiHk0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvNp0o%2FbtqBuPbal4m%2Fyk9XUWWep1u2OpCuRiHk0K%2Fimg.png&quot; data-origin-width=&quot;1431&quot; data-origin-height=&quot;837&quot; width=&quot;720&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;일반적으로 계좌 이체 요청을하면, 해당 은행에서 보유한 잔고보다 요청액수가 클 경우에는 이체 거절을하며, 반대의 경우에는 이체 승인을 합니다. 따라서 요청자인 Command 모듈에서는 Jeju 은행의 승인 혹은 거절 이벤트 발생 여부에 따라서 결과를 처리하면 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;보상 트랜잭션 발동 상황&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;720&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ewRQHy/btqBuQt2T9a/CyvaIpcukccHhIuk1GUmN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ewRQHy/btqBuQt2T9a/CyvaIpcukccHhIuk1GUmN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ewRQHy/btqBuQt2T9a/CyvaIpcukccHhIuk1GUmN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FewRQHy%2FbtqBuQt2T9a%2FCyvaIpcukccHhIuk1GUmN1%2Fimg.png&quot; width=&quot;720&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;207&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;보상 트랜잭션은 연결된 App 사이에 트랜잭션 문제가 발생하였을 때, 이미 처리된 데이터를 원상복구를 위하여 추가적인 트랜잭션을 발동하는 것입니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;예제에서는 Timeout이 발생하게 되면, Jeju 은행 App의 응답과 관계없이 트랜잭션 Rollback을 위하여 보상 트랜잭션을 요청하고, Command 모듈에서는 다음 로직을 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;로직2.png&quot; data-origin-width=&quot;1431&quot; data-origin-height=&quot;837&quot; width=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZoji6/btqBxAds1HI/lFKeqwQhcP9kwCTWeQ2oak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZoji6/btqBxAds1HI/lFKeqwQhcP9kwCTWeQ2oak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZoji6/btqBxAds1HI/lFKeqwQhcP9kwCTWeQ2oak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZoji6%2FbtqBxAds1HI%2FlFKeqwQhcP9kwCTWeQ2oak%2Fimg.png&quot; data-filename=&quot;로직2.png&quot; data-origin-width=&quot;1431&quot; data-origin-height=&quot;837&quot; width=&quot;720&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 위 그림과 같이 만약 Jeju 은행에 발생한 장애로 인하여 계좌 요청을 진행하였지만 응답이 오지 않는 상황이라고 가정해봅시다. Command 모듈에서는 장애 방지를 위해 Timeout을 설정했기 때문에 일정 시간이 지나면 보상 트랜잭션을 발동할 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;이상상황.png&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;326&quot; width=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bpSOLM/btqBvtyBMvs/dJgOO0zKcICv4jU6pBw8xk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bpSOLM/btqBvtyBMvs/dJgOO0zKcICv4jU6pBw8xk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bpSOLM/btqBvtyBMvs/dJgOO0zKcICv4jU6pBw8xk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbpSOLM%2FbtqBvtyBMvs%2FdJgOO0zKcICv4jU6pBw8xk%2Fimg.png&quot; data-filename=&quot;이상상황.png&quot; data-origin-width=&quot;1202&quot; data-origin-height=&quot;326&quot; width=&quot;720&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이후 Jeju 은행 App이 정상화된다면 이전 요청을 수행할 것입니다. 그 결과 요청이 적절하지 않으면, 이체 거절 이벤트를 발송합니다. 만약 이체 거절 상황에서 요청받은 보상 트랜잭션을 처리한다면, 잔고는 그대로인 상황에서 보상 트랜잭션에 의해 잔고가 늘어나는 기현상이 발생합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 해결하기 위해 다양한 방법이 있겠지만, 예제 프로젝트에서는 Timeout이 발생된 상황에서 이체 거절 메시지를 받게되면, 보상 트랜잭션 취소 요청하여 정상 처리하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;전체 로직.png&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;842&quot; width=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qlHXl/btqBuvXXebz/0LKYLcmgeTyNG2rnuovAj1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qlHXl/btqBuvXXebz/0LKYLcmgeTyNG2rnuovAj1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qlHXl/btqBuvXXebz/0LKYLcmgeTyNG2rnuovAj1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqlHXl%2FbtqBuvXXebz%2F0LKYLcmgeTyNG2rnuovAj1%2Fimg.png&quot; data-filename=&quot;전체 로직.png&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;842&quot; width=&quot;720&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 Command 모듈에서 트랜잭션 요청 이후 처리해야할 과정을 개략적으로 순서도로 나타냈습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Common 모듈 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Common 모듈 command 패키지내 command 클래스를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;공통 Command.png&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xSg9g/btqBs0LUz3Q/dxOfkgskM6qHzsK8h5kb0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xSg9g/btqBs0LUz3Q/dxOfkgskM6qHzsK8h5kb0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xSg9g/btqBs0LUz3Q/dxOfkgskM6qHzsK8h5kb0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxSg9g%2FbtqBs0LUz3Q%2FdxOfkgskM6qHzsK8h5kb0k%2Fimg.png&quot; data-filename=&quot;공통 Command.png&quot; data-origin-width=&quot;424&quot; data-origin-height=&quot;499&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. command 클래스를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;AbstractCancelTransferCommand&lt;/span&gt;.java&lt;/p&gt;
&lt;pre id=&quot;code_1579927200480&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ToString
@NoArgsConstructor
@AllArgsConstructor
@Getter
public abstract class AbstractCancelTransferCommand {
    @TargetAggregateIdentifier
    protected String srcAccountID;
    protected String dstAccountID;
    protected Long amount;
    protected String transferID;

    public AbortTransferCommand create(String srcAccountID, String dstAccountID, Long amount, String transferID) {
        this.srcAccountID = srcAccountID;
        this.dstAccountID = dstAccountID;
        this.transferID = srcAccountID;
        this.amount = amount;
        return this;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;JejuBankCancelTransferCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579927218718&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JejuBankCancelTransferCommand extends AbstractCancelTransferCommand {
    @Override
    public String toString() {
        return &quot;JejuBankCancelTransferCommand{&quot; +
                &quot;srcAccountID='&quot; + srcAccountID + '\'' +
                &quot;, dstAccountID='&quot; + dstAccountID + '\'' +
                &quot;, amount=&quot; + amount +
                &quot;, transferID='&quot; + transferID + '\'' +
                '}';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;SeoulBankCancelTransferCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579927233690&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SeoulBankCancelTransferCommand extends AbstractCancelTransferCommand {
    @Override
    public String toString() {
        return &quot;SeoulBankCancelTransferCommand{&quot; +
                &quot;srcAccountID='&quot; + srcAccountID + '\'' +
                &quot;, dstAccountID='&quot; + dstAccountID + '\'' +
                &quot;, amount=&quot; + amount +
                &quot;, transferID='&quot; + transferID + '\'' +
                '}';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AbstractCompensationCancelCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579927170181&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ToString
@NoArgsConstructor
@AllArgsConstructor
@Getter
public abstract class AbstractCompensationCancelCommand {
    @TargetAggregateIdentifier
    protected String srcAccountID;
    protected String dstAccountID;
    protected Long amount;
    protected String transferID;
    
    public AbstractCompensationCancelCommand create(String srcAccountID, String dstAccountID, Long amount, String transferID) {
        this.srcAccountID = srcAccountID;
        this.dstAccountID = dstAccountID;
        this.transferID = srcAccountID;
        this.amount = amount;
        return this;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;JejuBankCompensationCancelCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579927268518&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class JejuBankCompensationCancelCommand extends AbstractCompensationCancelCommand {
    @Override
    public String toString() {
        return &quot;JejuBankCompensationCancelCommand{&quot; +
                &quot;srcAccountID='&quot; + srcAccountID + '\'' +
                &quot;, dstAccountID='&quot; + dstAccountID + '\'' +
                &quot;, amount=&quot; + amount +
                &quot;, transferID='&quot; + transferID + '\'' +
                '}';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;SeoulBankCompensationCancelCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579927283299&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class SeoulBankCompensationCancelCommand extends AbstractCompensationCancelCommand {
    @Override
    public String toString() {
        return &quot;SeoulBankCompensationCancelCommand{&quot; +
                &quot;srcAccountID='&quot; + srcAccountID + '\'' +
                &quot;, dstAccountID='&quot; + dstAccountID + '\'' +
                &quot;, amount=&quot; + amount +
                &quot;, transferID='&quot; + transferID + '\'' +
                '}';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransferComamndFactory.java&lt;/p&gt;
&lt;pre id=&quot;code_1579929244109&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class TransferComamndFactory {
    private final AbstractTransferCommand transferCommand;
    private final AbstractCancelTransferCommand abortTransferCommand;
    private final AbstractCompensationCancelCommand compensationAbortCommand;

    public void create(String srcAccountID, String dstAccountID, Long amount, String transferID){
        transferCommand.create(srcAccountID, dstAccountID, amount, transferID);
        abortTransferCommand.create(srcAccountID, dstAccountID, amount, transferID);
        compensationAbortCommand.create(srcAccountID, dstAccountID, amount, transferID);
    }

    public AbstractTransferCommand getTransferCommand(){
        return this.transferCommand;
    }
    public AbstractCancelTransferCommand getAbortTransferCommand(){
        return this.abortTransferCommand;
    }
    public AbstractCompensationCancelCommand getCompensationAbortCommand(){
        return this.compensationAbortCommand;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. Common 모듈 event 패키지내 event 클래스를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;공통 Event.png&quot; data-origin-width=&quot;417&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bm8HUM/btqBuPu9tXc/WkojM8Z7TzmsA8O95W7ql1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bm8HUM/btqBuPu9tXc/WkojM8Z7TzmsA8O95W7ql1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bm8HUM/btqBuPu9tXc/WkojM8Z7TzmsA8O95W7ql1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbm8HUM%2FbtqBuPu9tXc%2FWkojM8Z7TzmsA8O95W7ql1%2Fimg.png&quot; data-filename=&quot;공통 Event.png&quot; data-origin-width=&quot;417&quot; data-origin-height=&quot;476&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;4. event 클래스 내용을 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;CompletedCancelTransferEvent.java&lt;/p&gt;
&lt;pre id=&quot;code_1579927614946&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Builder
@ToString
@Getter
public class CompletedCancelTransferEvent {
    private String srcAccountID;
    private String dstAccountID;
    private Long amount;
    private String transferID;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;CompletedCompensationCancelEvent.java&lt;/p&gt;
&lt;pre id=&quot;code_1579927631986&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Builder
@ToString
@Getter
public class CompletedCompensationCancelEvent {
    private String srcAccountID;
    private String dstAccountID;
    private Long amount;
    private String transferID;
}
&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;h4 data-ke-size=&quot;size20&quot;&gt;5. Jeju 모듈 수정&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;account.png&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V7K78/btqBtL8vYd8/aZVw0AaduFwi39RbRPpGh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V7K78/btqBtL8vYd8/aZVw0AaduFwi39RbRPpGh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V7K78/btqBtL8vYd8/aZVw0AaduFwi39RbRPpGh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV7K78%2FbtqBtL8vYd8%2FaZVw0AaduFwi39RbRPpGh0%2Fimg.png&quot; data-filename=&quot;account.png&quot; data-origin-width=&quot;418&quot; data-origin-height=&quot;528&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;보상 트랜잭션 처리를 위한 Handler 메소드를 추가하기 위해 Aggregate 클래스를 수정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Account.java&lt;/p&gt;
&lt;pre id=&quot;code_1579928000123&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Aggregate
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class Account {
    @AggregateIdentifier
    @Id
    private String accountID;
    private Long balance;
    private final transient Random random = new Random();

    @CommandHandler
    public Account(AccountCreationCommand command) throws IllegalAccessException {
        log.debug(&quot;handling {}&quot;, command);
        if (command.getBalance() &amp;lt;= 0)
            throw new IllegalAccessException(&quot;유효하지 않은 입력입니다.&quot;);
        apply(new AccountCreationEvent(command.getAccountID(), command.getBalance()));
    }

    @EventSourcingHandler
    protected void on(AccountCreationEvent event) {
        log.debug(&quot;event {}&quot;, event);
        this.accountID = event.getAccountID();
        this.balance = event.getBalance();
    }

    @CommandHandler
    protected void on(JejuBankTransferCommand command) throws InterruptedException {
        if (random.nextBoolean())
            TimeUnit.SECONDS.sleep(15);

        log.debug(&quot;handling {}&quot;, command);
        if (this.balance &amp;lt; command.getAmount()) {
            apply(TransferDeniedEvent.builder()
                                        .srcAccountID(command.getSrcAccountID())
                                        .dstAccountID(command.getDstAccountID())
                                        .amount(command.getAmount())
                                        .description(&quot;잔고가 부족합니다.&quot;)
                                        .transferID(command.getTransferID())
                                     .build());
        } else {
            apply(TransferApprovedEvent.builder()
                                            .srcAccountID(command.getSrcAccountID())
                                            .dstAccountID(command.getDstAccountID())
                                            .transferID(command.getTransferID())
                                            .amount(command.getAmount())
                                        .build());
        }
    }

    @EventSourcingHandler
    protected void on(TransferApprovedEvent event) {
        log.debug(&quot;event {}&quot;, event);
        this.balance -= event.getAmount();
    }

    @CommandHandler
    protected void on(JejuBankCancelTransferCommand command) {
        log.debug(&quot;handling {}&quot;, command);
        apply(CompletedCancelTransferEvent.builder()
                                            .srcAccountID(command.getSrcAccountID())
                                            .dstAccountID(command.getDstAccountID())
                                            .transferID(command.getTransferID())
                                            .amount(command.getAmount())
                                          .build());
    }

    @EventSourcingHandler
    protected void on(CompletedCancelTransferEvent event) {
        log.debug(&quot;event {}&quot;, event);
        this.balance += event.getAmount();
    }

    @CommandHandler
    protected void on(JejuBankCompensationCancelCommand command) {
        log.debug(&quot;handling {}&quot;, command);
        apply(CompletedCompensationCancelEvent.builder()
                                                .srcAccountID(command.getSrcAccountID())
                                                .dstAccountID(command.getDstAccountID())
                                                .transferID(command.getTransferID())
                                                .amount(command.getAmount())
                                              .build());
    }

    @EventSourcingHandler
    protected void on(CompletedCompensationCancelEvent event) {
        log.debug(&quot;event {}&quot;, event);
        this.balance -= event.getAmount();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 클래스 구현 내용 중에서 Timeout 테스트를 위해 50% 확률로 Sleep 하도록 임의로 추가하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1579928054695&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if (random.nextBoolean())
	TimeUnit.SECONDS.sleep(15);&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;h4 data-ke-size=&quot;size20&quot;&gt;6. Command 모듈 수정&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Command 클래스 수정을 위해 command 패키지 하위 MoneyTransferCommand 클래스 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;커맨드 Command.png&quot; data-origin-width=&quot;417&quot; data-origin-height=&quot;503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5e8HH/btqBtMfjWdx/HGPlPk0LT3R4JL7vMIYsp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5e8HH/btqBtMfjWdx/HGPlPk0LT3R4JL7vMIYsp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5e8HH/btqBtMfjWdx/HGPlPk0LT3R4JL7vMIYsp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5e8HH%2FbtqBtMfjWdx%2FHGPlPk0LT3R4JL7vMIYsp1%2Fimg.png&quot; data-filename=&quot;커맨드 Command.png&quot; data-origin-width=&quot;417&quot; data-origin-height=&quot;503&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. 클래스 파일을 수정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;MoneyTransferCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579929966937&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Builder
@ToString
@Getter
public class MoneyTransferCommand {
    private String srcAccountID;
    @TargetAggregateIdentifier
    private String dstAccountID;
    private Long amount;
    private String transferID;
    private BankType bankType;

    public enum BankType{
        JEJU(command -&amp;gt; new TransferComamndFactory(new JejuBankTransferCommand(),new JejuBankCancelTransferCommand(), new JejuBankCompensationCancelCommand())),
        SEOUL(command -&amp;gt; new TransferComamndFactory(new SeoulBankTransferCommand(), new SeoulBankCancelTransferCommand(), new SeoulBankCompensationCancelCommand()));

        private Function&amp;lt;MoneyTransferCommand, TransferComamndFactory&amp;gt; expression;
        BankType(Function&amp;lt;MoneyTransferCommand, TransferComamndFactory&amp;gt; expression){ this.expression = expression;}
        public TransferComamndFactory getCommandFactory(MoneyTransferCommand command){
            TransferComamndFactory factory = this.expression.apply(command);
            factory.create(command.getSrcAccountID(), command.getDstAccountID(), command.amount, command.getTransferID());
            return factory;
        }

    }

    public static MoneyTransferCommand of(TransferDTO dto){
        return MoneyTransferCommand.builder()
                .srcAccountID(dto.getSrcAccountID())
                .dstAccountID(dto.getDstAccountID())
                .amount(dto.getAmount())
                .bankType(dto.getBankType())
                .transferID(UUID.randomUUID().toString())
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. 보상 트랜잭션 구현을 위해 Saga 클래스를 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;saga.png&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;501&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cw6Ev7/btqBuiEz7rD/z7VPzOx8k7FPJVKMuBKU30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cw6Ev7/btqBuiEz7rD/z7VPzOx8k7FPJVKMuBKU30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cw6Ev7/btqBuiEz7rD/z7VPzOx8k7FPJVKMuBKU30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcw6Ev7%2FbtqBuiEz7rD%2Fz7VPzOx8k7FPJVKMuBKU30%2Fimg.png&quot; data-filename=&quot;saga.png&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;501&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;4. Saga 클래스를 수정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransferManager.java&lt;/p&gt;
&lt;pre id=&quot;code_1579930216873&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Saga
@Slf4j
public class TransferManager {
    @Autowired
    private transient CommandGateway commandGateway;
    private boolean isExecutingCompensation = false;
    private boolean isAbortingCompensation = false;
    private TransferComamndFactory comamndFactory;

    @StartSaga
    @SagaEventHandler(associationProperty = &quot;transferID&quot;)
    protected void on(MoneyTransferEvent event) {

        log.debug(&quot;Created saga instance&quot;);
        log.debug(&quot;event : {}&quot;, event);
        comamndFactory = event.getComamndFactory();
        SagaLifecycle.associateWith(&quot;srcAccountID&quot;, event.getSrcAccountID());

        try {
            log.info(&quot;계좌 이체 시작 : {} &quot;, event);
            commandGateway.sendAndWait(comamndFactory.getTransferCommand(), 10, TimeUnit.SECONDS);
        } catch (CommandExecutionException e) {
            log.error(&quot;Failed transfer process. Start cancel transaction&quot;);
            cancelTransfer();
        }
    }

    private void cancelTransfer() {
        isExecutingCompensation = true;
        log.info(&quot;보상 트랜잭션 요청&quot;);
        commandGateway.send(comamndFactory.getAbortTransferCommand());
    }

    @SagaEventHandler(associationProperty = &quot;srcAccountID&quot;)
    protected void on(CompletedCancelTransferEvent event) {
        isExecutingCompensation = false;
        if (!isAbortingCompensation) {
            log.info(&quot;계좌 이체 취소 완료 : {} &quot;, event);
            SagaLifecycle.end();
        }
    }

    @SagaEventHandler(associationProperty = &quot;srcAccountID&quot;)
    protected void on(TransferDeniedEvent event) {
        log.info(&quot;계좌 이체 실패 : {}&quot;, event);
        log.info(&quot;실패 사유 : {}&quot;, event.getDescription());
        if(isExecutingCompensation){
            isAbortingCompensation = true;
            log.info(&quot;보상 트랜잭션 취소 요청 : {}&quot;, event);
            commandGateway.send(comamndFactory.getCompensationAbortCommand());
        }
        else {
            SagaLifecycle.end();
        }
    }

    @SagaEventHandler(associationProperty = &quot;srcAccountID&quot;)
    @EndSaga
    protected void on(CompletedCompensationCancelEvent event){
        isAbortingCompensation = false;
        log.info(&quot;보상 트랜잭션 취소 완료 : {}&quot;,event);
    }

    @SagaEventHandler(associationProperty = &quot;srcAccountID&quot;)
    protected void on(TransferApprovedEvent event) {
        if (!isExecutingCompensation &amp;amp;&amp;amp; !isAbortingCompensation) {
            log.info(&quot;이체 금액 {} 계좌 반영 요청 : {}&quot;,event.getAmount(), event);
            SagaLifecycle.associateWith(&quot;accountID&quot;, event.getDstAccountID());
            commandGateway.send(TransferApprovedCommand.builder()
                                                            .accountID(event.getDstAccountID())
                                                            .amount(event.getAmount())
                                                            .transferID(event.getTransferID())
                                                       .build());
        }
    }

    @SagaEventHandler(associationProperty = &quot;accountID&quot;)
    @EndSaga
    protected void on(DepositCompletedEvent event){
        log.info(&quot;계좌 이체 성공 : {}&quot;, event);
    }
}&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;h4 data-ke-size=&quot;size20&quot;&gt;5. 테스트 결과&lt;/h4&gt;
&lt;p&gt;정상&lt;/p&gt;
&lt;pre id=&quot;code_1579931398430&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;com.cqrs.command.saga.TransferManager    : event : MoneyTransferEvent(dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, srcAccountID=test, amount=1, transferID=30dbfa4a-ea98-4343-bcbd-00e906870090, comamndFactory=com.cqrs.command.transfer.factory.TransferComamndFactory@d713db5)
com.cqrs.command.saga.TransferManager    : 계좌 이체 시작 : MoneyTransferEvent(dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, srcAccountID=test, amount=1, transferID=30dbfa4a-ea98-4343-bcbd-00e906870090, comamndFactory=com.cqrs.command.transfer.factory.TransferComamndFactory@d713db5) 
com.cqrs.command.saga.TransferManager    : 이체 금액 1 계좌 반영 요청 : TransferApprovedEvent(srcAccountID=test, dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, transferID=30dbfa4a-ea98-4343-bcbd-00e906870090, amount=1)
c.c.command.aggregate.AccountAggregate   : handling TransferApprovedCommand(accountID=a31faade-5b57-4435-85da-1de0ed1c55c4, amount=1, transferID=30dbfa4a-ea98-4343-bcbd-00e906870090)
c.c.command.aggregate.AccountAggregate   : applying DepositMoneyEvent(holderID=b01fae84-e8a5-427d-a5f4-baa7376b7163, accountID=a31faade-5b57-4435-85da-1de0ed1c55c4, amount=1)
c.c.command.aggregate.AccountAggregate   : balance 31
com.cqrs.command.saga.TransferManager    : 계좌 이체 성공 : DepositCompletedEvent(accountID=a31faade-5b57-4435-85da-1de0ed1c55c4, transferID=30dbfa4a-ea98-4343-bcbd-00e906870090)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Timeout&lt;/p&gt;
&lt;pre id=&quot;code_1579930631925&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;com.cqrs.command.saga.TransferManager    : event : MoneyTransferEvent(dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, srcAccountID=test, amount=1, transferID=7a01bd7a-6ce0-43bd-93e9-ee0224dfd791, comamndFactory=com.cqrs.command.transfer.factory.TransferComamndFactory@3226f9ff)
com.cqrs.command.saga.TransferManager    : 계좌 이체 시작 : MoneyTransferEvent(dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, srcAccountID=test, amount=1, transferID=7a01bd7a-6ce0-43bd-93e9-ee0224dfd791, comamndFactory=com.cqrs.command.transfer.factory.TransferComamndFactory@3226f9ff) 
com.cqrs.command.saga.TransferManager    : Failed transfer process. Start cancel transaction
com.cqrs.command.saga.TransferManager    : 보상 트랜잭션 요청
com.cqrs.command.saga.TransferManager    : 계좌 이체 취소 완료 : CompletedAbortTransferEvent(srcAccountID=test, dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, amount=1, transferID=7a01bd7a-6ce0-43bd-93e9-ee0224dfd791) &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;잔고 부족&lt;/p&gt;
&lt;pre id=&quot;code_1579930475677&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;c.c.command.aggregate.AccountAggregate   : handling MoneyTransferCommand(srcAccountID=test, dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, amount=300, transferID=f73d08ba-004c-4e44-9929-2866f6ea02da, bankType=JEJU)
com.cqrs.command.saga.TransferManager    : Created saga instance
com.cqrs.command.saga.TransferManager    : event : MoneyTransferEvent(dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, srcAccountID=test, amount=300, transferID=f73d08ba-004c-4e44-9929-2866f6ea02da, comamndFactory=com.cqrs.command.transfer.factory.TransferComamndFactory@66a12a11)
com.cqrs.command.saga.TransferManager    : 계좌 이체 시작 : MoneyTransferEvent(dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, srcAccountID=test, amount=300, transferID=f73d08ba-004c-4e44-9929-2866f6ea02da, comamndFactory=com.cqrs.command.transfer.factory.TransferComamndFactory@66a12a11) 
com.cqrs.command.saga.TransferManager    : 계좌 이체 실패 : TransferDeniedEvent(srcAccountID=test, dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, transferID=f73d08ba-004c-4e44-9929-2866f6ea02da, amount=300, description=잔고가 부족합니다.)
com.cqrs.command.saga.TransferManager    : 실패 사유 : 잔고가 부족합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;잔고 부족 + Timeout&lt;/p&gt;
&lt;pre id=&quot;code_1579930529169&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;com.cqrs.command.saga.TransferManager    : event : MoneyTransferEvent(dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, srcAccountID=test, amount=300, transferID=b132f585-3be8-49b0-ab67-0293a05684ff, comamndFactory=com.cqrs.command.transfer.factory.TransferComamndFactory@3d4d8677)
com.cqrs.command.saga.TransferManager    : 계좌 이체 시작 : MoneyTransferEvent(dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, srcAccountID=test, amount=300, transferID=b132f585-3be8-49b0-ab67-0293a05684ff, comamndFactory=com.cqrs.command.transfer.factory.TransferComamndFactory@3d4d8677) 
com.cqrs.command.saga.TransferManager    : Failed transfer process. Start cancel transaction
com.cqrs.command.saga.TransferManager    : 보상 트랜잭션 요청
com.cqrs.command.saga.TransferManager    : 계좌 이체 실패 : TransferDeniedEvent(srcAccountID=test, dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, transferID=b132f585-3be8-49b0-ab67-0293a05684ff, amount=300, description=잔고가 부족합니다.)
com.cqrs.command.saga.TransferManager    : 실패 사유 : 잔고가 부족합니다.
com.cqrs.command.saga.TransferManager    : 보상 트랜잭션 취소 요청 : TransferDeniedEvent(srcAccountID=test, dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, transferID=b132f585-3be8-49b0-ab67-0293a05684ff, amount=300, description=잔고가 부족합니다.)
com.cqrs.command.saga.TransferManager    : 보상 트랜잭션 취소 완료 : CompletedCompensationAbortEvent(srcAccountID=test, dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, amount=300, transferID=b132f585-3be8-49b0-ab67-0293a05684ff)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;테스트 결과 각기 다른 상황에서 정상적으로 수행되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. 마치며&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이상으로 AxonFramework에 대한 포스팅을 마치겠습니다. 테스팅에 대해서는 다루지 않았는데, 분산 App 환경에서 테스트 코드 작성은 반드시 필요하다고 생각합니다. 따라서 &lt;a href=&quot;https://docs.axoniq.io/reference-guide/implementing-domain-logic/complex-business-transactions/testing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;공식문서&lt;/u&gt;&lt;/a&gt;를 참고하여 테스트 코드 작성 방법을 익힌다면, 보다 안전한 프로그램이 될 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;그 외에 쿠버네티스 지원, Tracing에 대해서도 공식문서에 소개되어 있으니 참고바랍니다. 또한, 지금까지 구현한 프로젝트 내용은 &lt;a href=&quot;https://github.com/cla9/axon-account-example&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;깃헙&lt;/u&gt;&lt;/a&gt;에 업로드 했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;포스팅 내용 중 개선사항에 대해서는 댓글로 남겨주시면, 확인 후 내용 반영하도록 하겠습니다.&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>Event sourcing</category>
      <category>saga</category>
      <category>saga pattern</category>
      <category>마이크로서비스아키텍처</category>
      <category>사가 패턴</category>
      <category>이벤트소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/24</guid>
      <comments>https://cla9.tistory.com/24#entry24comment</comments>
      <pubDate>Sat, 25 Jan 2020 14:58:45 +0900</pubDate>
    </item>
    <item>
      <title>19. Saga 패턴을 활용한 트랜잭션 관리 - 2</title>
      <link>https://cla9.tistory.com/23</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;640&quot; data-origin-height=&quot;712&quot; data-origin-width=&quot;1448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0Au75/btqBsGfFedI/ePucekRBYQ14L7YJphDMrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0Au75/btqBsGfFedI/ePucekRBYQ14L7YJphDMrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0Au75/btqBsGfFedI/ePucekRBYQ14L7YJphDMrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0Au75%2FbtqBsGfFedI%2FePucekRBYQ14L7YJphDMrk%2Fimg.png&quot; width=&quot;640&quot; data-origin-height=&quot;712&quot; data-origin-width=&quot;1448&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅은 분산 트랜잭션 제어를 위한 Saga 패턴을 구현하겠습니다. 보상 트랜잭션까지 같이 구현하면 내용이 복잡하므로 보상 트랜잭션 및 Deadline 기능은 다음 포스팅에서 다루겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Saga 패턴 사용을 위한 가상의 시나리오는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;테스트 시나리오&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Jeju 모듈에 계좌를 개설한다.(계좌명 : test, 잔고 : 100)&lt;/p&gt;
&lt;p&gt;2. Command 모듈에 계정 &amp;amp; 계좌를 개설한다.&lt;/p&gt;
&lt;p&gt;3. Jeju 모듈의 계좌에서 30원을 Command 모듈 계좌로 이체한다.&lt;/p&gt;
&lt;p&gt;4. Jeju 모델 DB의 계좌와 Query 모델 DB를 통해서 이체금액을 확인한다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;a style=&quot;letter-spacing: 0px;&quot; href=&quot;https://cla9.tistory.com/21&quot;&gt;&lt;u&gt;이전 포스팅&lt;/u&gt;&lt;/a&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;에서 구현한 Jeju 모듈을 활용하여 코드 구현을 진행하겠습니다. Saga 요청을 위한 API 및 Saga 인스턴스는 Command 모듈에 생성하도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Common 모듈 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. 공통으로 사용할 Command 및 Event 추가를 위해 Common 모듈 Command 패키지 및 Command 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;416&quot; data-origin-width=&quot;423&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZGfRb/btqBuiq0VAp/s4habuo4k5Y8gk7QjsM3Gk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZGfRb/btqBuiq0VAp/s4habuo4k5Y8gk7QjsM3Gk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZGfRb/btqBuiq0VAp/s4habuo4k5Y8gk7QjsM3Gk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZGfRb%2FbtqBuiq0VAp%2Fs4habuo4k5Y8gk7QjsM3Gk%2Fimg.png&quot; data-origin-height=&quot;416&quot; data-origin-width=&quot;423&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. 생성한 Command 클래스를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransferComamndFactory.java&lt;/p&gt;
&lt;pre id=&quot;code_1579880299950&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class TransferComamndFactory {
    private final AbstractTransferCommand transferCommand;

    public void create(String srcAccountID, String dstAccountID, Long amount, String transferID){
        transferCommand.create(srcAccountID, dstAccountID, amount, transferID);
    }

    public AbstractTransferCommand getTransferCommand(){
        return this.transferCommand;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 클래스는 계좌 이체와 연관된 Command를 생성하는 Factory 클래스입니다. 나중에 보상 트랜잭션을 위한 Command를 같이 관리하기 위하여 생성하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AbstractTransferCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579880318939&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@ToString
@Getter
public abstract class AbstractTransferCommand {
    @TargetAggregateIdentifier
    protected String srcAccountID;
    protected String dstAccountID;
    protected Long amount;
    protected String transferID;

    public AbstractTransferCommand create(String srcAccountID, String dstAccountID, Long amount, String transferID) {
        this.srcAccountID = srcAccountID;
        this.dstAccountID = dstAccountID;
        this.transferID = transferID;
        this.amount = amount;
        return this;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;계좌 이체 요청을 위한 클래스입니다. srcAccountID는 인출할 계좌 ID이며, dstAccountID는 송금 대상 계좌 ID입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;JejuBankTransferCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579916765569&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class JejuBankTransferCommand extends AbstractTransferCommand {
    @Override
    public String toString() {
        return &quot;JejuBankTransferCommand{&quot; +
                &quot;srcAccountID='&quot; + srcAccountID + '\'' +
                &quot;, dstAccountID='&quot; + dstAccountID + '\'' +
                &quot;, amount=&quot; + amount +
                &quot;, transferID='&quot; + transferID + '\'' +
                '}';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;SeoulBankTransferCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579916798030&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public class SeoulBankTransferCommand extends AbstractTransferCommand {
    @Override
    public String toString() {
        return &quot;SeoulBankTransferCommand{&quot; +
                &quot;srcAccountID='&quot; + srcAccountID + '\'' +
                &quot;, dstAccountID='&quot; + dstAccountID + '\'' +
                &quot;, amount=&quot; + amount +
                &quot;, transferID='&quot; + transferID + '\'' +
                '}';
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. Common 모듈 event 패키지 하위에 Event 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;450&quot; data-origin-width=&quot;429&quot; data-filename=&quot;공통 Event.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhv54F/btqBtGGnZAG/baqWBACd1DKYXq84U5LcMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhv54F/btqBtGGnZAG/baqWBACd1DKYXq84U5LcMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhv54F/btqBtGGnZAG/baqWBACd1DKYXq84U5LcMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbhv54F%2FbtqBtGGnZAG%2FbaqWBACd1DKYXq84U5LcMk%2Fimg.png&quot; data-origin-height=&quot;450&quot; data-origin-width=&quot;429&quot; data-filename=&quot;공통 Event.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;4. Event 클래스를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;MoneyTransferEvent.java&lt;/p&gt;
&lt;pre id=&quot;code_1579880531054&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Builder
@ToString
@Getter
public class MoneyTransferEvent {
    private String dstAccountID;
    private String srcAccountID;
    private Long amount;
    private String transferID;
    private TransferComamndFactory comamndFactory;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 클래스는 Command 모듈로 계좌이체 요청이 들어오면, 인출할 대상 계좌번호(srcAccountID) 및 입금 계좌번호(dstAccountID), 그리고 이체 금액(amount)와 같은 기본정보와 트랜잭션간 고유 키(transferID) 및 요청 Command 구분 정보를 담고있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransferApprovedEvent.java&lt;/p&gt;
&lt;pre id=&quot;code_1579880542407&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Builder
@ToString
@Getter
public class TransferApprovedEvent {
    private String srcAccountID;
    private String dstAccountID;
    private String transferID;
    private Long amount;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 클래스는 계좌이체가 성공되었을 때, 금액을 반영하기 위한 Event입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransferDeniedEvent.java&lt;/p&gt;
&lt;pre id=&quot;code_1579927421015&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@Builder
@ToString
public class TransferDeniedEvent {
    private String srcAccountID;
    private String dstAccountID;
    private String transferID;
    private Long amount;
    private String description;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 클래스는 계좌이체가 거절될 때, 요청 App에 거절 내역을 전달하기 위한 Event입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Jeju 모듈 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Jeju 은행 모듈&lt;b&gt; build.gradle&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;473&quot; data-origin-width=&quot;400&quot; data-filename=&quot;jeju-build.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgkzUk/btqBua0X4BG/ktq9Pn06f72DphRaaWJZV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgkzUk/btqBua0X4BG/ktq9Pn06f72DphRaaWJZV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgkzUk/btqBua0X4BG/ktq9Pn06f72DphRaaWJZV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgkzUk%2FbtqBua0X4BG%2Fktq9Pn06f72DphRaaWJZV0%2Fimg.png&quot; data-origin-height=&quot;473&quot; data-origin-width=&quot;400&quot; data-filename=&quot;jeju-build.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;/&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;style1&quot; /&gt;
&lt;p&gt;2. Jeju 모듈에 State-Stored-Aggregate 구현을 위해 JPA 및 DB 의존성을 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1579878874210&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;ext{
    axonVersion = &quot;4.2.1&quot;
}
dependencies{
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation group: 'org.axonframework', name: 'axon-spring-boot-starter', version: &quot;$axonVersion&quot;
    implementation group: 'org.postgresql', name: 'postgresql', version: '42.2.6'
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. Jeju 모듈 resources 패키지 하위 &lt;b&gt;application.yml&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;446&quot; data-origin-width=&quot;402&quot; data-filename=&quot;application.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ARJtN/btqBs0yeNTJ/WzXfw0KfZqjMxrNMFm5gY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ARJtN/btqBs0yeNTJ/WzXfw0KfZqjMxrNMFm5gY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ARJtN/btqBs0yeNTJ/WzXfw0KfZqjMxrNMFm5gY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FARJtN%2FbtqBs0yeNTJ%2FWzXfw0KfZqjMxrNMFm5gY0%2Fimg.png&quot; data-origin-height=&quot;446&quot; data-origin-width=&quot;402&quot; data-filename=&quot;application.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;/&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;style1&quot; /&gt;
&lt;p&gt;4. datasource 속성을 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1579879056755&quot; class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;server:
  port: 9091

spring:
  application:
    name: eventsourcing-cqrs-jejuBank
  datasource:
    platform: postgres
    url: jdbc:postgresql://localhost:5432/jeju
    username: jeju
    password: jeju
  jpa:
    hibernate:
      ddl-auto: update

axon:
  serializer:
    general: xstream
  axonserver:
    servers: localhost:8124

logging.level.com.cqrs.jeju : debug&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;DB는 Command와 Query와 동일하게 Postgre를 사용하였으며, jeju 계정을 별도로 생성하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;5. 계좌 생성을 위해 Jeju 모듈 하위에 다음과 같은 패키지들을 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;477&quot; data-origin-width=&quot;394&quot; data-filename=&quot;jeju 기본 패키지 구조.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b77Fnc/btqBvsGlpf9/dA2aRjLKJ0IUWZLQk9Lkek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b77Fnc/btqBvsGlpf9/dA2aRjLKJ0IUWZLQk9Lkek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b77Fnc/btqBvsGlpf9/dA2aRjLKJ0IUWZLQk9Lkek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb77Fnc%2FbtqBvsGlpf9%2FdA2aRjLKJ0IUWZLQk9Lkek%2Fimg.png&quot; data-origin-height=&quot;477&quot; data-origin-width=&quot;394&quot; data-filename=&quot;jeju 기본 패키지 구조.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;/&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;style1&quot; /&gt;
&lt;p&gt;6. aggregate 패키지 내 aggregate 클래스부터 구현하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Account.java&lt;/p&gt;
&lt;pre id=&quot;code_1579879656319&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;
@Entity
@Aggregate
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class Account {
    @AggregateIdentifier
    @Id
    private String accountID;
    private Long balance;
    
    @CommandHandler
    public Account(AccountCreationCommand command) throws IllegalAccessException {
        log.debug(&quot;handling {}&quot;, command);
        if (command.getBalance() &amp;lt;= 0)
            throw new IllegalAccessException(&quot;유효하지 않은 입력입니다.&quot;);
        apply(new AccountCreationEvent(command.getAccountID(), command.getBalance()));
    }

    @EventSourcingHandler
    protected void on(AccountCreationEvent event) {
        log.debug(&quot;event {}&quot;, event);
        this.accountID = event.getAccountID();
        this.balance = event.getBalance();
    }

    @CommandHandler
    protected void on(JejuBankTransferCommand command) throws InterruptedException {

        log.debug(&quot;handling {}&quot;, command);
        if (this.balance &amp;lt; command.getAmount()) {
            apply(TransferDeniedEvent.builder()
                                        .srcAccountID(command.getSrcAccountID())
                                        .dstAccountID(command.getDstAccountID())
                                        .amount(command.getAmount())
                                        .description(&quot;잔고가 부족합니다.&quot;)
                                        .transferID(command.getTransferID())
                                     .build());
        } else {
            apply(TransferApprovedEvent.builder()
                                            .srcAccountID(command.getSrcAccountID())
                                            .dstAccountID(command.getDstAccountID())
                                            .transferID(command.getTransferID())
                                            .amount(command.getAmount())
                                        .build());
        }
    }

    @EventSourcingHandler
    protected void on(TransferApprovedEvent event) {
        log.debug(&quot;event {}&quot;, event);
        this.balance -= event.getAmount();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;트랜잭션 테스트 도중 Account 정보를 DB에서 바로 확인하기 위해 State-Stored Aggregate 형태로 구현하였습니다. 위 코드 내용은 Command 어플리케이션 구현파트에서 다룬 내용이 주이기에 별도 설명은 생략하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;7. command 패키지내 Command 클래스 생성 및 이를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountCreationCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579880660562&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@NoArgsConstructor
@AllArgsConstructor
@ToString
@Getter
public class AccountCreationCommand {
    @TargetAggregateIdentifier
    private String accountID;
    private Long balance;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;8. dto 패키지내 dto 클래스 생성 및 이를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountDTO.java&lt;/p&gt;
&lt;pre id=&quot;code_1579880734315&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@NoArgsConstructor
@AllArgsConstructor
@ToString
@Getter
public class AccountDTO {
    private String accountID;
    private Long balance;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;9. event 패키지내 event 클래스를 생성 및 이를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountCreationEvent.java&lt;/p&gt;
&lt;pre id=&quot;code_1579880771606&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@ToString
@RequiredArgsConstructor
@Getter
public class AccountCreationEvent {
    private final String accountID;
    private final Long balance;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;해당 이벤트는 Jeju 모듈내에서만 유효하기 때문에 공통 모듈에 생성하지 않았습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;10. service 패키지내 service 클래스 생성 및 이를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountService.java&lt;/p&gt;
&lt;pre id=&quot;code_1579880867134&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface AccountService {
    String createAccount(AccountDTO accountDTO);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountServiceImpl.java&lt;/p&gt;
&lt;pre id=&quot;code_1579880882626&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class AccountServiceImpl implements AccountService {
    private final CommandGateway commandGateway;

    @Override
    public String createAccount(AccountDTO accountDTO) {
        return commandGateway.sendAndWait(new AccountCreationCommand(accountDTO.getAccountID(), accountDTO.getBalance()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;11. controller 패키지내 controller 클래스 생성 및 이를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountController.java&lt;/p&gt;
&lt;pre id=&quot;code_1579880917699&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class AccountController {
    private final AccountService accountService;

    @PostMapping(&quot;/account&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; createAccount(@RequestBody AccountDTO accountDTO){
        return ResponseEntity.ok().body(accountService.createAccount(accountDTO));
    }
}&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;h4 data-ke-size=&quot;size20&quot;&gt;4. Command 모듈 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Command 구현 포스팅 8번 항목에서 AxonServerCommandBus를 SimpleCommandBus로 대체했습니다. Saga 트랜잭션에서는 다른 모듈에 대하여 Command 요청이 필요하므로 기존에 SImpleCommandBus 설정을 해제해야합니다. 이를 위해 config 패키지내에 있는 AxonConfig 클래스를 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;527&quot; data-origin-width=&quot;400&quot; data-filename=&quot;axonConfig.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJQ2Bs/btqBuPV55aW/jgVPTHj8Pww2PeMAPjda2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJQ2Bs/btqBuPV55aW/jgVPTHj8Pww2PeMAPjda2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJQ2Bs/btqBuPV55aW/jgVPTHj8Pww2PeMAPjda2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJQ2Bs%2FbtqBuPV55aW%2FjgVPTHj8Pww2PeMAPjda2k%2Fimg.png&quot; data-origin-height=&quot;527&quot; data-origin-width=&quot;400&quot; data-filename=&quot;axonConfig.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;/&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;style1&quot; /&gt;
&lt;p&gt;2. AxonConfig 파일에서 commandBus Bean 로직을 주석처리하거나 삭제합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonConfig.java&lt;/p&gt;
&lt;pre id=&quot;code_1579881546959&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@AutoConfigureAfter(AxonAutoConfiguration.class)
public class AxonConfig {
	(...중략...)
//    @Bean
//    SimpleCommandBus commandBus(TransactionManager transactionManager){
//        return  SimpleCommandBus.builder().transactionManager(transactionManager).build();
//    }
	(...중략...)
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. 계좌이체 요청 및 반영을 위한 Command 요청을 위해 command 패키지 내 command 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;497&quot; data-origin-width=&quot;397&quot; data-filename=&quot;커맨드 command.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsRnfH/btqBubyN2GV/8qbDHLJX15tXQLgzJpZCd0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsRnfH/btqBubyN2GV/8qbDHLJX15tXQLgzJpZCd0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsRnfH/btqBubyN2GV/8qbDHLJX15tXQLgzJpZCd0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsRnfH%2FbtqBubyN2GV%2F8qbDHLJX15tXQLgzJpZCd0%2Fimg.png&quot; data-origin-height=&quot;497&quot; data-origin-width=&quot;397&quot; data-filename=&quot;커맨드 command.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;/&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;style1&quot; /&gt;
&lt;p&gt;4. Command 클래스 내용을 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransferApprovedCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579881778775&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@ToString
@Getter
@Builder
public class TransferApprovedCommand {
    @TargetAggregateIdentifier
    private String accountID;
    private Long amount;
    private String transferID;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 클래스는 계좌이체가 정상적으로 수행되었을 때, 요청을 위한 Command 클래스입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;MoneyTransferCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1579881826561&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Builder
@ToString
@Getter
public class MoneyTransferCommand {
    private String srcAccountID;
    @TargetAggregateIdentifier
    private String dstAccountID;
    private Long amount;
    private String transferID;
    private BankType bankType;

    public enum BankType{
        JEJU(command -&amp;gt; new TransferComamndFactory(new JejuBankTransferCommand()),
        SEOUL(command -&amp;gt; new TransferComamndFactory(new SeoulBankTransferCommand());

        private Function&amp;lt;MoneyTransferCommand, TransferComamndFactory&amp;gt; expression;
        BankType(Function&amp;lt;MoneyTransferCommand, TransferComamndFactory&amp;gt; expression){ this.expression = expression;}
        public TransferComamndFactory getCommandFactory(MoneyTransferCommand command){
            TransferComamndFactory factory = this.expression.apply(command);
            factory.create(command.getSrcAccountID(), command.getDstAccountID(), command.amount, command.getTransferID());
            return factory;
        }

    }

    public static MoneyTransferCommand of(TransferDTO dto){
        return MoneyTransferCommand.builder()
                .srcAccountID(dto.getSrcAccountID())
                .dstAccountID(dto.getDstAccountID())
                .amount(dto.getAmount())
                .bankType(dto.getBankType())
                .transferID(UUID.randomUUID().toString())
                .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;계좌 이체 요청시, 은행구분에 따른 Command 생성을 달리 하기 위하여 enum을 사용했습니다. 또한, DTO 클래스를 Command 클래스로 변환하기 위한 Factory 메소드를 추가하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;5. DTO 추가를 위해 dto 패키지 내 dto 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;506&quot; data-origin-width=&quot;396&quot; data-filename=&quot;transfoerDTO.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bEiQTQ/btqBs0rtZ2F/3IpXG8vk56rOuSczp9n1d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bEiQTQ/btqBs0rtZ2F/3IpXG8vk56rOuSczp9n1d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bEiQTQ/btqBs0rtZ2F/3IpXG8vk56rOuSczp9n1d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbEiQTQ%2FbtqBs0rtZ2F%2F3IpXG8vk56rOuSczp9n1d0%2Fimg.png&quot; data-origin-height=&quot;506&quot; data-origin-width=&quot;396&quot; data-filename=&quot;transfoerDTO.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransferDTO.java&lt;/p&gt;
&lt;pre id=&quot;code_1579881658751&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class TransferDTO {
    private String srcAccountID;
    private String dstAccountID;
    private Long amount;
    private MoneyTransferCommand.BankType bankType;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;6. Service 메소드 추가를 위해서 service 패키지 하위 service 클래스를 수정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;473&quot; data-origin-width=&quot;405&quot; data-filename=&quot;커맨드 service.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/binw3Q/btqBtGl0e21/Cx2pesrdkvmfCYLRDciTc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/binw3Q/btqBtGl0e21/Cx2pesrdkvmfCYLRDciTc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/binw3Q/btqBtGl0e21/Cx2pesrdkvmfCYLRDciTc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbinw3Q%2FbtqBtGl0e21%2FCx2pesrdkvmfCYLRDciTc0%2Fimg.png&quot; data-origin-height=&quot;473&quot; data-origin-width=&quot;405&quot; data-filename=&quot;커맨드 service.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransactionService.java&lt;/p&gt;
&lt;pre id=&quot;code_1579882044310&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface TransactionService {
    (...중략...)
    String transferMoney(TransferDTO transferDTO);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransactionServiceImpl.java&lt;/p&gt;
&lt;pre id=&quot;code_1579882074834&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class TransactionServiceImpl implements TransactionService {
    private final CommandGateway commandGateway;
	(...중략...)

    @Override
    public String transferMoney(TransferDTO transferDTO) {
        return commandGateway.sendAndWait(MoneyTransferCommand.of(transferDTO));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;7. API 추가를 위해서 controller 패키지내 Controller에 메소드를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;357&quot; data-origin-width=&quot;398&quot; data-filename=&quot;커맨드 controller.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rchEX/btqBs0LM14g/1gau9qUbnWsdcj0nc1ckV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rchEX/btqBs0LM14g/1gau9qUbnWsdcj0nc1ckV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rchEX/btqBs0LM14g/1gau9qUbnWsdcj0nc1ckV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrchEX%2FbtqBs0LM14g%2F1gau9qUbnWsdcj0nc1ckV0%2Fimg.png&quot; data-origin-height=&quot;357&quot; data-origin-width=&quot;398&quot; data-filename=&quot;커맨드 controller.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransactionController.java&lt;/p&gt;
&lt;pre id=&quot;code_1579882122375&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class TransactionController {
    private final TransactionService transactionService;

    (...중략...)

    @PostMapping(&quot;/transfer&quot;)
    public ResponseEntity&amp;lt;String&amp;gt; transfer(@RequestBody TransferDTO transferDTO){
        return ResponseEntity.ok().body(transactionService.transferMoney(transferDTO));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;8. Aggregate 수정을 위해 aggregate 패키지내 AccountAggregate 클래스를 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;529&quot; data-origin-width=&quot;399&quot; data-filename=&quot;accountAggregate.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY3eMu/btqBtGsLl2i/jOIMlL1Ns5TnQ4K29Kk9a1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY3eMu/btqBtGsLl2i/jOIMlL1Ns5TnQ4K29Kk9a1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY3eMu/btqBtGsLl2i/jOIMlL1Ns5TnQ4K29Kk9a1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY3eMu%2FbtqBtGsLl2i%2FjOIMlL1Ns5TnQ4K29Kk9a1%2Fimg.png&quot; data-origin-height=&quot;529&quot; data-origin-width=&quot;399&quot; data-filename=&quot;accountAggregate.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;/&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;style1&quot; /&gt;
&lt;p&gt;9. Aggregate에 Command 및 EventSourcing 핸들러 로직을 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountAggregate.java&lt;/p&gt;
&lt;pre id=&quot;code_1579882340904&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@NoArgsConstructor
@AllArgsConstructor
@Slf4j
@Aggregate
@EqualsAndHashCode
public class AccountAggregate {
    (...중략...)

    @CommandHandler
    protected void transferMoney(MoneyTransferCommand command) {
        log.debug(&quot;handling {}&quot;, command);
        apply(MoneyTransferEvent.builder()
                .srcAccountID(command.getSrcAccountID())
                .dstAccountID(command.getDstAccountID())
                .amount(command.getAmount())
                .comamndFactory(command.getBankType().getCommandFactory(command))
                .transferID(command.getTransferID())
                .build());
    }

    @CommandHandler
    protected void transferMoney(TransferApprovedCommand command) {
        log.debug(&quot;handling {}&quot;, command);
        apply(new DepositMoneyEvent(this.holderID, command.getAccountID(), command.getAmount()));
        apply(new DepositCompletedEvent(command.getAccountID(), command.getTransferID()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;10. Saga 패키지 및 Saga 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;501&quot; data-origin-width=&quot;403&quot; data-filename=&quot;saga.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/C3Uks/btqBtWIDAPO/8kFaa2rAPrnrB6KHWMiAJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/C3Uks/btqBtWIDAPO/8kFaa2rAPrnrB6KHWMiAJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/C3Uks/btqBtWIDAPO/8kFaa2rAPrnrB6KHWMiAJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FC3Uks%2FbtqBtWIDAPO%2F8kFaa2rAPrnrB6KHWMiAJk%2Fimg.png&quot; data-origin-height=&quot;501&quot; data-origin-width=&quot;403&quot; data-filename=&quot;saga.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;/&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;style1&quot; /&gt;
&lt;p&gt;11. Saga 클래스를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransferManager.java&lt;/p&gt;
&lt;pre id=&quot;code_1579882530232&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Saga
@Slf4j
public class TransferManager {
    @Autowired
    private transient CommandGateway commandGateway;
    private TransferComamndFactory comamndFactory;

    @StartSaga
    @SagaEventHandler(associationProperty = &quot;transferID&quot;)
    protected void on(MoneyTransferEvent event) {
        log.debug(&quot;Created saga instance&quot;);
        log.debug(&quot;event : {}&quot;, event);
        comamndFactory = event.getComamndFactory();
        SagaLifecycle.associateWith(&quot;srcAccountID&quot;, event.getSrcAccountID());

		log.info(&quot;계좌 이체 시작 : {} &quot;, event);
		commandGateway.send(comamndFactory.getTransferCommand());
    }

    @SagaEventHandler(associationProperty = &quot;srcAccountID&quot;)
    protected void on(TransferApprovedEvent event) {
		log.info(&quot;이체 금액 {} 계좌 반영 요청 : {}&quot;, event.getAmount(), event);
        SagaLifecycle.associateWith(&quot;accountID&quot;, event.getDstAccountID());
        commandGateway.send(TransferApprovedCommand.builder()
                .accountID(event.getDstAccountID())
                .amount(event.getAmount())
                .transferID(event.getTransferID())
                .build());
    }
    
    @SagaEventHandler(associationProperty = &quot;srcAccountID&quot;)
    protected void on(TransferDeniedEvent event) {
        log.info(&quot;계좌 이체 실패 : {}&quot;, event);
        log.info(&quot;실패 사유 : {}&quot;, event.getDescription());
		SagaLifecycle.end();
    }

    @SagaEventHandler(associationProperty = &quot;accountID&quot;)
    @EndSaga
    protected void on(DepositCompletedEvent event){
        log.info(&quot;계좌 이체 성공 : {}&quot;, event);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;구현 내용을 간략하게 소개하면 다음과 같습니다. 먼저 @Saga 어노테이션을 통해 해당 클래스가 Saga의 대상임을 지정합니다. 해당 클래스는 NoArgsConstructor로 생성이 되어야하므로 이를 주의합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;@StartSaga는 Saga 인스턴스의 시작점입니다. 여기에서 associationProperty는 해당 인스턴스를 유일하게 구별할 수 있는 속성이 무엇인지 지정합니다. 자세한 내용은 &lt;a href=&quot;https://docs.axoniq.io/reference-guide/implementing-domain-logic/complex-business-transactions/managing-associations&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Axon 공식문서&lt;/u&gt;&lt;/a&gt;를 참고바랍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;생성된 Saga는 트랜잭션이 끝나면 종료되어야합니다. 위 예제에서는 DepositCompletedEvent 메시지를 수신받으면, 전체 트랜잭션이 끝난것으로 판단하여 Saga 인스턴스를 종료하도록 되어있습니다. Saga 인스턴스 종료방법은 크게 두가지입니다. 먼저 위 에제와 같이 명시적으로 end 메소드를 기입하거나 &lt;b&gt;@EndSaga&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;h4 data-ke-size=&quot;size20&quot;&gt;5. 테스트&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Command, Query, Jeju 모듈 App을 기동한 다음, Jeju 은행 계좌를 개설합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1579918969782&quot; class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;POST localhost:9091/account
Content-Type: application/json

{
	&quot;accountID&quot; : &quot;test&quot;,
	&quot;balance&quot; : 100
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;2. 생성된 계좌 내역을 DB에서 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;207&quot; data-origin-width=&quot;368&quot; data-filename=&quot;계좌 개설 확인.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNnK42/btqBxzZBW9i/CxEjYn06kywKBtHbqm5Dt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNnK42/btqBxzZBW9i/CxEjYn06kywKBtHbqm5Dt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNnK42/btqBxzZBW9i/CxEjYn06kywKBtHbqm5Dt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNnK42%2FbtqBxzZBW9i%2FCxEjYn06kywKBtHbqm5Dt0%2Fimg.png&quot; data-origin-height=&quot;207&quot; data-origin-width=&quot;368&quot; data-filename=&quot;계좌 개설 확인.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;/&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;style1&quot; /&gt;
&lt;p&gt;3. Command 모듈에 계정을 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1579919245357&quot; class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;POST http://localhost:8080/holder
Content-Type: application/json

{
	&quot;holderName&quot; : &quot;Kevin&quot;,
	&quot;tel&quot; : &quot;02-2645-5678&quot;,
	&quot;address&quot; : &quot;OO시 OO구&quot;,
    &quot;company&quot; : &quot;Korea&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;4. Command 모듈에 계좌를 개설합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1579919281647&quot; class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;POST http://localhost:8080/account
Content-Type: application/json

{
  &quot;holderID&quot; : &quot;b01fae84-e8a5-427d-a5f4-baa7376b7163&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;5. Query 모듈 DB에서 계정 및 계좌 생성 내역을 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;211&quot; data-origin-width=&quot;832&quot; data-filename=&quot;계정계좌 확인.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/daBaBL/btqBsZ0vLBa/ZX87lTEz5ZIjHghgHqRbZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/daBaBL/btqBsZ0vLBa/ZX87lTEz5ZIjHghgHqRbZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/daBaBL/btqBsZ0vLBa/ZX87lTEz5ZIjHghgHqRbZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdaBaBL%2FbtqBsZ0vLBa%2FZX87lTEz5ZIjHghgHqRbZ0%2Fimg.png&quot; data-origin-height=&quot;211&quot; data-origin-width=&quot;832&quot; data-filename=&quot;계정계좌 확인.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;/&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;style1&quot; /&gt;
&lt;p&gt;6. Jeju 은행 계좌에서 Command 모듈에 개설된 계좌로 30원 이체합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1579919451081&quot; class=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;POST http://localhost:8080/transfer
Content-Type: application/json
{
	&quot;srcAccountID&quot; : &quot;test&quot;,
	&quot;dstAccountID&quot; : &quot;a31faade-5b57-4435-85da-1de0ed1c55c4&quot;,
	&quot;amount&quot; : 30,
	&quot;bankType&quot; : &quot;JEJU&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1579921983561&quot; class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;AccountAggregate   : handling MoneyTransferCommand(srcAccountID=test, dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, amount=30, transferID=3a6583e3-0288-4e07-87f0-c818644e009b, bankType=JEJU)
TransferManager    : Created saga instance
TransferManager    : event : MoneyTransferEvent(dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, srcAccountID=test, amount=30, transferID=3a6583e3-0288-4e07-87f0-c818644e009b, comamndFactory=com.cqrs.command.transfer.factory.TransferComamndFactory@5a65bed)
TransferManager    : 계좌 이체 시작 : MoneyTransferEvent(dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, srcAccountID=test, amount=30, transferID=3a6583e3-0288-4e07-87f0-c818644e009b, comamndFactory=com.cqrs.command.transfer.factory.TransferComamndFactory@5a65bed) 
TransferManager    : 이체 금액 30 계좌 반영 요청 : TransferApprovedEvent(srcAccountID=test, dstAccountID=a31faade-5b57-4435-85da-1de0ed1c55c4, transferID=3a6583e3-0288-4e07-87f0-c818644e009b, amount=30)
AccountAggregate   : handling TransferApprovedCommand(accountID=a31faade-5b57-4435-85da-1de0ed1c55c4, amount=30, transferID=3a6583e3-0288-4e07-87f0-c818644e009b)
AccountAggregate   : applying DepositMoneyEvent(holderID=b01fae84-e8a5-427d-a5f4-baa7376b7163, accountID=a31faade-5b57-4435-85da-1de0ed1c55c4, amount=30)
AccountAggregate   : balance 30
TransferManager    : 계좌 이체 성공 : DepositCompletedEvent(accountID=a31faade-5b57-4435-85da-1de0ed1c55c4, transferID=3a6583e3-0288-4e07-87f0-c818644e009b)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;성공적으로 계좌이체가 완료되었으면 위와 비슷한 로그가 출력될 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;7. DB에서 계좌 상태를 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;188&quot; data-origin-width=&quot;666&quot; data-filename=&quot;query 결과.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x0MIT/btqBxAqHetH/xFOqN6JvsvhzDeB6ZVfPo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x0MIT/btqBxAqHetH/xFOqN6JvsvhzDeB6ZVfPo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x0MIT/btqBxAqHetH/xFOqN6JvsvhzDeB6ZVfPo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx0MIT%2FbtqBxAqHetH%2FxFOqN6JvsvhzDeB6ZVfPo1%2Fimg.png&quot; data-origin-height=&quot;188&quot; data-origin-width=&quot;666&quot; data-filename=&quot;query 결과.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;173&quot; data-origin-width=&quot;402&quot; data-filename=&quot;query 결과2.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mNb9w/btqBtFgqnr6/3eqO2neeGuKQRKntFmjXFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mNb9w/btqBtFgqnr6/3eqO2neeGuKQRKntFmjXFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mNb9w/btqBtFgqnr6/3eqO2neeGuKQRKntFmjXFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmNb9w%2FbtqBtFgqnr6%2F3eqO2neeGuKQRKntFmjXFK%2Fimg.png&quot; data-origin-height=&quot;173&quot; data-origin-width=&quot;402&quot; data-filename=&quot;query 결과2.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;/&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;h4 data-ke-size=&quot;size20&quot;&gt;6. 마치며&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;165&quot; data-origin-width=&quot;687&quot; data-filename=&quot;Saga 정보.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/st8Ek/btqBvuxvKWc/Wt6NVdYVC1Ef18UWswULm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/st8Ek/btqBvuxvKWc/Wt6NVdYVC1Ef18UWswULm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/st8Ek/btqBvuxvKWc/Wt6NVdYVC1Ef18UWswULm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fst8Ek%2FbtqBvuxvKWc%2FWt6NVdYVC1Ef18UWswULm0%2Fimg.png&quot; data-origin-height=&quot;165&quot; data-origin-width=&quot;687&quot; data-filename=&quot;Saga 정보.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;serialized_sga 내용&lt;/p&gt;
&lt;pre id=&quot;code_1579922896466&quot; class=&quot;html xml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;&amp;lt;com.cqrs.command.saga.TransferManager&amp;gt;
    &amp;lt;comamndFactory&amp;gt;
        &amp;lt;transferCommand class=&quot;com.cqrs.command.transfer.JejuBankTransferCommand&quot;&amp;gt;
        &amp;lt;srcAccountID&amp;gt;test&amp;lt;/srcAccountID&amp;gt;
        &amp;lt;dstAccountID&amp;gt;a31faade-5b57-4435-85da-1de0ed1c55c4&amp;lt;/dstAccountID&amp;gt;
        &amp;lt;amount&amp;gt;30&amp;lt;/amount&amp;gt;
        &amp;lt;transferID&amp;gt;test&amp;lt;/transferID&amp;gt;
        &amp;lt;/transferCommand&amp;gt;
        &amp;lt;/compensationAbortCommand&amp;gt;
    &amp;lt;/comamndFactory&amp;gt;
&amp;lt;/com.cqrs.command.saga.TransferManager&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Jeju, Command 모듈간에 일관성을 위하여 Saga 패턴을 사용했습니다. Command 모듈에서 MoneyTransferEvent Event가 발행되면, Saga 인스턴스가 생성됩니다. 이후 Application의 비즈니스 로직에 따라 모든 트랜잭션이 종료될 때까지 유지됩니다. Saga 인스턴스 정보는 동시에 SAGA_ENTRY 테이블에 적재되며, Saga 인스턴스가 종료되면 해당 데이터 또한 사라집니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;다음 포스팅에서는 &lt;span style=&quot;color: #333333;&quot;&gt;Deadline 기능, &lt;/span&gt;보상 트랜잭션 로직이 적용된 Saga 패턴을 구현하겠습니다.&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>Event sourcing</category>
      <category>MSA</category>
      <category>saga</category>
      <category>saga pattern</category>
      <category>Saga 패턴</category>
      <category>분산 트랜잭션</category>
      <category>이벤트소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/23</guid>
      <comments>https://cla9.tistory.com/23#entry23comment</comments>
      <pubDate>Sat, 25 Jan 2020 12:32:01 +0900</pubDate>
    </item>
    <item>
      <title>18. Saga 패턴을 활용한 트랜잭션 관리 - 1</title>
      <link>https://cla9.tistory.com/22</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 아키텍처를 구성하기 어려운 이유중 가장 큰 문제는 &lt;b&gt;트랜잭션(Transaction)&lt;/b&gt;입니다. 기존 모놀로틱(Monolithic) 환경에서 DBMS가 기본적으로 제공해주는 트랜잭션 기능을 통해 데이터 Commit 혹은 Rollback을 통해 데이터를 일관성 있게 관리했습니다. 하지만 Application 및 DB가 분산되면서, 트랜잭션 처리를 단일 DBMS에서 제공하는 기능을 통해서 달성할 수 없습니다.&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;이번 포스팅에서는 분산 트랜잭션의 종류인 Two-Phase Commit, Saga 패턴 및 Axon에서 제공하는 Saga 기능에 대하여 소개하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Two Phase Commit&lt;/h4&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-filename=&quot;그림1.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;720&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5TOZr/btqBrCqF4ui/SpCN8cJWSHUa21wHIIFFNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5TOZr/btqBrCqF4ui/SpCN8cJWSHUa21wHIIFFNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5TOZr/btqBrCqF4ui/SpCN8cJWSHUa21wHIIFFNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5TOZr%2FbtqBrCqF4ui%2FSpCN8cJWSHUa21wHIIFFNk%2Fimg.png&quot; data-filename=&quot;그림1.png&quot; data-origin-width=&quot;1242&quot; data-origin-height=&quot;720&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;분산 DB 환경에서 쓰는 방법으로 주요 RDBMS에서 기능을 제공합니다. Two-Phase Commit은 말 그대로 2단계에 거쳐서 데이터를 영속화 하는 작업입니다. 위 그림과 같이 여러 DB가 분산 되었을 때, 트랜잭션을 조율하는 조정자(Coordinator)가 존재합니다. 조정자의 역할은 트랜잭션 요청이 들어왔을 때 두 단계를 거쳐 트랜잭션을 진행을 담당합니다. 이때 첫 번째 단계는 &lt;b&gt;Prepare&lt;/b&gt;이며, 이는 쉽게말해 연관된 DB에게 데이터를 저장할 수 있는 상태인지 묻는 과정에 해당합니다.&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-filename=&quot;영속.png&quot; data-origin-width=&quot;1219&quot; data-origin-height=&quot;720&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSolTg/btqBsGTE73r/zbQ4TkSYtegKkkhUVrUTv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSolTg/btqBsGTE73r/zbQ4TkSYtegKkkhUVrUTv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSolTg/btqBsGTE73r/zbQ4TkSYtegKkkhUVrUTv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSolTg%2FbtqBsGTE73r%2FzbQ4TkSYtegKkkhUVrUTv0%2Fimg.png&quot; data-filename=&quot;영속.png&quot; data-origin-width=&quot;1219&quot; data-origin-height=&quot;720&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;메시지를 받은 DB에서는 Commit 작업을 위한 준비를 진행합니다. 이후 데이터를 영속할 수 있는 준비가 완료되면 조정자에게 준비가 완료되었음을 알리고, 반대로 Commit할 수 없다면 불가하다는 메시지를 전달합니다.&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;Commit&lt;/b&gt;을 진행합니다. Commit 단계에서는 조정자가 연관된 DB에게 데이터를 저장하라는 메시지를 송신하며, 수신받은 DB에서는 각자 DB에 데이터를 영속화 합니다.&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-filename=&quot;Rollback.png&quot; data-origin-width=&quot;1219&quot; data-origin-height=&quot;720&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/exd7h6/btqBr748j48/3AQBbQfUhL7UojYbXfcpfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/exd7h6/btqBr748j48/3AQBbQfUhL7UojYbXfcpfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/exd7h6/btqBr748j48/3AQBbQfUhL7UojYbXfcpfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fexd7h6%2FbtqBr748j48%2F3AQBbQfUhL7UojYbXfcpfk%2Fimg.png&quot; data-filename=&quot;Rollback.png&quot; data-origin-width=&quot;1219&quot; data-origin-height=&quot;720&quot; width=&quot;640&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;모든 DB에서 트랜잭션 처리가 완료되면 전체 트랜잭션을 종료합니다.&lt;span style=&quot;color: #333333;&quot;&gt;만약 두 단계를 거치는 과정에서 연관된 DB 중 하나의 DB라도 Commit을 할 수 없는 상황이라면, 모든 DB에게 Rollback을 요구합니다. 트랜잭션을 종료하는 동시에 모든 DB 데이터가 영속화됩니다. 따라서&amp;nbsp;&lt;b&gt;트랜잭션의 범위는 데이터를 처리하는 DB 전체&lt;/b&gt;입니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MSA 환경에서 Two-Phase Commit 문제점&lt;/b&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;Two-Phase Commit은 DBMS 간 분산 트랜잭션을 지원해야 적용가능합니다. 하지만 NoSQL 제품군에는 이를 지원하지 않고, 함께 사용되는 DBMS가 동일 제품군(Oracle, MySQL, Postgres)이여야합니다. 따라서 DBMS polyglot 구성은 어렵습니다.&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;또한 Two-Phase Commit은 보통 하나의 API 엔드포인트를 통해 서비스 요청이 들어오고 내부적으로 DB가 분산되어있을 때 사용됩니다. 하지만 MSA 환경에서는 각기 다른 App에서 API간으로 통신을 통해 서비스 요청이 이루어지기 때문에 구현이 쉽지 않습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Saga 패턴&lt;/h4&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-filename=&quot;saga1.png&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;824&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL3MKk/btqBuiX8Wnl/9S2jkqnfbKN5Gkkt2SV5dK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL3MKk/btqBuiX8Wnl/9S2jkqnfbKN5Gkkt2SV5dK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL3MKk/btqBuiX8Wnl/9S2jkqnfbKN5Gkkt2SV5dK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL3MKk%2FbtqBuiX8Wnl%2F9S2jkqnfbKN5Gkkt2SV5dK%2Fimg.png&quot; data-filename=&quot;saga1.png&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;824&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;Saga 패턴은 트랜잭션의 관리주체가 DBMS가 아닌 Application에 있습니다. App이 분산되어있을 때, 각 App 하위에 존재하는 DB는 Local 트랜잭션 처리만 담당합니다.&amp;nbsp;&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-filename=&quot;saga2.png&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;1019&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FyIyP/btqBr8pnDgR/JFUUCdDaxYi8lwWWRC3El0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FyIyP/btqBr8pnDgR/JFUUCdDaxYi8lwWWRC3El0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FyIyP/btqBr8pnDgR/JFUUCdDaxYi8lwWWRC3El0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFyIyP%2FbtqBr8pnDgR%2FJFUUCdDaxYi8lwWWRC3El0%2Fimg.png&quot; data-filename=&quot;saga2.png&quot; data-origin-width=&quot;1229&quot; data-origin-height=&quot;1019&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;따라서 각각의 App에 대한 연속적인 트랜잭션 요청 및 실패할 경우에 Rollback 처리(보상 트랜잭션)를&amp;nbsp; Application에서 구현해야합니다.&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;Saga 패턴은 위 그림과 같이 연속적인 업데이트 연산으로 이루어져있으며, 전체가 동시에 데이터가 영속화되는 것이아니라 순차적인 단계로 트랜잭션이 이루어집니다. 따라서 Application 비즈니스 로직에서 요구되는 마지막 트랜잭션이 끝났을 때, 데이터가 완전히 영속되었음을 인지하고 이를 종료합니다.&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;Two-Phase Commit과 다르게 Saga를 활용한 트랜잭션은 데이터 격리성(Isolation)을 보장해주지 않습니다. 하지만 Application의 트랜잭션 관리를 통해 최종 일관성(Eventually Consistency)을 달성할 수 있기 때문에 분산되어있는 DB간에 정합성을 맞출 수 있습니다. 또한 트랜잭션 관리를 Application에서 하기 때문에 DBMS를 다른 제품군으로 구성할 수 있는 장점이 있습니다.&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;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Saga 패턴 종류&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;b&gt;1. Choreography-Based Saga&lt;/b&gt;&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-filename=&quot;saga3.png&quot; data-origin-width=&quot;1777&quot; data-origin-height=&quot;609&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w5pQl/btqBtFtm7ax/EtSFfrFCa9cKsXyrZiIk3k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w5pQl/btqBtFtm7ax/EtSFfrFCa9cKsXyrZiIk3k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w5pQl/btqBtFtm7ax/EtSFfrFCa9cKsXyrZiIk3k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw5pQl%2FbtqBtFtm7ax%2FEtSFfrFCa9cKsXyrZiIk3k%2Fimg.png&quot; data-filename=&quot;saga3.png&quot; data-origin-width=&quot;1777&quot; data-origin-height=&quot;609&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;Choreography-&lt;span style=&quot;color: #333333;&quot;&gt;Based&lt;/span&gt; Saga는 자신이 보유한 서비스내 Local 트랜잭션을 관리하며, 트랜잭션이 종료되면 완료 Event를 발행합니다. 만약 그 다음에 수행되어야할 트랜잭션이 있다면, 해당 트랜잭션을 수행해야하는 App에서 완료 Event를 수신받고 다음 작업을 처리합니다. 이때 Event는 Kafka와 같은 메시지 큐를 이용해서 비동기 방식으로 전달할 수 있습니다.&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-filename=&quot;saga 롤백.png&quot; data-origin-width=&quot;1787&quot; data-origin-height=&quot;729&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bigQxE/btqBujXdUwD/ULK7mKyfwdmDujlPJj3gcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bigQxE/btqBujXdUwD/ULK7mKyfwdmDujlPJj3gcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bigQxE/btqBujXdUwD/ULK7mKyfwdmDujlPJj3gcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbigQxE%2FbtqBujXdUwD%2FULK7mKyfwdmDujlPJj3gcK%2Fimg.png&quot; data-filename=&quot;saga 롤백.png&quot; data-origin-width=&quot;1787&quot; data-origin-height=&quot;729&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Choreography-&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Based&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Saga 방식에서는 각 App별로 트랜잭션을 관리하는 로직이 있습니다. 따라서 중간에 트랜잭션이 실패하면, 해당 트랜잭션 취소처리를 실패한 App에서 보상 Event를 발행하여 Rollback 처리를 시도합니다.&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;color: #333333;&quot;&gt;위와 같은 구성은 구축하기 쉬운 장점이 있습니다. 하지만 운영자 입장에서 트랜잭션의 현재 상태를 알기 쉽지 않습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. Orchestration-&lt;span style=&quot;color: #333333;&quot;&gt;Based&lt;/span&gt; Saga&lt;/b&gt;&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-filename=&quot;saga-101.png&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;965&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b97dmZ/btqBs0EBjbO/EekNphZUWmwKQhza29KJp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b97dmZ/btqBs0EBjbO/EekNphZUWmwKQhza29KJp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b97dmZ/btqBs0EBjbO/EekNphZUWmwKQhza29KJp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb97dmZ%2FbtqBs0EBjbO%2FEekNphZUWmwKQhza29KJp1%2Fimg.png&quot; data-filename=&quot;saga-101.png&quot; data-origin-width=&quot;1281&quot; data-origin-height=&quot;965&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;Orchestration-&lt;span style=&quot;color: #333333;&quot;&gt;Based&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Saga는 트랜잭션 처리를 위한 Saga 인스턴스(Manager)가 별도로 존재합니다. 트랜잭션에 관여하는 모든 App은 Manager에 의하여 점진적으로 트랜잭션을 수행하며 결과를 Manager에게 전달합니다. 비즈니스 로직상 마지막 트랜잭션이 끝나면 Manager를 종료하여 전체 트랜잭션 처리를 종료합니다. 만약 중간에 실패하게 되면 Manager에서 보상 트랜잭션을 발동하여 일관성을 유지하도록 합니다.&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-filename=&quot;saga-101롤백.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;1034&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBHcqB/btqBr8iO9TH/NSZPa0sxoVEj57FZt4nBo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBHcqB/btqBr8iO9TH/NSZPa0sxoVEj57FZt4nBo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBHcqB/btqBr8iO9TH/NSZPa0sxoVEj57FZt4nBo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBHcqB%2FbtqBr8iO9TH%2FNSZPa0sxoVEj57FZt4nBo1%2Fimg.png&quot; data-filename=&quot;saga-101롤백.png&quot; data-origin-width=&quot;1354&quot; data-origin-height=&quot;1034&quot; width=&quot;720&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;모든 관리를 Manager가 호출하기 때문에 분산트랜잭션의 중앙 집중화가 이루어집니다. 따라서 서비스간의 복잡성이 줄어들고 구현 및 테스트가 상대적으로 쉽습니다. 또한 트랜잭션의 현재 상태를 Manager가 알고 있기 때문에 롤백을 쉽게할 수 있는 것 또한 장점입니다. 하지만 이를 관리하기 위한 Orchestrator 서비스가 추가되어야 하기 때문에 인프라 구현의 복잡성이 증가되는 단점이 존재합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. Axon Saga 기능 소개&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AxonFramework 에서는 Orchestration 방식의 Saga 패턴을 지원합니다. 즉 트랜잭션을 시작하는 시점에 Saga 인스턴스를 생성하며. Saga 인스턴스에서 트랜잭션을 관리합니다.&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-filename=&quot;사가 생성.png&quot; data-origin-width=&quot;2435&quot; data-origin-height=&quot;1550&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/15iFY/btqBtFGXH1E/gpH8MsSjkYRfb34uAVtJtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/15iFY/btqBtFGXH1E/gpH8MsSjkYRfb34uAVtJtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/15iFY/btqBtFGXH1E/gpH8MsSjkYRfb34uAVtJtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F15iFY%2FbtqBtFGXH1E%2FgpH8MsSjkYRfb34uAVtJtk%2Fimg.png&quot; data-filename=&quot;사가 생성.png&quot; data-origin-width=&quot;2435&quot; data-origin-height=&quot;1550&quot; data-ke-mobilestyle=&quot;widthOrigin&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;Saga 인스턴스는 Event를 처리하는 &lt;a href=&quot;https://docs.axoniq.io/reference-guide/configuring-infrastructure-components/messaging-concepts/unit-of-work&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;UnitOfWork&lt;/u&gt;&lt;/a&gt; 단계에서 생성되며, 전체 트랜잭션 처리가 완료되면 Saga 인스턴스를 종료합니다. Axon에서는 Annotation 기반으로 Saga 인스턴스를 간편하게 설정할 수 있습니다. 또한 DB에 Saga 정보를 저장하고 있어 복구가 가능합니다.&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;추가로 생성된 Saga에서 트랜잭션 요청시, Deadline 지정이 가능합니다. 이로인해 트랜잭션 수행 App으로부터 응답이 없을 경우 보상 트랜잭션을 수행할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6. 마치며&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA를 구성하는 환경에서 Saga를 도입하기전에 비즈니스 로직상 트랜잭션처리가 반드시 필요한지에 대한 충분한 고려가 필요합니다. 이곳 저곳에 적용했다가는 트랜잭션 처리 지옥을 경험할 수 있기 때문입니다. 반드시 필요한 부분에만 일부 도입하는 것이 좋으며, 가장 좋은 상황은 MSA 환경에서 트랜잭션 처리를 하지 않도록 비즈니스 로직을 설계하는 것입니다.&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;다음 시간에는 예제 구현을 통해 Axon에서 제공하는 Saga 기능을 익혀보겠습니다.&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon 프레임워크</category>
      <category>ddd</category>
      <category>MSA</category>
      <category>saga</category>
      <category>saga pattern</category>
      <category>twophase commit</category>
      <category>마이크로서비스아키텍처</category>
      <category>분산 트랜잭션</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/22</guid>
      <comments>https://cla9.tistory.com/22#entry22comment</comments>
      <pubDate>Thu, 23 Jan 2020 22:59:32 +0900</pubDate>
    </item>
    <item>
      <title>17. Query 어플리케이션 구현(Query) - 3</title>
      <link>https://cla9.tistory.com/21</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 Scatter-Gather Query를 구현하겠습니다. Scatter-Gather Query는 동일한 Query를 수행하는 Query Handler가 여러 App에 존재할 경우 모든 App에 Query를 요청하여 결과를 취합받아 최초 Query를 요청한 Application에서 결과를 처리합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;데모 프로젝트에 아래와 같은 요구사항이 추가되었음을 가정하여 Scatter-Gather Query 기능을 구현하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;707&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/clJHgy/btqBmVPpMTs/97zozjpCKr8J7TkM8Q6WZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/clJHgy/btqBmVPpMTs/97zozjpCKr8J7TkM8Q6WZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/clJHgy/btqBmVPpMTs/97zozjpCKr8J7TkM8Q6WZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FclJHgy%2FbtqBmVPpMTs%2F97zozjpCKr8J7TkM8Q6WZ0%2Fimg.png&quot; data-origin-width=&quot;1450&quot; data-origin-height=&quot;707&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Axon Server에 Jeju 은행과 Seoul 은행 외부시스템이 연결되어있다고 가정해봅시다. 이때 소유주(HolderID)가 보유한 잔고에 대하여 각 은행에게 대출한도를 Query하면 은행별로 전달받은 답변을 Client 화면에 표시하는 요구사항을 코드를 통해 알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Jeju 은행 모듈 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Jeju 은행과, Seoul 은행에 Query를 요청하려면, Query 클래스 정보를 공유해야하므로, 공통 모듈(Common)에 Query 클래스를 생성해야합니다. 먼저 Common 모듈에 query &amp;gt; loan 패키지를 생성합니다. 이후 Query 및 결과를 저장할 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;공통Query.png&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;471&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/co26Dk/btqBiBSQp5Q/KTMhCBAlpB3MqCXTEygCZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/co26Dk/btqBiBSQp5Q/KTMhCBAlpB3MqCXTEygCZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/co26Dk/btqBiBSQp5Q/KTMhCBAlpB3MqCXTEygCZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fco26Dk%2FbtqBiBSQp5Q%2FKTMhCBAlpB3MqCXTEygCZk%2Fimg.png&quot; data-filename=&quot;공통Query.png&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;471&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. 생성한 두 클래스를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;LoanLimitQuery.java&lt;/p&gt;
&lt;pre id=&quot;code_1579439758405&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@ToString
@Getter
public class LoanLimitQuery {
    private String holderID;
    private Long balance;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;LoanLimitResult.java&lt;/p&gt;
&lt;pre id=&quot;code_1579439773485&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@ToString
@Getter
@Builder
public class LoanLimitResult {
    private String holderID;
    private String bankName;
    private Long balance;
    private Long loanLimit;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. 새로운 은행 모듈 생성을 위하여 프로젝트 root 디렉토리에 위치한 settings.gradle 파일을 연다음 모듈 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;settings.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1579439884962&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;rootProject.name = 'demo'
include 'command'
include 'query'
include 'common'
include 'seoulBank'
include 'jejuBank'&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;4. 추가된 두 묘듈에서 공통 모듈을 사용하기 위해 빌드 설정을 추가해야 합니다. 프로젝트 root 디렉토리에 위치한 build.gradle 파일을 연다음 빌드 스크립트 내용을 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1579439967969&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;(...중략...)
project(':jejuBank') {
    dependencies {
        compile project(':common')
    }
}

project(':seoulBank') {
    dependencies {
        compile project(':common')
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;이후 gradle build를 수행하면, jejuBank, seoulBank 모듈이 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;5.&amp;nbsp; jejuBank 프로젝트 하위에 build.gradle 파일을 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;jeju build.png&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;278&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DoRSN/btqBkNEyGbP/lNOKTByrcKdU1akIrbtshK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DoRSN/btqBkNEyGbP/lNOKTByrcKdU1akIrbtshK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DoRSN/btqBkNEyGbP/lNOKTByrcKdU1akIrbtshK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDoRSN%2FbtqBkNEyGbP%2FlNOKTByrcKdU1akIrbtshK%2Fimg.png&quot; data-filename=&quot;jeju build.png&quot; data-origin-width=&quot;434&quot; data-origin-height=&quot;278&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;6. build.gradle 파일에 의존성을 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1579440292488&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ext{
    axonVersion = &quot;4.2.1&quot;
}
dependencies{
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation group: 'org.axonframework', name: 'axon-spring-boot-starter', version: &quot;$axonVersion&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;7. resource 패키지 하위에 application.yml 파일을 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;jeju application.png&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qiIhM/btqBjA0sZQv/eEnrjedQkQ8Iuo6mkFW71K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qiIhM/btqBjA0sZQv/eEnrjedQkQ8Iuo6mkFW71K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qiIhM/btqBjA0sZQv/eEnrjedQkQ8Iuo6mkFW71K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqiIhM%2FbtqBjA0sZQv%2FeEnrjedQkQ8Iuo6mkFW71K%2Fimg.png&quot; data-filename=&quot;jeju application.png&quot; data-origin-width=&quot;448&quot; data-origin-height=&quot;362&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;8. application.yml 파일에 설정 값을 기술합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1579440463064&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: 9091

spring:
  application:
    name: eventsourcing-cqrs-jejuBank

axon:
  serializer:
    general: xstream
  axonserver:
    servers: localhost:8124&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;9. 패키지 구조 설정 한다음 Component 패키지와 Main 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;패키지구조.png&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8j2Pj/btqBjULcout/SNPJ0XTMC0rHfOpKLRHbq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8j2Pj/btqBjULcout/SNPJ0XTMC0rHfOpKLRHbq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8j2Pj/btqBjULcout/SNPJ0XTMC0rHfOpKLRHbq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8j2Pj%2FbtqBjULcout%2FSNPJ0XTMC0rHfOpKLRHbq0%2Fimg.png&quot; data-filename=&quot;패키지구조.png&quot; data-origin-width=&quot;464&quot; data-origin-height=&quot;467&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;9. Main 클래스 내용을 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;jejuBankApp.java&lt;/p&gt;
&lt;pre id=&quot;code_1579440590826&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
public class JejuBankApp {
    public static void main(String[] args) {
        SpringApplication.run(JejuBankApp.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;10. component 패키지내에 Query를 처리할 Component 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;jejuComponent.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yeGlj/btqBiCdeIpD/hpqikS0B93E4L7zlQmtsg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yeGlj/btqBiCdeIpD/hpqikS0B93E4L7zlQmtsg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yeGlj/btqBiCdeIpD/hpqikS0B93E4L7zlQmtsg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyeGlj%2FbtqBiCdeIpD%2FhpqikS0B93E4L7zlQmtsg1%2Fimg.png&quot; data-filename=&quot;jejuComponent.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;472&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;11. Component 클래스 내용을 구현합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountLoanComponent.java&lt;/p&gt;
&lt;pre id=&quot;code_1579440929361&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Slf4j
public class AccountLoanComponent {

    @QueryHandler
    private LoanLimitResult on(LoanLimitQuery query) {
        log.debug(&quot;handling {}&quot;,query);
        return LoanLimitResult.builder()
                .holderID(query.getHolderID())
                .balance(query.getBalance())
                .bankName(&quot;JejuBank&quot;)
                .loanLimit(Double.valueOf(query.getBalance() * 1.2).longValue())
                .build();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드에서 jeju 은행의 대출한도는 일괄적으로 보유 잔고의 120%만 가능하도록 가정하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Seoul 은행 모듈 구현&lt;/h4&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;1. seoulBank 프로젝트 하위에 build.gradle 파일을 생성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;seoul build.png&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;336&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sbTxc/btqBikKBese/aRZi75Yo74rI7zCYnZGvJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sbTxc/btqBikKBese/aRZi75Yo74rI7zCYnZGvJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sbTxc/btqBikKBese/aRZi75Yo74rI7zCYnZGvJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsbTxc%2FbtqBikKBese%2FaRZi75Yo74rI7zCYnZGvJK%2Fimg.png&quot; data-filename=&quot;seoul build.png&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;336&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. build.gradle 파일에 의존성을 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1579441436649&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ext{
    axonVersion = &quot;4.2.1&quot;
}
dependencies{
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation group: 'org.axonframework', name: 'axon-spring-boot-starter', version: &quot;$axonVersion&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. &lt;span style=&quot;color: #333333;&quot;&gt;resource 패키지 하위에 application.yml 파일을 생성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;seoul application.png&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcaVfS/btqBik4SBSI/mfxDU2K1EDlIuH71hOCUnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcaVfS/btqBik4SBSI/mfxDU2K1EDlIuH71hOCUnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcaVfS/btqBik4SBSI/mfxDU2K1EDlIuH71hOCUnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcaVfS%2FbtqBik4SBSI%2FmfxDU2K1EDlIuH71hOCUnk%2Fimg.png&quot; data-filename=&quot;seoul application.png&quot; data-origin-width=&quot;449&quot; data-origin-height=&quot;420&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4. application.yml 파일에 설정 값을 기술합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1579441523551&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: 9092

spring:
  application:
    name: eventsourcing-cqrs-seoulBank

axon:
  serializer:
    general: xstream
  axonserver:
    servers: localhost:8124&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;5. 패키지 구조 설정 한다음 Component 패키지와 Main 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;seoul 패키지구조.png&quot; data-origin-width=&quot;453&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oDQqv/btqBjCcYuVR/AyBBcU7ceZyObmJrKSoswk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oDQqv/btqBjCcYuVR/AyBBcU7ceZyObmJrKSoswk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oDQqv/btqBjCcYuVR/AyBBcU7ceZyObmJrKSoswk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoDQqv%2FbtqBjCcYuVR%2FAyBBcU7ceZyObmJrKSoswk%2Fimg.png&quot; data-filename=&quot;seoul 패키지구조.png&quot; data-origin-width=&quot;453&quot; data-origin-height=&quot;504&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;6. Main 클래스 내용을 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;SeoulBankApp.java&lt;/p&gt;
&lt;pre id=&quot;code_1579441912293&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
public class SeoulBankApp {
    public static void main(String[] args) {
        SpringApplication.run(SeoulBankApp.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;7. &lt;span style=&quot;color: #333333;&quot;&gt;component 패키지내에 Query를 처리할 Component 클래스를 생성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;seoul Component.png&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;529&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bvKiWp/btqBjBSFZm9/N3V2BUvkBx3ApGkTUF0FE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bvKiWp/btqBjBSFZm9/N3V2BUvkBx3ApGkTUF0FE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bvKiWp/btqBjBSFZm9/N3V2BUvkBx3ApGkTUF0FE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvKiWp%2FbtqBjBSFZm9%2FN3V2BUvkBx3ApGkTUF0FE1%2Fimg.png&quot; data-filename=&quot;seoul Component.png&quot; data-origin-width=&quot;452&quot; data-origin-height=&quot;529&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;8. &lt;span&gt;&amp;nbsp;&lt;/span&gt;Component 클래스 내용을 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountLoanComponent.java&lt;/p&gt;
&lt;pre id=&quot;code_1579441996274&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@Slf4j
public class AccountLoanComponent {
    @QueryHandler
    private LoanLimitResult on(LoanLimitQuery query) {
        log.debug(&quot;handling {}&quot;,query);
        return LoanLimitResult.builder()
                .holderID(query.getHolderID())
                .balance(query.getBalance())
                .bankName(&quot;SeoulBank&quot;)
                .loanLimit(Double.valueOf(query.getBalance() * 1.5).longValue())
                .build();
    }
}&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;h4 data-ke-size=&quot;size20&quot;&gt;4. Query 인터페이스 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. 화면 생성을 위해 Query 모듈 resources &amp;gt; templates 패키지내에 scatter-gather.html 파일을 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;화면.png&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nDefz/btqBikqgk8Z/dbW8nltUPEmPHLldfII5y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nDefz/btqBikqgk8Z/dbW8nltUPEmPHLldfII5y0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nDefz/btqBikqgk8Z/dbW8nltUPEmPHLldfII5y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnDefz%2FbtqBikqgk8Z%2FdbW8nltUPEmPHLldfII5y0%2Fimg.png&quot; data-filename=&quot;화면.png&quot; data-origin-width=&quot;443&quot; data-origin-height=&quot;506&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. 화면 코드를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;scatter-gather.html&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1579442483037&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Scatter-Gather Query Example&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;script&amp;gt;
    window.addEventListener(&quot;DOMContentLoaded&quot;, function (){
        (function () {
            let appendDiv = document.getElementById(&quot;layout&quot;);
            let text = document.getElementById(&quot;holderInput&quot;);
            let pElem = document.createElement(&quot;p&quot;);
            document.getElementById(&quot;wrapper&quot;).addEventListener(&quot;click&quot;, append);

            function append(e) {
                let target = e.target;
                let callbackFunction = callback[target.getAttribute(&quot;data-cb&quot;)];
                callbackFunction();
            }

            let callback = {
                &quot;search&quot;: (function () {
                    let holderId = text.value;
                    if (holderId === undefined || holderId === null || holderId ===&quot;&quot;) {
                        alert(&quot;소유주를 입력하시오.&quot;);
                    } else {
                        let xhr = new XMLHttpRequest();
                        xhr.open('GET','http://localhost:9090/account/info/scatter/gather/'+holderId, true);
                        xhr.send();
                        xhr.onload = function(){
                            if(xhr.status === 200){
                                let elem = pElem.cloneNode();
                                elem.innerText = xhr.responseText;
                                appendDiv.appendChild(elem);
                            }
                        }
                    }
                })
            }
        }());
    });
&amp;lt;/script&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div id=&quot;wrapper&quot;&amp;gt;
    &amp;lt;input type=&quot;button&quot; data-cb=&quot;search&quot; value=&quot;조회&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;input type=&quot;text&quot; id=&quot;holderInput&quot; placeholder=&quot;소유주 ID를 입력하시오.&quot;&amp;gt;
&amp;lt;div id=&quot;layout&quot;/&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. Scatter-Query를 요청할 서비스를 구현하기 위하여 먼저 메소드를 정의해야합니다. Query 모듈 service 패키지에 위치한 QueryService 클래스를 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;서비스인터페이스.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/863DL/btqBjVQTX9S/MVHBhDPXPOdDQwuLAFfqPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/863DL/btqBjVQTX9S/MVHBhDPXPOdDQwuLAFfqPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/863DL/btqBjVQTX9S/MVHBhDPXPOdDQwuLAFfqPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F863DL%2FbtqBjVQTX9S%2FMVHBhDPXPOdDQwuLAFfqPK%2Fimg.png&quot; data-filename=&quot;서비스인터페이스.png&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;500&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;4. 인터페이스에 추상 메소드를 정의합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;QueryService.java&lt;/p&gt;
&lt;pre id=&quot;code_1579442633607&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface QueryService {
    (...중략...)
    List&amp;lt;LoanLimitResult&amp;gt; getAccountInfoScatterGather(String holderId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;5. QueryServiceImpl 클래스를 열어 추가된 추상 메소드를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;QueryServiceImpl.java&lt;/p&gt;
&lt;pre id=&quot;code_1579442720386&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Slf4j
@Service
public class QueryServiceImpl implements QueryService {
    (...중략...)
    private final AccountRepository repository;
    (...중략...)
    @Override
    public List&amp;lt;LoanLimitResult&amp;gt; getAccountInfoScatterGather(String holderId) {
        HolderAccountSummary accountSummary = repository.findByHolderId(holderId).orElseThrow();

        return queryGateway.scatterGather(new LoanLimitQuery(accountSummary.getHolderId(), accountSummary.getTotalBalance()),
                ResponseTypes.instanceOf(LoanLimitResult.class),
                30, TimeUnit.SECONDS)
                .collect(Collectors.toList());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Scatter-Gather 쿼리는 단일 App에 요청하는 것이 아니므로, 만약 Handler 처리 App에 장애가 발생한다면 무한정 대기할 수 있습니다. 따라서 요청시, DeadLine을 정하여 요청시간 만큼만 대기할 수 있도록 지정이 필요합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;6. API End Point 지정 및 화면 호출을 위하여 Controller 클래스 수정이 필요합니다. Query 모듈내 Controller 패키지안에 있는 두개의 Controller 클래스에 관련 메소드를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;컨트롤러.png&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;499&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sg75V/btqBkpxbzpF/B7nnh7OqHCzeoLJRwh2UuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sg75V/btqBkpxbzpF/B7nnh7OqHCzeoLJRwh2UuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sg75V/btqBkpxbzpF/B7nnh7OqHCzeoLJRwh2UuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsg75V%2FbtqBkpxbzpF%2FB7nnh7OqHCzeoLJRwh2UuK%2Fimg.png&quot; data-filename=&quot;컨트롤러.png&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;499&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAccountController.java&lt;/p&gt;
&lt;pre id=&quot;code_1579442928835&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class HolderAccountController {
    (...중략...)

    @GetMapping(&quot;account/info/scatter/gather/{id}&quot;)
    public ResponseEntity&amp;lt;List&amp;lt;LoanLimitResult&amp;gt;&amp;gt; getAccountInfoScatterGather(@PathVariable(value = &quot;id&quot;) @NonNull @NotBlank String holderId){
        return ResponseEntity.ok()
                .body(queryService.getAccountInfoScatterGather(holderId));
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;WebController.java&lt;/p&gt;
&lt;pre id=&quot;code_1579442949854&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class WebController {
	(...중략...)
    @GetMapping(&quot;/scatter-gather&quot;)
    public void scatterGatherQueryView(){}
}
&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;h4 data-ke-size=&quot;size20&quot;&gt;5. 테스트&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. jeju, seoul 은행 App과 Query App을 기동합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. 웹브라우저(Chrome)에서 http://localhost:9090/scatter-gather URL 입력합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;웹화면.png&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wsWjd/btqBkqXaC85/zwCzCPWzKEEudlxTYqlaH1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wsWjd/btqBkqXaC85/zwCzCPWzKEEudlxTYqlaH1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wsWjd/btqBkqXaC85/zwCzCPWzKEEudlxTYqlaH1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwsWjd%2FbtqBkqXaC85%2FzwCzCPWzKEEudlxTYqlaH1%2Fimg.png&quot; data-filename=&quot;웹화면.png&quot; data-origin-width=&quot;460&quot; data-origin-height=&quot;177&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;3. 임의의 소유주 ID를 입력후 조회 버튼을 눌러 결과를 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;결과화면.png&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T40k3/btqBkpcTOxk/z5l56lkCKK35dJJlAjWkBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T40k3/btqBkpcTOxk/z5l56lkCKK35dJJlAjWkBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T40k3/btqBkpcTOxk/z5l56lkCKK35dJJlAjWkBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT40k3%2FbtqBkpcTOxk%2Fz5l56lkCKK35dJJlAjWkBK%2Fimg.png&quot; data-filename=&quot;결과화면.png&quot; data-origin-width=&quot;1420&quot; data-origin-height=&quot;274&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;h4 data-ke-size=&quot;size20&quot;&gt;6. 마치며&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅을 끝으로 EventSourcing에 필요한 기본적인 Command, Event 처리 및 Query 요청에 대한 필수 기능 구현을 완료했습니다. 각 기능별로 세부적인 기능은 Axon 공식 홈페이지에서 제공하는 &lt;a href=&quot;https://docs.axoniq.io/reference-guide/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Document&lt;/u&gt;&lt;/a&gt;나 &lt;a href=&quot;https://groups.google.com/forum/#!forum/axonframework&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Google Groups&lt;/u&gt;&lt;/a&gt;를 이용하여 검색하시면 많은 자료를 구할 수 있으니 참고 바랍니다.&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>Axon Server</category>
      <category>Axon 프레임워크</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>EventSourcing</category>
      <category>MSA</category>
      <category>마이크로서비스아키텍처</category>
      <category>이벤트소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/21</guid>
      <comments>https://cla9.tistory.com/21#entry21comment</comments>
      <pubDate>Mon, 20 Jan 2020 00:30:05 +0900</pubDate>
    </item>
    <item>
      <title>16. Query 어플리케이션 구현(Query) - 2</title>
      <link>https://cla9.tistory.com/20</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p style=&quot;position: absolute;&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 시간에는 Query 기능 중 Point to Point, Subscription 기능을 구현합니다. 또한, Query 결과를 보기 위하여 Client 화면을 간략하게 만들겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Client 화면은 크게 Point to Point Query와 Subsciprtion Query를 조회하는 화면 2개를 분할하였으며, 화면 호출 URL은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Point to Point : http://localhost:9090/p2p&lt;/p&gt;
&lt;p&gt;Subscription : http://localhost:9090/subscription&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;화면.png&quot; data-origin-width=&quot;785&quot; data-origin-height=&quot;223&quot; width=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Tn17U/btqBjAMRWvM/GaM1KTIZ1XXGVz2aKmuvok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Tn17U/btqBjAMRWvM/GaM1KTIZ1XXGVz2aKmuvok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Tn17U/btqBjAMRWvM/GaM1KTIZ1XXGVz2aKmuvok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTn17U%2FbtqBjAMRWvM%2FGaM1KTIZ1XXGVz2aKmuvok%2Fimg.png&quot; data-filename=&quot;화면.png&quot; data-origin-width=&quot;785&quot; data-origin-height=&quot;223&quot; width=&quot;620&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Subscription 에서는 조회를 누르면 Server와의 Connection이 설정되므로 이를 해제하기 위한 종료 버튼을 추가하였습니다. 조회 버튼을 누르게되면, Server API를 호출합니다. 두 API 주소는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Point to Point : http://localhost:9090/account/info/{id}&lt;/p&gt;
&lt;p&gt;Subscription : http://localhost:9090/account/info/subcription/{id}&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이제 본격적으로 기능 구현을 진행하겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Point to Point Query&lt;/h4&gt;
&lt;p&gt;Query를 처리하는 Handler가 하나만 존재하고, 한번만 질의만 하면되는 상황이라면 Point to Point Query가 적합합니다.&lt;/p&gt;
&lt;p&gt;해당 기능 구현을 통해 사용방법을 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Query 모듈&lt;b&gt; build.gradle&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;gradle.png&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bICORl/btqBjAMLL3g/sEYNmpWJJwWQqnpAvUvVP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bICORl/btqBjAMLL3g/sEYNmpWJJwWQqnpAvUvVP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bICORl/btqBjAMLL3g/sEYNmpWJJwWQqnpAvUvVP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbICORl%2FbtqBjAMLL3g%2FsEYNmpWJJwWQqnpAvUvVP0%2Fimg.png&quot; data-filename=&quot;gradle.png&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;564&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. 화면 구현을 위하여 &lt;span&gt;thymeleaf 의존성을 추가합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;build.gradle&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1579408595483&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies{
    (...중략...)
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. 화면 호출을 위하여 Query 모듈 &lt;b&gt;Controller&lt;/b&gt; 패키지에 &lt;b&gt;WebController&lt;/b&gt; 클래스를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;webController.png&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwBWRn/btqBkqijKuN/8AL6aGvXcJDzuNAZOK9xz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwBWRn/btqBkqijKuN/8AL6aGvXcJDzuNAZOK9xz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwBWRn/btqBkqijKuN/8AL6aGvXcJDzuNAZOK9xz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwBWRn%2FbtqBkqijKuN%2F8AL6aGvXcJDzuNAZOK9xz0%2Fimg.png&quot; data-filename=&quot;webController.png&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;652&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;4. 클래스 내용을 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;WebController.java&lt;/p&gt;
&lt;pre id=&quot;code_1579408727470&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class WebController {
    @GetMapping(&quot;/p2p&quot;)
    public void pointToPointQueryView(){}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Controller에서 &lt;/span&gt;http://localhost:&lt;span&gt;9090/p2p URL 호출 시 &lt;b&gt;p2p.html&lt;/b&gt; 파일을 전달하도록 지정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;5. 화면 구현을 위해서 Query 모듈 resources 패키지 하위에 &lt;b&gt;templates&lt;/b&gt; 패키지 및 &lt;b&gt;p2p.html&lt;/b&gt; 파일을 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;p2p.png&quot; data-origin-width=&quot;387&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vDRxv/btqBliYqxrT/TtKl35YkV85sTIWOXYbblk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vDRxv/btqBliYqxrT/TtKl35YkV85sTIWOXYbblk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vDRxv/btqBliYqxrT/TtKl35YkV85sTIWOXYbblk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvDRxv%2FbtqBliYqxrT%2FTtKl35YkV85sTIWOXYbblk%2Fimg.png&quot; data-filename=&quot;p2p.png&quot; data-origin-width=&quot;387&quot; data-origin-height=&quot;547&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;6. html 내용을을 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;p2p.html&lt;/p&gt;
&lt;pre id=&quot;code_1579409000051&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;PointToPoint Query Example&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;script&amp;gt;
    window.addEventListener(&quot;DOMContentLoaded&quot;, function (){
        (function () {
            let appendDiv = document.getElementById(&quot;layout&quot;);
            let text = document.getElementById(&quot;holderInput&quot;);
            let pElem = document.createElement(&quot;p&quot;);
            document.getElementById(&quot;wrapper&quot;).addEventListener(&quot;click&quot;, append);

            function append(e) {
                let target = e.target;
                let callbackFunction = callback[target.getAttribute(&quot;data-cb&quot;)];
                callbackFunction();
            }

            let callback = {
                &quot;search&quot;: (function () {
                    let holderId = text.value;
                    if (holderId === undefined || holderId === null || holderId ===&quot;&quot;) {
                        alert(&quot;소유주를 입력하시오.&quot;);
                    } else {
                        let xhr = new XMLHttpRequest();
                        xhr.open('GET','http://localhost:9090/account/info/'+holderId, true);
                        xhr.send();
                        xhr.onload = function(){
                            if(xhr.status === 200){
                                let elem = pElem.cloneNode();
                                elem.innerText = xhr.responseText;
                                appendDiv.appendChild(elem);
                            }
                        }
                    }
                })
            }
        }());
    });
&amp;lt;/script&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div id=&quot;wrapper&quot;&amp;gt;
    &amp;lt;input type=&quot;button&quot; data-cb=&quot;search&quot; value=&quot;조회&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;input type=&quot;text&quot; id=&quot;holderInput&quot; placeholder=&quot;소유주 ID를 입력하시오.&quot;&amp;gt;
&amp;lt;div id=&quot;layout&quot;/&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드 내용 중 가장 핵심이 되는 로직은 &lt;b&gt;callback&lt;/b&gt; 객체입니다. 구현 내용은 비동기로 Query를 수행하는 API에 소유주 정보를 인자로 요청하면, 해당 내용을 수신받아 화면에 표시합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;7. 화면이 정상적으로 출력되는지 확인하기 위하여, Query App을 기동합니다. 이후 웹브라우저(Chrome)을 열고 화면 호출 테스트를 수행합니다.(http://localhost:9090/p2p)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;257&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/db1Scg/btqBh0L6PWU/IzAruUfNipP7SNatz4GeK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/db1Scg/btqBh0L6PWU/IzAruUfNipP7SNatz4GeK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/db1Scg/btqBh0L6PWU/IzAruUfNipP7SNatz4GeK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdb1Scg%2FbtqBh0L6PWU%2FIzAruUfNipP7SNatz4GeK1%2Fimg.png&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;257&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;8. 테스트가 완료되었으면, Query를 수행할 API 내용을 구현하겠습니다. 먼저 Query 모듈 &lt;b&gt;service&lt;/b&gt; 패키지내 &lt;b&gt;QueryService&lt;/b&gt; 인터페이스를 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;queryService.png&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CkNmt/btqBjBLDIRQ/LQvNCEgor1TzGKDUAyXrd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CkNmt/btqBjBLDIRQ/LQvNCEgor1TzGKDUAyXrd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CkNmt/btqBjBLDIRQ/LQvNCEgor1TzGKDUAyXrd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCkNmt%2FbtqBjBLDIRQ%2FLQvNCEgor1TzGKDUAyXrd1%2Fimg.png&quot; data-filename=&quot;queryService.png&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;456&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;9. Query 수행을 위한 메소드를 정의합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;QueryService.java&lt;/p&gt;
&lt;pre id=&quot;code_1579409918154&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface QueryService {
    void reset();
    HolderAccountSummary getAccountInfo(String holderId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;10. Service 구현을 위하여 &lt;span style=&quot;color: #333333;&quot;&gt;Query 모듈 &lt;b&gt;service&lt;/b&gt; 패키지내 &lt;b&gt;QueryServiceImpl&lt;/b&gt; 클래스를 엽니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;queryServiceImpl.png&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGP5nQ/btqBh15mBK8/0acXPirAvdBBWiM2vYZvuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGP5nQ/btqBh15mBK8/0acXPirAvdBBWiM2vYZvuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGP5nQ/btqBh15mBK8/0acXPirAvdBBWiM2vYZvuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGP5nQ%2FbtqBh15mBK8%2F0acXPirAvdBBWiM2vYZvuk%2Fimg.png&quot; data-filename=&quot;queryServiceImpl.png&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;504&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;11. Interface에 정의된 메소드를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;QueryServiceImpl.java&lt;/p&gt;
&lt;pre id=&quot;code_1579410091689&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Slf4j
@Service
public class QueryServiceImpl implements QueryService {
	(...중략...)
    @Override
    public HolderAccountSummary getAccountInfo(String holderId) {
        AccountQuery accountQuery = new AccountQuery(holderId);
        log.debug(&quot;handling {}&quot;, accountQuery);
        return queryGateway.query(accountQuery, ResponseTypes.instanceOf(HolderAccountSummary.class)).join();
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;12. API End Point 설정을 위해 &lt;b&gt;controller&lt;/b&gt; 패키지내 위치한 &lt;b&gt;HolderAccountController&lt;/b&gt; 클래스를 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;holerController.png&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mUAF1/btqBkpqcJ6X/jsFQSeTvC89wwkMr8DhNsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mUAF1/btqBkpqcJ6X/jsFQSeTvC89wwkMr8DhNsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mUAF1/btqBkpqcJ6X/jsFQSeTvC89wwkMr8DhNsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmUAF1%2FbtqBkpqcJ6X%2FjsFQSeTvC89wwkMr8DhNsk%2Fimg.png&quot; data-filename=&quot;holerController.png&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;445&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;13. API End Point를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAccountController.java&lt;/p&gt;
&lt;pre id=&quot;code_1579410352950&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class HolderAccountController {
    private final QueryService queryService;

	(...중략...)
    @GetMapping(&quot;/account/info/{id}&quot;)
    public ResponseEntity&amp;lt;HolderAccountSummary&amp;gt; getAccountInfo(@PathVariable(value = &quot;id&quot;) @NonNull @NotBlank String holderId){
        return ResponseEntity.ok()
                             .body(queryService.getAccountInfo(holderId));
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;14. QueryGateway로 전달된 Query를 처리하는 Handler 작성을 위해 Query 모듈&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;projection&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;패키지 하위&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;HolderAccountProjection&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클래스를 엽니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfaLdh/btqBjUxyjTp/vZsvRrycKnxd6HQFRxvwe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfaLdh/btqBjUxyjTp/vZsvRrycKnxd6HQFRxvwe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfaLdh/btqBjUxyjTp/vZsvRrycKnxd6HQFRxvwe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfaLdh%2FbtqBjUxyjTp%2FvZsvRrycKnxd6HQFRxvwe1%2Fimg.png&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;389&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;15. HolderAccountProjection 클래스에서 &lt;b&gt;QueryHandler&lt;/b&gt; 메소드를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAccountProjection.java&lt;/p&gt;
&lt;pre id=&quot;code_1579429729792&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@EnableRetry
@AllArgsConstructor
@Slf4j
@ProcessingGroup(&quot;accounts&quot;)
public class HolderAccountProjection {
    (...중략...)

    @QueryHandler
    public HolderAccountSummary on(AccountQuery query){
        log.debug(&quot;handling {}&quot;, query);
        return repository.findByHolderId(query.getHolderId()).orElse(null);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;16. Query App을 기동합니다. 이후 EventStore에 저장된 HolderID 중 하나를 선택하여 입력창에 기입합니다. 조회 버튼을 눌러 정상적으로 조회되는지 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;p2p 조회결과.png&quot; data-origin-width=&quot;1173&quot; data-origin-height=&quot;197&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uBigi/btqBiDpybWQ/zrMsKRI8QPBDNo2idXnaHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uBigi/btqBiDpybWQ/zrMsKRI8QPBDNo2idXnaHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uBigi/btqBiDpybWQ/zrMsKRI8QPBDNo2idXnaHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuBigi%2FbtqBiDpybWQ%2FzrMsKRI8QPBDNo2idXnaHk%2Fimg.png&quot; data-filename=&quot;p2p 조회결과.png&quot; data-origin-width=&quot;1173&quot; data-origin-height=&quot;197&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;테스트 결과, Read Model에 저장된 데이터가 정상적으로 출력되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Subscription Query&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Subscription.png&quot; data-origin-width=&quot;1493&quot; data-origin-height=&quot;1099&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/svV1U/btqBmVhuskC/3zZgfZpjKhx5hD6rEBezXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/svV1U/btqBmVhuskC/3zZgfZpjKhx5hD6rEBezXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/svV1U/btqBmVhuskC/3zZgfZpjKhx5hD6rEBezXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsvV1U%2FbtqBmVhuskC%2F3zZgfZpjKhx5hD6rEBezXk%2Fimg.png&quot; data-filename=&quot;Subscription.png&quot; data-origin-width=&quot;1493&quot; data-origin-height=&quot;1099&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Subscription Query는 Client로부터 Connection을 연결하면, 이를 해제하지 않고 유지합니다. Query를 처리하는 Hanlder App에서는 초기 결과를 최초에 반환합니다. 이때 Flux 타입으로 반환하며, QueryUpdateEmitter를 통해서 Read Model의 변경이 있을 때마다 수신 받습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;데모 프로젝트에서는 &lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;SSE&lt;/span&gt;&lt;/b&gt;(Server Sent Event) 방식으로 구현하기 위해 Client 화면에서는 &lt;b&gt;EventSource&lt;/b&gt; 객체를 사용하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;1. Query 모듈 &lt;b&gt;build.gradle&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;564&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bICORl/btqBjAMLL3g/sEYNmpWJJwWQqnpAvUvVP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bICORl/btqBjAMLL3g/sEYNmpWJJwWQqnpAvUvVP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bICORl/btqBjAMLL3g/sEYNmpWJJwWQqnpAvUvVP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbICORl%2FbtqBjAMLL3g%2FsEYNmpWJJwWQqnpAvUvVP0%2Fimg.png&quot; data-origin-width=&quot;403&quot; data-origin-height=&quot;564&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr style=&quot;margin: 20px auto 0px; border: none; cursor: pointer !important; z-index: 1; font-size: 0px; line-height: 0; background: url('../image/divider-line.svg') center 0px / 200px 420px no-repeat; width: 64px; height: 4px; padding: 20px;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;2. &lt;b&gt;Flux&lt;/b&gt; 사용을 위하여 &lt;b&gt;reactor-core&lt;/b&gt; 의존성을 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1579427823799&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies{
    (...중략...)
    implementation group: 'io.projectreactor', name: 'reactor-core'
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. 화면 호출을 위하여 Query 모듈&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Controller&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;패키지에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;WebController&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;클래스를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dwBWRn/btqBkqijKuN/8AL6aGvXcJDzuNAZOK9xz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dwBWRn/btqBkqijKuN/8AL6aGvXcJDzuNAZOK9xz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dwBWRn/btqBkqijKuN/8AL6aGvXcJDzuNAZOK9xz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdwBWRn%2FbtqBkqijKuN%2F8AL6aGvXcJDzuNAZOK9xz0%2Fimg.png&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;652&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr style=&quot;margin: 20px auto 0px; border: none; cursor: pointer !important; z-index: 1; font-size: 0px; line-height: 0; background: url('../image/divider-line.svg') center 0px / 200px 420px no-repeat; width: 64px; height: 4px; padding: 20px; color: #333333; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;4. 클래스 내용을 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;WebController.java&lt;/p&gt;
&lt;pre id=&quot;code_1579427898298&quot; class=&quot;java&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Controller
public class WebController {
   (...중략...)
    @GetMapping(&quot;/subscription&quot;)
    public void subscriptionQueryView(){}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Controller에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;http://localhost:&lt;span&gt;9090/subscription URL 호출 시 &lt;b&gt;subscription.html&lt;/b&gt; 파일을 전달하도록 지정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;5. 화면 구현을 위해서 Query 모듈 resources 패키지 하위에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;templates&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;패키지 및&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;subscription.html&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일을 생성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;subscription.png&quot; data-origin-width=&quot;385&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xL8HH/btqBil3EVnT/B8DdQ49flN4SXLOacaORuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xL8HH/btqBil3EVnT/B8DdQ49flN4SXLOacaORuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xL8HH/btqBil3EVnT/B8DdQ49flN4SXLOacaORuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxL8HH%2FbtqBil3EVnT%2FB8DdQ49flN4SXLOacaORuK%2Fimg.png&quot; data-filename=&quot;subscription.png&quot; data-origin-width=&quot;385&quot; data-origin-height=&quot;698&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;6. html 내용을을 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;subscription.html&lt;/p&gt;
&lt;pre id=&quot;code_1579428428490&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;title&amp;gt;Subscription Query Example&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;script&amp;gt;
    window.addEventListener(&quot;DOMContentLoaded&quot;, function (){
        (function () {
            let appendDiv = document.getElementById(&quot;layout&quot;);
            let text = document.getElementById(&quot;holderInput&quot;);
            let eventSource = undefined;
            let pElem = document.createElement(&quot;p&quot;);
            document.getElementById(&quot;wrapper&quot;).addEventListener(&quot;click&quot;, append);

            function append(e) {
                let target = e.target;
                let callbackFunction = callback[target.getAttribute(&quot;data-cb&quot;)];
                callbackFunction();
            }

            function closeEventSource() {
                eventSource.close();
                eventSource = undefined;
            }

            let callback = {
                &quot;search&quot;: (function () {
                    let holderId = text.value;
                    if (eventSource !== undefined) {
                        closeEventSource();
                    }

                    if (holderId === undefined || holderId === null || holderId === &quot;&quot;) {
                        alert(&quot;소유주를 입력하시오.&quot;);
                    } else {
                        eventSource = new EventSource('/account/info/subscription/' + holderId);
                        eventSource.onopen = function () {
                            console.log(&quot;connected&quot;);
                        };
                        eventSource.onmessage = function (event) {
                            let elem = pElem.cloneNode();
                            elem.innerText = event.data;
                            appendDiv.appendChild(elem);
                        };
                        eventSource.onerror = function () {
                            console.error(&quot;Connection error has occurred&quot;);
                            closeEventSource();
                        }
                    }
                }),
                &quot;disconnect&quot;: (function () {
                    if (eventSource !== undefined) {
                        console.log(&quot;disconnected&quot;);
                        closeEventSource();
                    }
                })
            }
        }());
    });
&amp;lt;/script&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;div id=&quot;wrapper&quot;&amp;gt;
    &amp;lt;input type=&quot;button&quot; data-cb=&quot;search&quot; value=&quot;조회&quot;/&amp;gt;
    &amp;lt;input type=&quot;button&quot; data-cb=&quot;disconnect&quot; value=&quot;종료&quot;/&amp;gt;
&amp;lt;/div&amp;gt;
&amp;lt;input type=&quot;text&quot; id=&quot;holderInput&quot; placeholder=&quot;소유주 ID를 입력하시오.&quot;&amp;gt;
&amp;lt;div id=&quot;layout&quot;/&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;조회 버튼을 누르면, EventSource 객체를 생성하여 Server Sent Event를 수신받으며, 메시지가 전달되면 수신된 데이터를 화면에 출력하도록 구현하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;7. 화면이 정상적으로 출력되는지 확인하기 위하여, Query App을 기동합니다. 이후 웹브라우저(Chrome)을 열고 화면 호출 테스트를 수행합니다.(http://localhost:9090/usbscription)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;subscription 화면.png&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zBzbH/btqBjWvkNux/wqBkY3jy5otBitdGhxM7DK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zBzbH/btqBjWvkNux/wqBkY3jy5otBitdGhxM7DK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zBzbH/btqBjWvkNux/wqBkY3jy5otBitdGhxM7DK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzBzbH%2FbtqBjWvkNux%2FwqBkY3jy5otBitdGhxM7DK%2Fimg.png&quot; data-filename=&quot;subscription 화면.png&quot; data-origin-width=&quot;497&quot; data-origin-height=&quot;138&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;8. 테스트가 완료되었으면, Query를 수행할 API 내용을 구현하겠습니다. 먼저 Query 모듈 &lt;b&gt;service&lt;/b&gt; 패키지내 &lt;i&gt;QueryService&lt;/i&gt; 인터페이스를 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CkNmt/btqBjBLDIRQ/LQvNCEgor1TzGKDUAyXrd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CkNmt/btqBjBLDIRQ/LQvNCEgor1TzGKDUAyXrd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CkNmt/btqBjBLDIRQ/LQvNCEgor1TzGKDUAyXrd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCkNmt%2FbtqBjBLDIRQ%2FLQvNCEgor1TzGKDUAyXrd1%2Fimg.png&quot; data-origin-width=&quot;392&quot; data-origin-height=&quot;456&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr style=&quot;margin: 20px auto 0px; border: none; cursor: pointer !important; z-index: 1; font-size: 0px; line-height: 0; background: url('../image/divider-line.svg') center 0px / 200px 420px no-repeat; width: 64px; height: 4px; padding: 20px; color: #333333; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;9. Query 수행을 위한 메소드를 정의합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;QueryService.java&lt;/p&gt;
&lt;pre id=&quot;code_1579428559926&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface QueryService {
    (...중략...)
    Flux&amp;lt;HolderAccountSummary&amp;gt; getAccountInfoSubscription(String holderId);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;10. Service 구현을 위하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Query 모듈 &lt;b&gt;service&lt;/b&gt; 패키지내 &lt;b&gt;QueryServiceImpl&lt;/b&gt; 클래스를 엽니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGP5nQ/btqBh15mBK8/0acXPirAvdBBWiM2vYZvuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGP5nQ/btqBh15mBK8/0acXPirAvdBBWiM2vYZvuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGP5nQ/btqBh15mBK8/0acXPirAvdBBWiM2vYZvuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGP5nQ%2FbtqBh15mBK8%2F0acXPirAvdBBWiM2vYZvuk%2Fimg.png&quot; data-origin-width=&quot;386&quot; data-origin-height=&quot;504&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr style=&quot;margin: 20px auto 0px; border: none; cursor: pointer !important; z-index: 1; font-size: 0px; line-height: 0; background: url('../image/divider-line.svg') center 0px / 200px 420px no-repeat; width: 64px; height: 4px; padding: 20px; color: #333333; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;11. Interface에 정의된 메소드를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;QueryServiceImpl.java&lt;/p&gt;
&lt;pre id=&quot;code_1579428601744&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Slf4j
@Service
public class QueryServiceImpl implements QueryService {
    (...중략...)
    @Override
    public Flux&amp;lt;HolderAccountSummary&amp;gt; getAccountInfoSubscription(String holderId) {
        AccountQuery accountQuery = new AccountQuery(holderId);
        log.debug(&quot;handling {}&quot;, accountQuery);

        SubscriptionQueryResult&amp;lt;HolderAccountSummary, HolderAccountSummary&amp;gt; queryResult = queryGateway.subscriptionQuery(accountQuery,
                ResponseTypes.instanceOf(HolderAccountSummary.class),
                ResponseTypes.instanceOf(HolderAccountSummary.class)
        );

        return Flux.create(emitter -&amp;gt; {
            queryResult.initialResult().subscribe(emitter::next);
            queryResult.updates()
                    .doOnNext(holder -&amp;gt; {
                        log.debug(&quot;doOnNext : {}, isCanceled {}&quot;, holder, emitter.isCancelled());
                        if (emitter.isCancelled()) {
                            queryResult.close();
                        }
                    })
                    .doOnComplete(emitter::complete)
                    .subscribe(emitter::next);
        });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드 구현 내용은 최초에 initalResult 생성 후에, 지속적으로 updates 메소드를 통해 Stream 데이터를 전달받아 Client에게 전달합니다. 만약 중간에 Connection이 실패하게되면, 해당 Flux를 종료하도록 구현하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;12. API End Point 설정을 위해 &lt;b&gt;controller&lt;/b&gt; 패키지내 위치한 &lt;b&gt;HolderAccountController&lt;/b&gt; 클래스를 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;445&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mUAF1/btqBkpqcJ6X/jsFQSeTvC89wwkMr8DhNsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mUAF1/btqBkpqcJ6X/jsFQSeTvC89wwkMr8DhNsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mUAF1/btqBkpqcJ6X/jsFQSeTvC89wwkMr8DhNsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmUAF1%2FbtqBkpqcJ6X%2FjsFQSeTvC89wwkMr8DhNsk%2Fimg.png&quot; data-origin-width=&quot;388&quot; data-origin-height=&quot;445&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr style=&quot;margin: 20px auto 0px; border: none; cursor: pointer !important; z-index: 1; font-size: 0px; line-height: 0; background: url('../image/divider-line.svg') center 0px / 200px 420px no-repeat; width: 64px; height: 4px; padding: 20px; color: #333333; font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; white-space: normal; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;13. EndPoint를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAccountController.java&lt;/p&gt;
&lt;pre id=&quot;code_1579428709611&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class HolderAccountController {
    (...중략...)

    @GetMapping(&quot;account/info/subscription/{id}&quot;)
    public ResponseEntity&amp;lt;Flux&amp;lt;HolderAccountSummary&amp;gt;&amp;gt; getAccountInfoSubscription(@PathVariable(value = &quot;id&quot;) @NonNull @NotBlank String holderId){
        return ResponseEntity.ok()
                             .body(queryService.getAccountInfoSubscription(holderId));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;14. Subscription에서는 Read Model에 변경이 발생되었을 때 이를 전파해야합니다. 따라서 이를 작성하기 위해&amp;nbsp; Query 모듈 &lt;b&gt;projection&lt;/b&gt; 패키지 하위 &lt;b&gt;HolderAccountProjection&lt;/b&gt; 클래스를 엽니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;projection.png&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfaLdh/btqBjUxyjTp/vZsvRrycKnxd6HQFRxvwe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfaLdh/btqBjUxyjTp/vZsvRrycKnxd6HQFRxvwe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfaLdh/btqBjUxyjTp/vZsvRrycKnxd6HQFRxvwe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfaLdh%2FbtqBjUxyjTp%2FvZsvRrycKnxd6HQFRxvwe1%2Fimg.png&quot; data-filename=&quot;projection.png&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;389&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;15. EventSourcingHandler를 통해 ReadModel의 변화가 발생하였을 때, &lt;b&gt;QueryUpdateEmitter&lt;/b&gt; 클래스를 통해 이벤트 변경 내용을 전파하도록 클래스 내용을 수정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;HolderAccountProjection.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1579429863713&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@EnableRetry
@AllArgsConstructor
@Slf4j
@ProcessingGroup(&quot;accounts&quot;)
public class HolderAccountProjection {
    private final AccountRepository repository;
    private final QueryUpdateEmitter queryUpdateEmitter;

    (...중략...)

    @EventHandler
    @AllowReplay
    protected void on(DepositMoneyEvent event, @Timestamp Instant instant){
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setTotalBalance(holderAccount.getTotalBalance() + event.getAmount());

        queryUpdateEmitter.emit(AccountQuery.class,
                query -&amp;gt; query.getHolderId().equals(event.getHolderID()),
                holderAccount);

        repository.save(holderAccount);
    }
    @EventHandler
    @AllowReplay
    protected void on(WithdrawMoneyEvent event, @Timestamp Instant instant){
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setTotalBalance(holderAccount.getTotalBalance() - event.getAmount());

        queryUpdateEmitter.emit(AccountQuery.class,
                query -&amp;gt; query.getHolderId().equals(event.getHolderID()),
                holderAccount);

        repository.save(holderAccount);
    }

	(...중략...)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;각 Handler안에 queryUpdateEmitter를 통해 구독중인 Query와 동일한 ID의 Event가 들어오면, Query 결과에 전달되도록 처리하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;16. Query App을 기동합니다. 이후 EventStore에 저장된 HolderID 중 하나를 선택하여 입력창에 기입합니다. 조회 버튼을 눌러 정상적으로 조회되는지 확인합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;subscription 화면 결과 1.png&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;181&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpL8O2/btqBjBrsx7v/ckuu7mzZLUFsdCxG2sQKMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpL8O2/btqBjBrsx7v/ckuu7mzZLUFsdCxG2sQKMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpL8O2/btqBjBrsx7v/ckuu7mzZLUFsdCxG2sQKMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpL8O2%2FbtqBjBrsx7v%2Fckuu7mzZLUFsdCxG2sQKMK%2Fimg.png&quot; data-filename=&quot;subscription 화면 결과 1.png&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;181&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;위 예제에서 holderId가&amp;nbsp;&lt;b&gt;924eb0ab-c35f-4d3e-b753-a4ce35bd7c27&lt;/b&gt;인 계좌 전체의 잔고는 현재 290입니다. Update가 정상 수신되는지 확인하기 위하여, 해당 소유주가 보유한 계좌에서 5원을 인출하는 API를 호출합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;인출.png&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bikZ1r/btqBjBSAD33/rK8Jjrx9tGbPrh9b1RkWDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bikZ1r/btqBjBSAD33/rK8Jjrx9tGbPrh9b1RkWDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bikZ1r/btqBjBSAD33/rK8Jjrx9tGbPrh9b1RkWDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbikZ1r%2FbtqBjBSAD33%2FrK8Jjrx9tGbPrh9b1RkWDK%2Fimg.png&quot; data-filename=&quot;인출.png&quot; data-origin-width=&quot;458&quot; data-origin-height=&quot;194&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;17. 5원 인출 후 Client 화면에서 변경된 데이터가 정상 수신되었는지 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;변경결과 수신.png&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GAFgU/btqBkNEtnQm/KxjmFPFEWUbIRpfavN9agK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GAFgU/btqBkNEtnQm/KxjmFPFEWUbIRpfavN9agK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GAFgU/btqBkNEtnQm/KxjmFPFEWUbIRpfavN9agK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGAFgU%2FbtqBkNEtnQm%2FKxjmFPFEWUbIRpfavN9agK%2Fimg.png&quot; data-filename=&quot;변경결과 수신.png&quot; data-origin-width=&quot;1159&quot; data-origin-height=&quot;226&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;확인 결과, 정상적으로 데이터 수신되었음을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;18. 종료 버튼을 눌러 구독을 중지합니다. 이후 &lt;b&gt;924eb0ab-c35f-4d3e-b753-a4ce35bd7c27 &lt;/b&gt;소유주가 보유한 계좌에서 추가로 5원 인출하였을 때, Query 결과가 화면에 표시되지 않음을 확인합니다. 화면의 변화가 없으면, 정상적으로 Connection이 종료된 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 마치며&lt;/h4&gt;
&lt;p&gt;이번 시간에는 Point to Point, Subscription Query에 대해서 살펴보았습니다. Subscription Query는 반환 형태가 Flux 형태다보니 아무래도 Spring MVC에서는 사용하기 힘든 부분이 있을듯 합니다. 또한, 이를 지원하기 위해서는 Client에서도 SSE를 위한 구현이 필요하며, Server에서는 Client와 Connection 유지를 위해 Subscription Query가 증가할 수록 그에 상응하는 Thread 수가 증가합니다. 따라서 비즈니스 요건에 맞게 적절한 사용이 필요하며, Spring Webflux를 사용한다면 도입을 검토해볼 수 있을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>Axon Server</category>
      <category>Axon 프레임워크</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>EventSourcing</category>
      <category>MSA</category>
      <category>마이크로서비스아키텍처</category>
      <category>이벤트소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/20</guid>
      <comments>https://cla9.tistory.com/20#entry20comment</comments>
      <pubDate>Sun, 19 Jan 2020 19:41:51 +0900</pubDate>
    </item>
    <item>
      <title>15. Query 어플리케이션 구현(Query) - 1</title>
      <link>https://cla9.tistory.com/19</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅부터 생성된 Read Model에 대하여 Query하는 방법에 대하여 소개하겠습니다. AxonServer를 통해서 수많은 MicroService App들이 연결되어 있을 수 있습니다. 이러한 환경에서 Query 요청을 했을때, 단순 1:1 요청 응답을 요구할 수 도 있고 때로는 Query 요청에 따라 2개 이상의 다른 App에서 데이터를 수신받는 경우도 있습니다. 따라서 각기 다른 경우에 따라 처리해야하는 방법이 다릅니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Axon Framework는 총 3가지 타입의 Query 기능을 제공합니다. 이번 포스팅에서는 Axon 에서 제공하는 Query 종류와 동작 원리를 알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Axon Server 라우팅 기능(Query)&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Command 명령을 요청할 때는 CommandBus를 이용하였고, Event 발생시에는 EventBus를 이용하였습니다. 마찬가지로 Query는 QueryGateway를 통해 요청을 전달하며, 전달된 Query는 QueryBus를 통해서 해당 Query를 처리하는 Handler로 연결됩니다. 이때 QueryHandler가 속한 App에 Query를 전달하는 역할을 Axon Server가 수행합니다. Query 관련 Axon Server의 라우팅 기능 동작 흐름을 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;1. Point to Point Query&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Axon Server 라우팅.png&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;546&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byEojm/btqA7rhb63y/fBlJZ1sYVv8pevevZLfGl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byEojm/btqA7rhb63y/fBlJZ1sYVv8pevevZLfGl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byEojm/btqA7rhb63y/fBlJZ1sYVv8pevevZLfGl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyEojm%2FbtqA7rhb63y%2FfBlJZ1sYVv8pevevZLfGl1%2Fimg.png&quot; data-filename=&quot;Axon Server 라우팅.png&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;546&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Command Handler와 마찬가지로 Application 기동시 AxonServer와 연결을 시도합니다. 연결이 완료되면, 해당 App은 자신이 처리가능한 Query Handler 정보를 Server에 등록합니다. Point to Point Query는 해당 Query를 처리하는 Handler가 단 하나의 Application에만 존재할 경우 해당 처리할 수 있는 App으로 Query를 전달하여 결과를 전달합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;b&gt;2. Scatter &amp;amp; Gatter Query&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Scatter Gatter.png&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;797&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dG1ZD5/btqA8iYB5ex/SytC2TTSt58lQ9Q74Vdux1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dG1ZD5/btqA8iYB5ex/SytC2TTSt58lQ9Q74Vdux1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dG1ZD5/btqA8iYB5ex/SytC2TTSt58lQ9Q74Vdux1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdG1ZD5%2FbtqA8iYB5ex%2FSytC2TTSt58lQ9Q74Vdux1%2Fimg.png&quot; data-filename=&quot;Scatter Gatter.png&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;797&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;두번째는 Scatter-Gather Query입니다. 이는 동일한 Query를 처리하는 Handler가 여러 App에 등록되어있을 때, 이를 처리하는 방법입니다. Application 기동시 Query Handler 정보를 Axon Server에 등록하면, Application 정보가 라우팅 테이블에 해당 App 정보를 기록합니다. 이때는 Client 측에서 각기 다른 App에서 수신되는 결과를 수집하여 처리 방법(한쪽 결과만 수집, 둘다 수집 등)을 정해야합니다. Axon Server는 Query 요청이 들어오면 등록된 Application에게 Query를 전달하며, 수신받은 App에서는 결과를 취합하여 전송합니다. 이후 Client 측에서 결과를 수신받아 데이터 결과를 종합합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;b&gt;3. Subscription Query&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Subscription 일반상황.png&quot; data-origin-width=&quot;1455&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wXUYY/btqA6oebCSK/EEARJeuogtzDk59r1SW5a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wXUYY/btqA6oebCSK/EEARJeuogtzDk59r1SW5a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wXUYY/btqA6oebCSK/EEARJeuogtzDk59r1SW5a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwXUYY%2FbtqA6oebCSK%2FEEARJeuogtzDk59r1SW5a0%2Fimg.png&quot; data-filename=&quot;Subscription 일반상황.png&quot; data-origin-width=&quot;1455&quot; data-origin-height=&quot;376&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Point to Point Query를 요청하였을 때, 만약 Query를 수행하는 Read Model에 대한 변경이 발생한다면, 화면에 출력되는 결과와 Read Model 사이 데이터 정합성 불일치 문제가 발생합니다. 따라서 이를 해결하기 위해서는 주기적으로 Query를 재요청하는 방법이 있습니다. 하지만 데이터 변경이 발생하지 않아도 계속 Query를 요청해야하는 문제점이 있으므로 효율적이지 못합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;subscription.png&quot; data-origin-width=&quot;1803&quot; data-origin-height=&quot;442&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bk8WyW/btqA574Scb0/J0NckgIs1k2FphqJ5NpVV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bk8WyW/btqA574Scb0/J0NckgIs1k2FphqJ5NpVV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bk8WyW/btqA574Scb0/J0NckgIs1k2FphqJ5NpVV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbk8WyW%2FbtqA574Scb0%2FJ0NckgIs1k2FphqJ5NpVV0%2Fimg.png&quot; data-filename=&quot;subscription.png&quot; data-origin-width=&quot;1803&quot; data-origin-height=&quot;442&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Subscription Query는 Client측에서 Query를 요청할 때, Query 결과를 전달받고 Connection을 끊는 것이 아니라 계속 지속합니다. 이후 Query Handler가 위치한 App의 Read Model 변경이 발생할 경우 변경분에 대한 데이터를 전달받아 이를 최신화합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Query Handler 동작 과정&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;빈생성.png&quot; data-origin-width=&quot;2150&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bawMwD/btqA3ejtH2f/IsyiuJLRgVz0EDhBYIGosk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bawMwD/btqA3ejtH2f/IsyiuJLRgVz0EDhBYIGosk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bawMwD/btqA3ejtH2f/IsyiuJLRgVz0EDhBYIGosk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbawMwD%2FbtqA3ejtH2f%2FIsyiuJLRgVz0EDhBYIGosk%2Fimg.png&quot; data-filename=&quot;빈생성.png&quot; data-origin-width=&quot;2150&quot; data-origin-height=&quot;772&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonFramework 관련 Bean 생성시 사용자가 지정한 QueryBus가 없으면, Default로 &lt;b&gt;AxonServerQueryBus&lt;/b&gt;가 생성됩니다. 이때 내부적으로 &lt;b&gt;QueryProcessor&lt;/b&gt;가 만들어지고, 해당 생성자 안에서 &lt;b&gt;ExecuterService&lt;/b&gt;를 통해 요청시 최대 10개의 Thread를 Default로 생성하도록 Handler에 등록합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;ExecuterService에 의해서 만들어지는 Thread는 &lt;b&gt;QueryProcessingTask&lt;/b&gt;이며, AxonServer로부터 Query를 전달받으면 해당 클래스의 run 메소드를 통해 QueryHandler 작업이 이어집니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Query 전달과정.png&quot; data-origin-width=&quot;2631&quot; data-origin-height=&quot;1643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Jk5po/btqA5QClQse/be7lw6VdTcNu35VBMmWExK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Jk5po/btqA5QClQse/be7lw6VdTcNu35VBMmWExK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Jk5po/btqA5QClQse/be7lw6VdTcNu35VBMmWExK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJk5po%2FbtqA5QClQse%2Fbe7lw6VdTcNu35VBMmWExK%2Fimg.png&quot; data-filename=&quot;Query 전달과정.png&quot; data-origin-width=&quot;2631&quot; data-origin-height=&quot;1643&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Application 구동 이후, Query 요청이 발생되면, 내부적으로는 위와 같은 흐름을 거쳐 메시지가 전달됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;QueryGateway&lt;/b&gt;로 Query를 전달합니다. 이때 전달하는 Query가 Scatter-Gather, Subsciprtion, Point to Point 중 하나임을 메소드를 통해 AxonServer에게 전달합니다.&lt;/li&gt;
&lt;li&gt;사용자가 전달한 Query를 &lt;b&gt;GenericQueryMessage&lt;/b&gt;로 변환한다음 &lt;b&gt;QueryBus&lt;/b&gt;로 전달합니다. Default QueryBus는 AxonServerQueryBus이므로 AxonServer에 전달됩니다.&lt;/li&gt;
&lt;li&gt;전달된 Query는 &lt;b&gt;QueryHandler가 존재하는 App&lt;/b&gt;으로 라우팅됩니다. 이때 AxonServer로부터 gRPC를 통해 onMessage 메소드가 호출되면, AxonServerQueryBus 내부에 할당된 Handler들의 onNext 메소드를 호출합니다.&lt;/li&gt;
&lt;li&gt;Bean 등록 당시 Handler 호출시 ExecutorService로부터 &lt;b&gt;QueryProcessingTask&lt;/b&gt; 생성을 요청하였습니다. 따라서 Handler 호출과정에서 Thread 생성을 요청합니다.&lt;/li&gt;
&lt;li&gt;QueryProcessingTask 내부에 있는 run 메소드가 수행되면서 &lt;b&gt;QueryProcessor&lt;/b&gt;에게 Query 수행을 위임합니다.&lt;/li&gt;
&lt;li&gt;QueryProcessor 내부 로직 수행중 Query 수행을 &lt;b&gt;SimpleQueryBus&lt;/b&gt;에게 위임합니다.&lt;/li&gt;
&lt;li&gt;내부적으로 &lt;a href=&quot;https://docs.axoniq.io/reference-guide/configuring-infrastructure-components/messaging-concepts/unit-of-work&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;&lt;b&gt;UnitOfWork&lt;/b&gt; &lt;/u&gt;&lt;/a&gt;과정을 거치면서, Reflection을 통해 QueryHandler 메소드를 찾아 수행후 결과를 돌려 받습니다.&lt;/li&gt;
&lt;li&gt;최종 수행된 결과를&amp;nbsp;&lt;b&gt;QueryProviderOutbound&lt;/b&gt;에게 전달합니다.&lt;/li&gt;
&lt;li&gt;AxonServer에게 결과를 전달합니다.&lt;/li&gt;
&lt;li&gt;Query를 요청한 Client에게 결과를 전달합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 마치며&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Query App 구현을 위해 기본적으로 알아야하는 내부 과정에 대해서 살펴봤습니다. 다음 포스팅에서는 코드 구현을 진행하겠습니다.&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>Axon Server</category>
      <category>Axon 프레임워크</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>Event sourcing</category>
      <category>MSA</category>
      <category>마이크로서비스아키텍처</category>
      <category>이벤트소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/19</guid>
      <comments>https://cla9.tistory.com/19#entry19comment</comments>
      <pubDate>Tue, 14 Jan 2020 20:51:25 +0900</pubDate>
    </item>
    <item>
      <title>14. Query 어플리케이션 구현(Event) - 4</title>
      <link>https://cla9.tistory.com/18</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Software 개발 및 유지보수 단계에서 요구사항에 의하여 데이터 모델은 변하기 마련입니다. 그리고 바뀌는 데이터모델에 맞춰 Event 또한 형태가 변합니다. 이때, 이전 발행된 Event와 앞으로 적재되는 Event의 형태는 다르게 됩니다. 따라서 Replay 과정에서 변경된 Event를 적용하는데 있어 문제가 발생되지 않도록 코드를 통한 중재가 필요합니다. Axon 에서는 이를 위해 Event Upcasting 기능을 제공합니다. 이번 포스팅에서는 코드를 통하여 Event Upcasting을 적용하는 방법을 알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Versioning&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Event.png&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;668&quot; width=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLsPNX/btqA2T0udv9/k9JmkapTW6hVomghn5tlCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLsPNX/btqA2T0udv9/k9JmkapTW6hVomghn5tlCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLsPNX/btqA2T0udv9/k9JmkapTW6hVomghn5tlCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLsPNX%2FbtqA2T0udv9%2Fk9JmkapTW6hVomghn5tlCk%2Fimg.png&quot; data-filename=&quot;Event.png&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;668&quot; width=&quot;720&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Application 개발 후 요구사항 변경으로 계정 가입시에 &lt;b&gt;회사명&lt;/b&gt; 정보가 추가되며, Read Model MView(Materialized View)에도 회사명 정보가 포함되고, &lt;b&gt;회사명이 기입되지 않으면 N/A&lt;/b&gt;로 표시된다고 가정하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;요구사항 변경 내용을 코드로 구현하면 다음과 같습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;1. Command 수정을 위해서 API 변경이 필요합니다. Command 모듈내 dto 패키지에 위치한 &lt;b&gt;HolderDTO&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;holderDTO.png&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;551&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkD55w/btqA6D2VRAB/KOKHMp1PLafsFs9SbUcP7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkD55w/btqA6D2VRAB/KOKHMp1PLafsFs9SbUcP7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkD55w/btqA6D2VRAB/KOKHMp1PLafsFs9SbUcP7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkD55w%2FbtqA6D2VRAB%2FKOKHMp1PLafsFs9SbUcP7k%2Fimg.png&quot; data-filename=&quot;holderDTO.png&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;551&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. DTO 클래스에 company 정보를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderDTO.java&lt;/p&gt;
&lt;pre id=&quot;code_1578745874367&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Getter
@AllArgsConstructor
@NoArgsConstructor
public class HolderDTO {
    private String holderName;
    private String tel;
    private String address;
    private String company;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. Common 모듈 commands 패키지내 &lt;b&gt;HolderCreationCommand&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Command.png&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjfUTz/btqA3eJ16P5/J0tJUpL0lr9pxn8FTA1nBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjfUTz/btqA3eJ16P5/J0tJUpL0lr9pxn8FTA1nBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjfUTz/btqA3eJ16P5/J0tJUpL0lr9pxn8FTA1nBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjfUTz%2FbtqA3eJ16P5%2FJ0tJUpL0lr9pxn8FTA1nBK%2Fimg.png&quot; data-filename=&quot;Command.png&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;480&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;4. Command 클래스에 company 정보를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderCreationCommand.java&lt;/p&gt;
&lt;pre id=&quot;code_1578743370073&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@ToString
@Getter
public class HolderCreationCommand {
    @TargetAggregateIdentifier
    private String holderID;
    private String holderName;
    private String tel;
    private String address;
    private String company;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;5. Command 모듈 service 패키지에 위치한 &lt;b&gt;TransactionServiceImpl&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;서비스.png&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGJjdH/btqA5QopllT/Ii3NUiucgA8bnksKRGOv81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGJjdH/btqA5QopllT/Ii3NUiucgA8bnksKRGOv81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGJjdH/btqA5QopllT/Ii3NUiucgA8bnksKRGOv81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGJjdH%2FbtqA5QopllT%2FIi3NUiucgA8bnksKRGOv81%2Fimg.png&quot; data-filename=&quot;서비스.png&quot; data-origin-width=&quot;663&quot; data-origin-height=&quot;698&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;6. Service 클래스에서 계정 생성 로직에 Company 정보를 넘기도록 수정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TransactionServiceImpl.java&lt;/p&gt;
&lt;pre id=&quot;code_1578746037213&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class TransactionServiceImpl implements TransactionService {
	(...중략...)
    @Override
    public CompletableFuture&amp;lt;String&amp;gt; createHolder(HolderDTO holderDTO) {
        return commandGateway.send(new HolderCreationCommand(UUID.randomUUID().toString()
                , holderDTO.getHolderName()
                , holderDTO.getTel()
                , holderDTO.getAddress()
                , holderDTO.getCompany())
                );
    }
    (...중략...)
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;7. Common 모듈의 &lt;b&gt;build.gradle&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;Common build.png&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ONqSt/btqA7rnDLnS/PBoSidOvfRzk2HHF8rfIA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ONqSt/btqA7rnDLnS/PBoSidOvfRzk2HHF8rfIA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ONqSt/btqA7rnDLnS/PBoSidOvfRzk2HHF8rfIA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FONqSt%2FbtqA7rnDLnS%2FPBoSidOvfRzk2HHF8rfIA0%2Fimg.png&quot; data-filename=&quot;Common build.png&quot; data-origin-width=&quot;634&quot; data-origin-height=&quot;239&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;8. build.gradle 파일에 &lt;b&gt;axon-messaging&lt;/b&gt; 의존성을 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1578743904728&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ext{
    axonVersion = &quot;4.2.1&quot;
}
bootJar { 
    enabled = false 
}
jar {
    enabled = true
}
dependencies{
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation group: 'org.axonframework', name: 'axon-messaging', version: &quot;$axonVersion&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;9. Common 모듈 events 패키지 내 &lt;b&gt;HolderCreationEvent&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;HolderCreationEVent.png&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGVDxd/btqA5QIHfL6/6YZ2VJKK3EIi9w9VLjlZgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGVDxd/btqA5QIHfL6/6YZ2VJKK3EIi9w9VLjlZgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGVDxd/btqA5QIHfL6/6YZ2VJKK3EIi9w9VLjlZgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGVDxd%2FbtqA5QIHfL6%2F6YZ2VJKK3EIi9w9VLjlZgK%2Fimg.png&quot; data-filename=&quot;HolderCreationEVent.png&quot; data-origin-width=&quot;667&quot; data-origin-height=&quot;418&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;10. Event에 변경된 사항을 추가합니다. &lt;span style=&quot;color: #333333;&quot;&gt;이때 Event에는 변경이 발생하므로, Event의 변경이 발생했음을 알리는 마커가 필요합니다. 이때 사용되는 어노테이션이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;@Revision&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;입니다. Revision 표시를 통해서 실제 Event가 발생되었을 때 EventStore에는 해당 Event의 버전이 저장되며, 추후 Event Upcasting 시에 해당 정보가 사용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderCreationEvent.java&lt;/p&gt;
&lt;pre id=&quot;code_1578743972963&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@ToString
@Getter
@Revision(&quot;1.0&quot;)
public class HolderCreationEvent {
    private String holderID;
    private String holderName;
    private String tel;
    private String address;
    private String company;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;11. Command 모듈 aggregate 패키지내에 위치한 &lt;b&gt;HolderAggregate&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;HolderAggregate.png&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;551&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bknHBG/btqA6okCKfS/ElaL9Dzj4IroitokpD4HCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bknHBG/btqA6okCKfS/ElaL9Dzj4IroitokpD4HCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bknHBG/btqA6okCKfS/ElaL9Dzj4IroitokpD4HCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbknHBG%2FbtqA6okCKfS%2FElaL9Dzj4IroitokpD4HCK%2Fimg.png&quot; data-filename=&quot;HolderAggregate.png&quot; data-origin-width=&quot;654&quot; data-origin-height=&quot;551&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;12. Aggregate 클래스 Event 발행 로직에 company 정보를 전달할 수 있도록 변경합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAggregate.java&lt;/p&gt;
&lt;pre id=&quot;code_1578746205177&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@NoArgsConstructor
@Aggregate
@Slf4j
public class HolderAggregate {
	(...중략...)
    @CommandHandler
    public HolderAggregate(HolderCreationCommand command) {
        log.debug(&quot;handling {}&quot;, command);

        apply(new HolderCreationEvent(command.getHolderID(), command.getHolderName(), command.getTel(), command.getAddress(), command.getCompany()));
    }
    (...중략...)
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;13. Query 모듈에 version 패키지를 생성한 다음 &lt;b&gt;HolderCreationEventV1&lt;/b&gt; 클래스를 만듭니다. 해당 클래스는 변경 이전HolderCreationEvent에 대해서 변경 후에 어떻게 추가된 정보를 처리를 지정 용도로 사용됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;version.png&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tcSlP/btqA5PC1hCB/cYskglmWlmLn7jrxFRtdrk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tcSlP/btqA5PC1hCB/cYskglmWlmLn7jrxFRtdrk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tcSlP/btqA5PC1hCB/cYskglmWlmLn7jrxFRtdrk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtcSlP%2FbtqA5PC1hCB%2FcYskglmWlmLn7jrxFRtdrk%2Fimg.png&quot; data-filename=&quot;version.png&quot; data-origin-width=&quot;644&quot; data-origin-height=&quot;418&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;14. HolderCreationEventV1 클래스 내용을 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderCreationEventV1.java&lt;/p&gt;
&lt;pre id=&quot;code_1578746800423&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class HolderCreationEventV1 extends SingleEventUpcaster {
    private static SimpleSerializedType targetType = new SimpleSerializedType(HolderCreationEvent.class.getTypeName(), null);

    @Override
    protected boolean canUpcast(IntermediateEventRepresentation intermediateRepresentation) {
        return intermediateRepresentation.getType().equals(targetType);
    }

    @Override
    protected IntermediateEventRepresentation doUpcast(IntermediateEventRepresentation intermediateRepresentation) {
        return intermediateRepresentation.upcastPayload(
                new SimpleSerializedType(targetType.getName(), &quot;1.0&quot;),
                org.dom4j.Document.class,
                document -&amp;gt; {
                    document.getRootElement()
                            .addElement(&quot;company&quot;)
                            .setText(&quot;N/A&quot;);
                    return document;
                }
        );
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;코드 구현내역은 다음과 같습니다. 먼저 targetType에는 대상 Event를 지정합니다. 데모 프로젝트에서는 HolderCreationEvent가 변경되었으므로 해당 Class 타입을 지정합니다. 두번째 인자에 위치한 null 값은 최초에는 &lt;b&gt;@Revision&lt;/b&gt; 정보를 명시하지 않았으므로 존재하는 값이 없기 때문에 null로 지정하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;doUpcast&lt;/b&gt; 메소드는 실제 Event Version을 확인하고 이전 버전의 Event가 들어왔을 때 행동해야할 내용을 기술합니다.&lt;/p&gt;
&lt;p&gt;EventStore에 저장된 Event 내용이 XML로 지정되므로, XML로 되어있는 Payload 에서&amp;nbsp;신규 추가된 company 정보와 값이 없을 경우 입력될 Default 값 N/A를 setText 메소드를 통해 지정합니다. 또한 해당 작업을 수행할 대상을 Revision 정보가 null인 targetType으로 한정합니다. 따라서 해당 메소드를 통해서 확인할 수 있는 사실은 HolderCreationEvent에 수많은 Revision 정보가 존재하더라도, HolderEventCreationEventV1 클래스에서는 Revision 번호가 1.0과 null인 두 Event간의 속성값에만 영향을 미칩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;15. Query 모듈내 config 패키지에 존재하는 &lt;b&gt;AxonConfig&lt;/b&gt; 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;config.png&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s7KlO/btqA6qbIJ7s/wYxOH3FMKrxCXVqtjNeU81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s7KlO/btqA6qbIJ7s/wYxOH3FMKrxCXVqtjNeU81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s7KlO/btqA6qbIJ7s/wYxOH3FMKrxCXVqtjNeU81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs7KlO%2FbtqA6qbIJ7s%2FwYxOH3FMKrxCXVqtjNeU81%2Fimg.png&quot; data-filename=&quot;config.png&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;494&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;14. AxonConfig 파일에 &lt;b&gt;Event Chain&lt;/b&gt;을 등록합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonConfig.java&lt;/p&gt;
&lt;pre id=&quot;code_1578747399554&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AxonConfig {
	(...중략...)

    @Bean
    public EventUpcasterChain eventUpcasterChain(){
        return new EventUpcasterChain(
                new HolderCreationEventV1()
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 Event의 버전이 여러개 생성되었다면, HolderCreationEventV1이외 여러개의 버전간 핸들러 클래스를 생성한 다음 Event Chain 생성 로직에 핸들러 인스턴스를 등록합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;15. 새로운 계정 등록 테스트를 진행한다음 Query Application을 &lt;b&gt;Replay&lt;/b&gt; 시킵니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578747866339&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST http://localhost:8080/holder
Content-Type: application/json

{
	&quot;holderName&quot; : &quot;Kane&quot;,
	&quot;tel&quot; : &quot;02-2645-5678&quot;,
	&quot;address&quot; : &quot;OO시 OO구&quot;,
    &quot;company&quot; : &quot;Korea&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578747882604&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;c.c.q.p.HolderAccountProjection          : projecting HolderCreationEvent(holderID=0df539ed-2ee2-41be-af32-0f6724a75da3, holderName=kevin, tel=02-1234-5678, address=OO시 OO구, company=N/A) , timestamp : 2020-01-07T12:11:21.047Z
c.c.q.p.HolderAccountProjection          : projecting HolderCreationEvent(holderID=a5d267c6-fbd8-4f0d-b93b-03a8dbf09747, holderName=bruce, tel=02-5291-5678, address=OO시 OO구, company=N/A) , timestamp : 2020-01-07T12:12:14.238Z
c.c.q.p.HolderAccountProjection          : projecting HolderCreationEvent(holderID=1b77f5bc-73e6-4b7a-a1fd-d5453723b9c7, holderName=Kane, tel=02-2645-5678, address=OO시 OO구, company=Korea) , timestamp : 2020-01-11T13:00:10.487Z&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 내용은 Replay 이후 Application 로그 일부를 발췌한 내용입니다. 신규 생성한 &lt;i&gt;Kane&lt;/i&gt; 사용자의 company는 입력값으로 정확히 등록이 되었으며, 기존에 발행된 Event는 N/A로 매핑된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 마치며&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅을 끝으로 Query Application에서 Event 처리와 관련된 내용을 마무리하겠습니다. 다음 포스팅에서는 Query Application에 저장된 Read Model을 기반으로 Query하는 방법에 대해서 다루도록 하겠습니다.&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>Axon Server</category>
      <category>Axon 프레임워크</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>Event sourcing</category>
      <category>MSA</category>
      <category>마이크로서비스아키텍처</category>
      <category>이벤트소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/18</guid>
      <comments>https://cla9.tistory.com/18#entry18comment</comments>
      <pubDate>Sat, 11 Jan 2020 22:14:00 +0900</pubDate>
    </item>
    <item>
      <title>13. Query 어플리케이션 구현(Event) - 3</title>
      <link>https://cla9.tistory.com/17</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cla9.tistory.com/16&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;이전 포스팅&lt;/u&gt;&lt;/a&gt;에서 Replay에 대해서 학습했습니다. Replay는 신규 Read Model이 추가되거나 기존 모델의 변경이 있을 때, EventStore에서 기존 내역을 전달받아 재수행하는 작업입니다. 따라서 EventSourcing &amp;amp; CQRS 모델을 사용한다면, Replay 성능 고민이 반드시 필요합니다. 이번 포스팅에서는 &lt;a href=&quot;https://axoniq.io/blog-overview/cqrs-replay-performance-tuning&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;AxonIQ 블로그&lt;/u&gt;&lt;/a&gt;를 기반으로 Replay 성능 개선 방법에 대해서 소개하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;본문 설명에 앞서 AxonIQ 벤치마크 테스트 환경 Spec은 다음과 같습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DBMS : Postgres(9.6), MongoDB(3.6)&lt;/li&gt;
&lt;li&gt;CPU : vCPU 8코어 (GCP)&lt;/li&gt;
&lt;li&gt;RAM : 30G&lt;/li&gt;
&lt;li&gt;OS : Ubuntu 18.10&lt;/li&gt;
&lt;li&gt;DISK : SSD 1T&lt;/li&gt;
&lt;/ul&gt;
&lt;p&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;h4 data-ke-size=&quot;size20&quot;&gt;2. Replay 문제점&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 EventStore에 저장된 Event 수가 10억개이고, Read Model에서 Replay 수행 시 초당 1000개의 Event를 재생할 수 있다고 가정한다면, Replay 작업에만 약 &lt;b&gt;11일&lt;/b&gt;이 걸립니다. 이는 대부분의 상황에서는 적용하기 힘듭니다. 따라서 Replay 작업&amp;nbsp; 최적화가 반드시 필요합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cla9.tistory.com/15&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;앞선 포스팅&lt;/u&gt;&lt;/a&gt;에서&amp;nbsp;&lt;b&gt;TrackingEventProcessor에&lt;/b&gt; 의하여 @EventHandler 메소드가 호출된다고 설명했습니다. 이때 별도 속성을 지정하지 않으면, TrackingEventProcessor는 Default 값으로 설정되며 기본값은 다음과 같습니다.&lt;/p&gt;
&lt;p&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;Thread 수 : 1개&lt;/li&gt;
&lt;li&gt;Batch Size : 1&lt;/li&gt;
&lt;li&gt;최대 Thread 수 : Segment 개수&lt;/li&gt;
&lt;li&gt;TokenClaim 주기 : 5000ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이를 통해 알 수 있는 사실은 Axon에서 Event 처리는 &lt;b&gt;Batch&lt;/b&gt; 단위로 이루어지는데, 기본 설정 값은 Event 1개씩 단일 Thread로 처리됨을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonIQ에서 TrackingEventProcessor에 대한 기본값 설정으로 Event 처리에 대한 벤치마크 수행 결과, 초당 260개의 Event를 처리하였습니다. 이는 100만개 Event 기준 10시간의 처리 능력을 보여주어 좋지 않은 처리 능력을 나타냈습니다. 지금부터 Event 처리 능력을 강화하는 방법에 대해서 하나씩 살펴보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Replay 개선 전략&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-1. Batch Size 조정&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전 설명에서 Axon Framework에서 Event를 Batch 단위로 처리한다고 했습니다. Batch 작업 시, 개별적인 Event를 다루는 것 외에 부가적인 기능은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Tracking Token을 갱신&lt;/b&gt;하여 최신의 Event Stream 위치를 기억하도록 함.&lt;/li&gt;
&lt;li&gt;Transactional Store를 사용했을 때 DB의 &lt;b&gt;Transaction Commit&lt;/b&gt; 작업 수행.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;사용자의 별도 설정이 없다면, 기본 Batch Size는 1입니다. 이는 Replay 작업을 수행하기에는 너무 적은 수치입니다. 따라서 적절한 PoC를 통해 최적의 Size를 맞추어야 합니다. Axon에서는 &lt;b&gt;TrackingEventProcessorConfiguration&lt;/b&gt;을 통해서 Size 조정이 가능합니다. 설정 방법은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Query 모듈내 &lt;b&gt;config&lt;/b&gt; 패키지 생성 후 &lt;b&gt;AxonConfig&lt;/b&gt; 파일을 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;config 설정.png&quot; data-origin-width=&quot;483&quot; data-origin-height=&quot;415&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AtEAY/btqAY7wWBj5/pOBxOFA3Xsy8K3Loftu1zK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AtEAY/btqAY7wWBj5/pOBxOFA3Xsy8K3Loftu1zK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AtEAY/btqAY7wWBj5/pOBxOFA3Xsy8K3Loftu1zK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAtEAY%2FbtqAY7wWBj5%2FpOBxOFA3Xsy8K3Loftu1zK%2Fimg.png&quot; data-filename=&quot;config 설정.png&quot; data-origin-width=&quot;483&quot; data-origin-height=&quot;415&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. AxonConfig 클래스를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonConfig.java&lt;/p&gt;
&lt;pre id=&quot;code_1578488408801&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AxonConfig {
    @Autowired
    public void configure(EventProcessingConfigurer configurer) {
        configurer.registerTrackingEventProcessor(
                &quot;accounts&quot;,
                org.axonframework.config.Configuration::eventStore,
                c -&amp;gt; TrackingEventProcessorConfiguration.forSingleThreadedProcessing()
                        .andBatchSize(100)
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;코드 구현 내용은 accounts ProcessingGroup을 처리하는 TrackingEventProcessor의 Batch Size를 100으로 설정하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;배치 벤치마크.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G3Q3o/btqAXngtLlC/DC5PgCi3iYoj3BNzzTxAnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G3Q3o/btqAXngtLlC/DC5PgCi3iYoj3BNzzTxAnK/img.png&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://axoniq.io/blog-overview/cqrs-replay-performance-tuning&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G3Q3o/btqAXngtLlC/DC5PgCi3iYoj3BNzzTxAnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG3Q3o%2FbtqAXngtLlC%2FDC5PgCi3iYoj3BNzzTxAnK%2Fimg.png&quot; data-filename=&quot;배치 벤치마크.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;384&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://axoniq.io/blog-overview/cqrs-replay-performance-tuning&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 Batch Size 조정에 따른 초당 Event 처리량을 그래프로 표시한 결과입니다. Size를 1부터 500까지 늘렸을 때 초당 260개의 이벤트 처리량에서 4000개로 &lt;b&gt;15배&lt;/b&gt;의 성능 개선이 나타났습니다. 대략 500개 이상부터는 Size 증가에 따른 개선폭이 크지 않으므로 해당 예제에서는 500개가 적정선입니다. Batch Size 크기에 대한 적절한 가이드는 없으며, 이는 각 Application 환경에 맞게 테스트 이후 찾는 것이 좋습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Batch Size를 늘려서 Transaction을 처리할 때 주의점이 있습니다. 이는 Read Model에 사용되는 DB가 Non-Transactional 하다면, 만약 Batch 중간에 실패했을 때 데이터가 자동 Rollback되지 않습니다. 따라서 이후 다시 Replay를 시도하게되면, 이미 처리된 Event가 다시 수행되므로 유의해야합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-2. 병렬 처리&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;병렬도 벤치마크.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NYzWV/btqAYqjvPwb/EuZt6sBh8aPj2eLzwtDmvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NYzWV/btqAYqjvPwb/EuZt6sBh8aPj2eLzwtDmvk/img.png&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://axoniq.io/blog-overview/cqrs-replay-performance-tuning&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NYzWV/btqAYqjvPwb/EuZt6sBh8aPj2eLzwtDmvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNYzWV%2FbtqAYqjvPwb%2FEuZt6sBh8aPj2eLzwtDmvk%2Fimg.png&quot; data-filename=&quot;병렬도 벤치마크.png&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;384&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://axoniq.io/blog-overview/cqrs-replay-performance-tuning&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TrackingEventProcessor의 Thread 수 기본 값은 1입니다. 즉 하나의 Thread로 모든 작업을 순차처리합니다. 따라서 Event Replay 성능을 높이기 위해서는 병렬도 증가가 필요합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 Batch Size는 500으로 설정한 상태에서 Thread 개수를 1에서 8개로 점차 늘렸을 때 초당 Event 처리량을 나타낸 것입니다. 병렬도를 8로 지정했을 때 초당 15000개의 Event를 처리할 수 있으므로 단일 Thread 대비 대략 &lt;b&gt;4배&lt;/b&gt;정도의 개선이 이루어졌습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;쓰레드 문제점.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;618&quot; width=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9AWxz/btqAYHL5dwv/FRS4KsycUpRhkclAjgEpRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9AWxz/btqAYHL5dwv/FRS4KsycUpRhkclAjgEpRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9AWxz/btqAYHL5dwv/FRS4KsycUpRhkclAjgEpRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9AWxz%2FbtqAYHL5dwv%2FFRS4KsycUpRhkclAjgEpRK%2Fimg.png&quot; data-filename=&quot;쓰레드 문제점.png&quot; data-origin-width=&quot;1500&quot; data-origin-height=&quot;618&quot; width=&quot;740&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 Thread 개수를 늘릴 때는 고려해야할 사항이 많습니다. 그 이유는 병렬로 처리하게되면 처리되는 Event의 순서가 뒤바뀔 수 있기 때문입니다. 따라서 병렬 처리를 수행시, 순서가 보장될 수 있도록 처리해야합니다. Axon에서는 이러한 문제를 해결 하기 위해 &lt;b&gt;Sequencing 정책&lt;/b&gt;을 제공합니다. Sequencing 정책이란 동일 Thread 내에서 Event는 반드시 처리 순서 보장에 대한 결정을 의미합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;기본적으로 Axon에서는&amp;nbsp; 동일 Aggregate에 속한 Event는 동일 Thread에서 처리될 수 있도록 &lt;b&gt;&lt;span&gt;SequentialPerAggregatePolicy&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 클래스를 제공합니다. 이를 적용하여 AxonConfig 클래스 수정을 통해 병렬도를 변경하도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonConfig.java&lt;/p&gt;
&lt;pre id=&quot;code_1578490696251&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class AxonConfig {
    @Autowired
    public void configure(EventProcessingConfigurer configurer) {
        configurer.registerTrackingEventProcessor(
                &quot;accounts&quot;,
                org.axonframework.config.Configuration::eventStore,
                c -&amp;gt; TrackingEventProcessorConfiguration.forParallelProcessing(3)
                        .andBatchSize(100)
        );

        configurer.registerSequencingPolicy(&quot;accounts&quot;,
                configuration -&amp;gt; SequentialPerAggregatePolicy.instance());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이전 대비 변경된 내용은 TrackingEventProcessor에 대해서 병렬도를 3으로 지정하였습니다. 또한 accounts ProcessingGroup을 대상으로 SequentialPerAggregatePolicy를 적용하여, 단일 Thread 내에서 동일 Aggregate Event가 순서대로 처리되도록 정책 설정하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;토큰 Store.png&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDoU1u/btqAYpZc2X2/1ueHhQKp4vISoMOL367E60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDoU1u/btqAYpZc2X2/1ueHhQKp4vISoMOL367E60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDoU1u/btqAYpZc2X2/1ueHhQKp4vISoMOL367E60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDoU1u%2FbtqAYpZc2X2%2F1ueHhQKp4vISoMOL367E60%2Fimg.png&quot; data-filename=&quot;토큰 Store.png&quot; data-origin-width=&quot;1268&quot; data-origin-height=&quot;272&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;코드 변경 후 실제 Application을 구동한다음 Token Store에 TrackingEventProcessor 별로 Token이 생긴 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;하지만 데모 프로젝트에서 단순히 위와같이 병렬도를 지정하고 Application을 수행하면, &lt;b&gt;소유주가 존재하지 않습니다 Error&lt;/b&gt;가 발생할 수도 있습니다. 이유는 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;aggregate.png&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;177&quot; width=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s6G2Y/btqA05Fdins/5Zb9jTl0LW9Q8sK2KXGGHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s6G2Y/btqA05Fdins/5Zb9jTl0LW9Q8sK2KXGGHK/img.png&quot; data-alt=&quot;데모 프로젝트 Aggregate 종류&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s6G2Y/btqA05Fdins/5Zb9jTl0LW9Q8sK2KXGGHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs6G2Y%2FbtqA05Fdins%2F5Zb9jTl0LW9Q8sK2KXGGHK%2Fimg.png&quot; data-filename=&quot;aggregate.png&quot; data-origin-width=&quot;697&quot; data-origin-height=&quot;177&quot; width=&quot;420&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데모 프로젝트 Aggregate 종류&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;쓰레드 문제점 2.png&quot; data-origin-width=&quot;2066&quot; data-origin-height=&quot;629&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dMLc5F/btqAZORuXhj/dvpsZ85v7Xb3M41gy7mCeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dMLc5F/btqAZORuXhj/dvpsZ85v7Xb3M41gy7mCeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dMLc5F/btqAZORuXhj/dvpsZ85v7Xb3M41gy7mCeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdMLc5F%2FbtqAZORuXhj%2FdvpsZ85v7Xb3M41gy7mCeK%2Fimg.png&quot; data-filename=&quot;쓰레드 문제점 2.png&quot; data-origin-width=&quot;2066&quot; data-origin-height=&quot;629&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Command App에서 구현된 Aggregate는 Holder와 Account 두개 입니다. 비즈니스 로직상 Account Aggregate는 반드시 Holder Aggregate가 존재해야지만 생성이 가능하며, Event Stream에도 순차적으로 생성되어 있습니다. 하지만 Read Model을 반영하는 과정에서 Thread를 분리시키면, 근본적으로 두 개의 Aggregate는 다릅니다. 따라서&amp;nbsp;&lt;span&gt;SequentialPerAggregatePolicy&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt; 설정했어도&amp;nbsp;&lt;/span&gt;&lt;/span&gt;다른 Thread에 생성되어 처리될 가능성이 높습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAccountProjection.java&lt;/p&gt;
&lt;pre id=&quot;code_1578493049376&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@EnableRetry
@AllArgsConstructor
@Slf4j
@ProcessingGroup(&quot;accounts&quot;)
public class HolderAccountProjection {
	(...중략...)
	private HolderAccountSummary getHolderAccountSummary(String holderID) {
        log.debug(&quot;getHolder : {} &quot;,holderID);
        return repository.findByHolderId(holderID)
                .orElseThrow(() -&amp;gt; new NoSuchElementException(&quot;소유주가 존재하지 않습니다.&quot; + holderID));
    }
}    &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드는 Account 생성 Event를 처리하기 위해 Repository 에서 Holder를 찾는 로직을 일부 발췌하였습니다. 이때 두 Aggregate가 다르므로 Account&amp;nbsp; 생성 시점 Thread 1번에서 수행중인 Holder Aggregate가 DB에 반영되어있지 않을 수 있습니다. 그 결과 &lt;b&gt;NoSuchElementException&lt;/b&gt;이 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Replay를 수행할 때 의도적으로 &lt;b&gt;@DisallowReplay &lt;/b&gt;어노테이션을 추가하지 않는 이상 EventHandler에서 Event 누락이 발생되어서는 안됩니다. 따라서 문제점 해결을 위해 데모 프로젝트에서는 저장소 검색 과정에서 위와 같은 에러를 만나게 되었을 때 약간의 시차를 두고 다시 시도하게끔 지정하고자 합니다. 이를 위해 &lt;span&gt;&lt;b&gt;spring-retry&lt;/b&gt; 기능을 사용하도록 하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;1. Query 모듈 &lt;b&gt;build.gradle&lt;/b&gt; 파일을 열어 dependencies를 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;build.gradle&lt;/p&gt;
&lt;pre id=&quot;code_1578493586293&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies{
 (...중략...)
 implementation group: 'org.springframework.retry', name: 'spring-retry'
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;2. projection 패키지 HolderAccountProjection 클래스 파일을 엽니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;projection 구현.png&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sa7Hy/btqA2a0lNX6/lkJ8wyZ1eUMKbQJm8EdqKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sa7Hy/btqA2a0lNX6/lkJ8wyZ1eUMKbQJm8EdqKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sa7Hy/btqA2a0lNX6/lkJ8wyZ1eUMKbQJm8EdqKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsa7Hy%2FbtqA2a0lNX6%2FlkJ8wyZ1eUMKbQJm8EdqKK%2Fimg.png&quot; data-filename=&quot;projection 구현.png&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;418&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;3. &lt;span style=&quot;color: #333333;&quot;&gt;HolderAccountProjection 클래스 상단에 &lt;b&gt;@EnableRetry&lt;/b&gt; 어노테이션을 추가합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;HolderAccountProjection.java&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1578493755144&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@EnableRetry
@AllArgsConstructor
@Slf4j
@ProcessingGroup(&quot;accounts&quot;)
public class HolderAccountProjection {
	(...중략...)
}    &lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;4. &lt;span style=&quot;color: #333333;&quot;&gt;Account 생성 이벤트를 처리하는 Event Handler 메소드에 &lt;b&gt;@Retryable&lt;/b&gt; 어노테이션을 추가합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;HolderAccountProjection.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1578493858517&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@EnableRetry
@AllArgsConstructor
@Slf4j
@ProcessingGroup(&quot;accounts&quot;)
public class HolderAccountProjection {
	(...중략...)
    
    @EventHandler
    @Retryable(value = {NoSuchElementException.class}, maxAttempts = 5, backoff = @Backoff(delay = 1000))
    @AllowReplay
    protected void on(AccountCreationEvent event, @Timestamp Instant instant)  {
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setAccountCnt(holderAccount.getAccountCnt()+1);
        repository.save(holderAccount);
    }
    
    (...중략...)
}    &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드의 내용은 Repository로부터 NoSuchElementException이 발생하면 1초 대기후에 다시 시도하며, 최대 5번까지 재수행을 시도하도록 설정하였습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 4단계를 거친 다음 Query App을 기동하여 Reset을 수행하면, 발생하였던 문제가 정상적으로 수행됨을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p style=&quot;font-size: 1.12em;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3-3. Batch 최적화&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Read Model 구현에 있어 JPA를 사용한다면 Batch Update를 수행할 때 자동으로 최적화를 수행합니다. 가령 동일한 Record에 대하여 Update가 연속 두번 발생하면, 실제로는 Entitiy 로딩하기 위한 &lt;span style=&quot;color: #333333;&quot;&gt;1번의 &lt;/span&gt;DBMS Call과 JPA 내부 Persistence Context에서 2번의 수정 작업을 거쳐 최종 1번의 Update DBMS Call이 발생합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또한 다량의 Insert 작업이 중간에 Update 작업 없이 발생한다면, DBMS에 Insert를 반영할 때 SQL 단건씩 DBMS Call을 발생시키는 것이 아니라 Bulk Insert를 수행하여 최적화를 달성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 JPA를 통한 Model 이외에도 적용할 수 있는 최적화 방법은 여러가지가 있습니다. 그 중 2가지 방법을 소개하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Entitiy에 대해서 Update를 수행하기 위해서는 기본적으로 데이터를 Persistence Context에 Load 이후에 변경을 실시하는데, Replay 과정에서는 굳이 데이터를 Load할 필요없이 바로 Update 구문을 수행함으로써 DBMS Call을 줄일 수 있습니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;동일 Aggregate에 대해서 발생하는 Insert, Update, delete Event 순서를 결과에 어긋나지 않게 재배치하여 Bulk 작업을 수행한다면, 획기적으로 DBMS Call을 줄일 수 있습니다.&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;방금 소개시켜드린 2가지 방법은 단순히 Configuration 설정 변경으로는 적용할 수 없습니다. 따라서 이를 해결 하기위해서는 DBMS 최적화에 대한 기술적인 고민과 이를 적용하는 내용을 프로그래밍해야 합니다. Axon 에서는 이를 보조하기 위하여 다양한 API를 제공합니다. 또한 각각의 Batch 마다 하나씩 &lt;a href=&quot;https://docs.axoniq.io/reference-guide/configuring-infrastructure-components/messaging-concepts/unit-of-work&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;UnitOfWork&lt;/u&gt;&lt;/a&gt;가&amp;nbsp;존재합니다. 따라서 EventHandler 메소드 파라미터에 UnitOfWork 인자를 추가하면 UnitOfWork에서 처리되는 자원에 접근할 수 있습니다. 이를 통해 메시징 처리를 사용자가 효율적으로 Customizing 처리할 수 있도록 도움을 줍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 소개드린 2가지 기법들을 활용하여 Axon 에서 벤치마크 테스트한 결과, 기존 초당 15000개의 처리량에서 30000개로 2배의 성능 향상을 이룰 수 있었습니다. 이는 최초대비 &lt;b&gt;115배&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;4. Mongo DB 테스트 결과&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Read Model을 Mongo DB를 사용햇을 때 이전 Replay 최적화 방식을 도입함에 있어 AxonIQ 벤치마크 테스트 결과는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;최적화 없음 : 초당 12000개&lt;/li&gt;
&lt;li&gt;Batch 최적화 : 초당 30000개&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonIQ 벤치마크 결과 Postgresql과 크게 다르지 않은 처리량을 볼 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 마치며&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 시간에는 Replay 성능 개선에 대하여 다루었습니다. 개인적으로 EventSourcing &amp;amp; CQRS를 도입하는데 있어 기술적으로 가장 고민을 많이해야하는 부분이 이번 포스팅 내용인 것 같습니다. Replay 수행에 있어 Application 개선 뿐만 아니라 DBMS 최적화 기법을 같이 고려한다면 더 큰 성능개선이 이루어질 수 있으므로 Read Model DB에 대한 지식은 큰 도움이 될 것입니다. 다음 시간에는 요구사항 변경에 따른 Event 모델 수정 및 Versioning에 대하여 다루겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>Axon Server</category>
      <category>Axon 프레임워크</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>Event sourcing</category>
      <category>MSA</category>
      <category>마이크로서비스아키텍처</category>
      <category>이벤트소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/17</guid>
      <comments>https://cla9.tistory.com/17#entry17comment</comments>
      <pubDate>Thu, 9 Jan 2020 00:22:52 +0900</pubDate>
    </item>
    <item>
      <title>12. Query 어플리케이션 구현(Event) - 2</title>
      <link>https://cla9.tistory.com/16</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 시간에는 Query App의 EventHandler 로직을 구현하도록 하겠습니다. Query App의 Read Model은 &lt;a href=&quot;https://cla9.tistory.com/6&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;이전 포스팅&lt;/u&gt;&lt;/a&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;h4 data-ke-size=&quot;size20&quot;&gt;2. Projection 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Command에서 발생된 Event를 적용하는 과정을 &lt;b&gt;Projection&lt;/b&gt;이라 합니다. 먼저 &lt;span style=&quot;color: #333333;&quot;&gt;설계 단계에서 도출한 &lt;/span&gt;Entity 구현 이후 Projection을 구현하겠습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;모델.png&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;557&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DaI8R/btqAY7v1j1B/Mjgzk72LNj2VC13yiqb9T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DaI8R/btqAY7v1j1B/Mjgzk72LNj2VC13yiqb9T1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DaI8R/btqAY7v1j1B/Mjgzk72LNj2VC13yiqb9T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDaI8R%2FbtqAY7v1j1B%2FMjgzk72LNj2VC13yiqb9T1%2Fimg.png&quot; data-filename=&quot;모델.png&quot; data-origin-width=&quot;903&quot; data-origin-height=&quot;557&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. &lt;span style=&quot;color: #000000;&quot;&gt;Query 모듈내&lt;span&gt;&lt;b&gt; entity&lt;/b&gt; &lt;/span&gt;&lt;/span&gt;&lt;b&gt;패키지&lt;/b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span&gt;를 생성후 Entity 클래스를&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;생성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;HolderAccountSummary.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYlk2n/btqAXh7GgkD/1KEJsU5ewf4fDUHhEUBPO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYlk2n/btqAXh7GgkD/1KEJsU5ewf4fDUHhEUBPO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYlk2n/btqAXh7GgkD/1KEJsU5ewf4fDUHhEUBPO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYlk2n%2FbtqAXh7GgkD%2F1KEJsU5ewf4fDUHhEUBPO1%2Fimg.png&quot; data-filename=&quot;HolderAccountSummary.png&quot; data-origin-width=&quot;499&quot; data-origin-height=&quot;375&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. Entity 클래스를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAccountSummary.java&lt;/p&gt;
&lt;pre id=&quot;code_1578396927755&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;MV_ACCOUNT&quot;)
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter @Setter
public class HolderAccountSummary {
    @Id
    @Column(name = &quot;holder_id&quot;, nullable = false)
    private String holderId;
    @Column(nullable = false)
    private String name;
    @Column(nullable = false)
    private String tel;
    @Column(nullable = false)
    private String address;
    @Column(name = &quot;total_balance&quot;, nullable = false)
    private Long totalBalance;
    @Column(name = &quot;account_cnt&quot;, nullable = false)
    private Long accountCnt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. repository 패키지를 생성 후 entitiy &lt;b&gt;repository&lt;/b&gt; 인터페이스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;repository.png&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sKcfb/btqAYHYEGfp/4jDOkUPPBX0K1AA5HyEklk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sKcfb/btqAYHYEGfp/4jDOkUPPBX0K1AA5HyEklk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sKcfb/btqAYHYEGfp/4jDOkUPPBX0K1AA5HyEklk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsKcfb%2FbtqAYHYEGfp%2F4jDOkUPPBX0K1AA5HyEklk%2Fimg.png&quot; data-filename=&quot;repository.png&quot; data-origin-width=&quot;501&quot; data-origin-height=&quot;464&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;4. repository 인터페이스 메소드를 정의합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountRepository.java&lt;/p&gt;
&lt;pre id=&quot;code_1578397198518&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface AccountRepository extends JpaRepository&amp;lt;HolderAccountSummary,String&amp;gt; {
    Optional&amp;lt;HolderAccountSummary&amp;gt; findByHolderId(String holderId);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;5. projection 패키지 생성 후 &lt;b&gt;projection&lt;/b&gt; 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;projection 구현.png&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZ2E5Z/btqAYHRSw6g/7nAjgKkJXhnUkPKOkVBcz1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZ2E5Z/btqAYHRSw6g/7nAjgKkJXhnUkPKOkVBcz1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZ2E5Z/btqAYHRSw6g/7nAjgKkJXhnUkPKOkVBcz1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZ2E5Z%2FbtqAYHRSw6g%2F7nAjgKkJXhnUkPKOkVBcz1%2Fimg.png&quot; data-filename=&quot;projection 구현.png&quot; data-origin-width=&quot;498&quot; data-origin-height=&quot;418&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;6. Projection 클래스를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAccountProjection.java&lt;/p&gt;
&lt;pre id=&quot;code_1578397407280&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@AllArgsConstructor
@Slf4j
public class HolderAccountProjection {
    private final AccountRepository repository;

    @EventHandler
    protected void on(HolderCreationEvent event, @Timestamp Instant instant) {
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary accountSummary = HolderAccountSummary.builder()
                                                                        .holderId(event.getHolderID())
                                                                        .name(event.getHolderName())
                                                                        .address(event.getAddress())
                                                                        .tel(event.getTel())
                                                                        .totalBalance(0L)
                                                                        .accountCnt(0L)
                                                                    .build();
        repository.save(accountSummary);
    }
    
    @EventHandler
    protected void on(AccountCreationEvent event, @Timestamp Instant instant){
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setAccountCnt(holderAccount.getAccountCnt()+1);
        repository.save(holderAccount);
    }
    
    @EventHandler
    protected void on(DepositMoneyEvent event, @Timestamp Instant instant){
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setTotalBalance(holderAccount.getTotalBalance() + event.getAmount());
        repository.save(holderAccount);
    }
    
    @EventHandler
    protected void on(WithdrawMoneyEvent event, @Timestamp Instant instant){
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setTotalBalance(holderAccount.getTotalBalance() - event.getAmount());
        repository.save(holderAccount);
    }

    private HolderAccountSummary getHolderAccountSummary(String holderID) {
        return repository.findByHolderId(holderID)
                .orElseThrow(() -&amp;gt; new NoSuchElementException(&quot;소유주가 존재하지 않습니다.&quot;));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드는 Projection 로직을 담고 있습니다. EventHandler 메소드 파라미터에는 &lt;b&gt;@Timestamp&lt;/b&gt; 외 &lt;b&gt;@SequenceNumber&lt;/b&gt;, &lt;b&gt;ReplayStatus&lt;/b&gt; 등이 추가로 전달될 수 있으며, 자세한 내용은 &lt;a href=&quot;https://docs.axoniq.io/reference-guide/implementing-domain-logic/event-handling/handling-events&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Axon 공식 문서&lt;/u&gt;&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 테스트&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;API 테스트를 통해 발행된 Command가 Read Model에 제대로 반영되는지 테스트 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. reousrces 하위 &lt;b&gt;application.yml&lt;/b&gt; 파일을 오픈 후에 로깅 정보를 입력합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;로그 설정.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K8AAs/btqAXQaHmjN/2J7KzhwKYrcX0MmkxqQSx0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K8AAs/btqAXQaHmjN/2J7KzhwKYrcX0MmkxqQSx0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K8AAs/btqAXQaHmjN/2J7KzhwKYrcX0MmkxqQSx0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK8AAs%2FbtqAXQaHmjN%2F2J7KzhwKYrcX0MmkxqQSx0%2Fimg.png&quot; data-filename=&quot;로그 설정.png&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;444&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. AxonServer 기동 후에 Query App을 수행합니다. 기동이 완료되면 DB에 테이블이 정상 생성되었는지 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;테이블.png&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;215&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dD5NIl/btqAVH0iTfD/V2EwhE27FgDwTPzfGwB4nK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dD5NIl/btqAVH0iTfD/V2EwhE27FgDwTPzfGwB4nK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dD5NIl/btqAVH0iTfD/V2EwhE27FgDwTPzfGwB4nK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdD5NIl%2FbtqAVH0iTfD%2FV2EwhE27FgDwTPzfGwB4nK%2Fimg.png&quot; data-filename=&quot;테이블.png&quot; data-origin-width=&quot;368&quot; data-origin-height=&quot;215&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;3. Command App을 기동합니다. 이후 &lt;b&gt;계정 생성, 계좌 생성, 입금, 출금&lt;/b&gt; Command 명령을 수차례 반복 수행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;API.png&quot; data-origin-width=&quot;505&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XJ07U/btqAVIdRIHk/uLvEKcnAVSURkzTgLjWIcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XJ07U/btqAVIdRIHk/uLvEKcnAVSURkzTgLjWIcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XJ07U/btqAVIdRIHk/uLvEKcnAVSURkzTgLjWIcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXJ07U%2FbtqAVIdRIHk%2FuLvEKcnAVSURkzTgLjWIcK%2Fimg.png&quot; data-filename=&quot;API.png&quot; data-origin-width=&quot;505&quot; data-origin-height=&quot;344&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;4. 테스트 이후 Query App Read Model 갱신 여부를 확인합니다. 또한 Application Log를 통해서 Event 정상 처리를 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;결과.png&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYmx3H/btqAY6Yftav/lggM8ekyf3aLJK6tmxmbdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYmx3H/btqAY6Yftav/lggM8ekyf3aLJK6tmxmbdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYmx3H/btqAY6Yftav/lggM8ekyf3aLJK6tmxmbdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbYmx3H%2FbtqAY6Yftav%2FlggM8ekyf3aLJK6tmxmbdK%2Fimg.png&quot; data-filename=&quot;결과.png&quot; data-origin-width=&quot;653&quot; data-origin-height=&quot;216&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1578399559886&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;c.c.q.p.HolderAccountProjection          : projecting HolderCreationEvent(holderID=0df539ed-2ee2-41be-af32-0f6724a75da3, holderName=kevin, tel=02-1234-5678, address=OO시 OO구) , timestamp : 2020-01-07T12:11:21.047Z
c.c.q.p.HolderAccountProjection          : projecting HolderCreationEvent(holderID=a5d267c6-fbd8-4f0d-b93b-03a8dbf09747, holderName=bruce, tel=02-5291-5678, address=OO시 OO구) , timestamp : 2020-01-07T12:12:14.238Z
c.c.q.p.HolderAccountProjection          : projecting AccountCreationEvent(holderID=0df539ed-2ee2-41be-af32-0f6724a75da3, accountID=0e6d3546-4163-4083-bf2f-50f1289d8c25) , timestamp : 2020-01-07T12:12:27.633Z
c.c.q.p.HolderAccountProjection          : projecting AccountCreationEvent(holderID=0df539ed-2ee2-41be-af32-0f6724a75da3, accountID=042937fb-e658-441d-b23a-fd56be237563) , timestamp : 2020-01-07T12:12:33.961Z
c.c.q.p.HolderAccountProjection          : projecting AccountCreationEvent(holderID=a5d267c6-fbd8-4f0d-b93b-03a8dbf09747, accountID=9eb1f188-c38f-401d-b212-3c26ea84acfa) , timestamp : 2020-01-07T12:12:45.193Z
c.c.q.p.HolderAccountProjection          : projecting AccountCreationEvent(holderID=0df539ed-2ee2-41be-af32-0f6724a75da3, accountID=40d52cac-20d0-49c2-b973-049bd585108f) , timestamp : 2020-01-07T12:13:28.542Z
c.c.q.p.HolderAccountProjection          : projecting DepositMoneyEvent(holderID=0df539ed-2ee2-41be-af32-0f6724a75da3, accountID=40d52cac-20d0-49c2-b973-049bd585108f, amount=300) , timestamp : 2020-01-07T12:15:02.059Z
c.c.q.p.HolderAccountProjection          : projecting WithdrawMoneyEvent(holderID=0df539ed-2ee2-41be-af32-0f6724a75da3, accountID=40d52cac-20d0-49c2-b973-049bd585108f, amount=30) , timestamp : 2020-01-07T12:15:08.242Z
c.c.q.p.HolderAccountProjection          : projecting WithdrawMoneyEvent(holderID=0df539ed-2ee2-41be-af32-0f6724a75da3, accountID=40d52cac-20d0-49c2-b973-049bd585108f, amount=20) , timestamp : 2020-01-07T12:15:13.488Z
c.c.q.p.HolderAccountProjection          : projecting DepositMoneyEvent(holderID=a5d267c6-fbd8-4f0d-b93b-03a8dbf09747, accountID=9eb1f188-c38f-401d-b212-3c26ea84acfa, amount=300) , timestamp : 2020-01-07T12:15:29.451Z
c.c.q.p.HolderAccountProjection          : projecting WithdrawMoneyEvent(holderID=a5d267c6-fbd8-4f0d-b93b-03a8dbf09747, accountID=9eb1f188-c38f-401d-b212-3c26ea84acfa, amount=150) , timestamp : 2020-01-07T12:15:38.423Z&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 내역은 전체 로그중 일부만 발췌한 결과입니다. 확인 결과 Event가 정상적으로 반영된 것을 알 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Replay&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;replay.png&quot; data-origin-width=&quot;1037&quot; data-origin-height=&quot;546&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ds3Zqu/btqAXQIyRto/wC1EeliJ5zWMpc3zycxiiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ds3Zqu/btqAXQIyRto/wC1EeliJ5zWMpc3zycxiiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ds3Zqu/btqAXQIyRto/wC1EeliJ5zWMpc3zycxiiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fds3Zqu%2FbtqAXQIyRto%2FwC1EeliJ5zWMpc3zycxiiK%2Fimg.png&quot; data-filename=&quot;replay.png&quot; data-origin-width=&quot;1037&quot; data-origin-height=&quot;546&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cla9.tistory.com/15&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;이전 포스팅&lt;/u&gt;&lt;/a&gt;에서 마지막 수신 Event Token 정보를 토대로 AxonSever으로부터 다음 목록을 수신받아 처리한다고 설명 했습니다. 하지만 때로는 Read Model 구조를 재구성하기 위해서 Event를 재생해야될 수 있습니다. Axon에서는 이를 위해 Replay 기능을 제공하며, 특정 시점부터 혹은 전체의 Event에 대한 Replay를 수행할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Replay 기능이 동작하면, 내부적으로는 &lt;b&gt;Token 정보를 초기화&lt;/b&gt;하여, 특정 시점 혹은 처음부터 발행된 Event를 전달받아 재수행합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 시간에는 전체 Event를 재생하는 방법에 대해서 설명하며, 특정 시점부터 이벤트 Replay는 &lt;a href=&quot;https://docs.axoniq.io/reference-guide/configuring-infrastructure-components/event-processing/event-processors&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Axon 공식 문서&lt;/u&gt;&lt;/a&gt;를 참고 바랍니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Replay를 수행하기 위해 Projection 클래스를 변경합니다. 먼저 &lt;b&gt;ProcessingGroup&lt;/b&gt;을 지정하여 TrackingEventProcessor로 하여금 어떤 Group을 대상으로 Replay를 수행할지 지정합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAccountProjection.java&lt;/p&gt;
&lt;pre id=&quot;code_1578400391240&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@AllArgsConstructor
@Slf4j
@ProcessingGroup(&quot;accounts&quot;)
public class HolderAccountProjection {
(...중략...)
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;2. Replay 수행시 Read Model 초기화 작업이 이루어지지 않으면, 남아있는 데이터에 이벤트가 적용되므로 데이터 정합성이 맞지 않습니다. 따라서 Replay가 수행되기전, 대상 테이블 또한 초기화가 선행 되어야합니다. 이를 위해 &lt;b&gt;@ResetHandler&lt;/b&gt; 어노테이션을 추가한 메소드를 정의하여 초기화 작업을 처리합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;HolderAccountProjection.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1578401240918&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@AllArgsConstructor
@Slf4j
@ProcessingGroup(&quot;accounts&quot;)
public class HolderAccountProjection {
(...중략...)
    @ResetHandler
    private void resetHolderAccountInfo(){
        log.debug(&quot;reset triggered&quot;);
        repository.deleteAll();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;3. Replay시 적용 대상 Handler 메소드에 &lt;span&gt;&lt;b&gt;@AllowReplay&lt;/b&gt; 어노테이션을 추가합니다.(Optional) 만약 Replay 대상에서 해당 Event는 처리하고 싶지 않을 경우에는 &lt;b&gt;@DisallowReplay&lt;/b&gt;를 추가합니다. 예제에서는 전체 Replay 재생을 위해 @AllowReplay 어노테이션만 추가했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;1~3번 과정을 모두 적용한 Projection 클래스 코드는 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span&gt;HolderAccountProjection.java&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1578401518369&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@AllArgsConstructor
@Slf4j
@ProcessingGroup(&quot;accounts&quot;)
public class HolderAccountProjection {
    private final AccountRepository repository;

    @EventHandler
    @AllowReplay
    protected void on(HolderCreationEvent event, @Timestamp Instant instant) {
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary accountSummary = HolderAccountSummary.builder()
                                                                        .holderId(event.getHolderID())
                                                                        .name(event.getHolderName())
                                                                        .address(event.getAddress())
                                                                        .tel(event.getTel())
                                                                        .totalBalance(0L)
                                                                        .accountCnt(0L)
                                                                    .build();
        repository.save(accountSummary);
    }
    @EventHandler
    @AllowReplay
    protected void on(AccountCreationEvent event, @Timestamp Instant instant){
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setAccountCnt(holderAccount.getAccountCnt()+1);
        repository.save(holderAccount);
    }
    @EventHandler
    @AllowReplay
    protected void on(DepositMoneyEvent event, @Timestamp Instant instant){
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setTotalBalance(holderAccount.getTotalBalance() + event.getAmount());
        repository.save(holderAccount);
    }
    @EventHandler
    @AllowReplay
    protected void on(WithdrawMoneyEvent event, @Timestamp Instant instant){
        log.debug(&quot;projecting {} , timestamp : {}&quot;, event, instant.toString());
        HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID());
        holderAccount.setTotalBalance(holderAccount.getTotalBalance() - event.getAmount());
        repository.save(holderAccount);
    }

    private HolderAccountSummary getHolderAccountSummary(String holderID) {
        return repository.findByHolderId(holderID)
                .orElseThrow(() -&amp;gt; new NoSuchElementException(&quot;소유주가 존재하지 않습니다.&quot;));
    }

    @ResetHandler
    private void resetHolderAccountInfo(){
        log.debug(&quot;reset triggered&quot;);
        repository.deleteAll();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;4. Reset 수행 EndPoint를 구현하기 위해 &lt;b&gt;controller&lt;/b&gt; 패키지 및 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;컨트롤러.png&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;422&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cN4U0Y/btqAYoZltFs/hwAUv3EKKLFOzD7Sw3p51K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cN4U0Y/btqAYoZltFs/hwAUv3EKKLFOzD7Sw3p51K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cN4U0Y/btqAYoZltFs/hwAUv3EKKLFOzD7Sw3p51K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcN4U0Y%2FbtqAYoZltFs%2FhwAUv3EKKLFOzD7Sw3p51K%2Fimg.png&quot; data-filename=&quot;컨트롤러.png&quot; data-origin-width=&quot;502&quot; data-origin-height=&quot;422&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;5. controller 클래스를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAccountController.java&lt;/p&gt;
&lt;pre id=&quot;code_1578401699661&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class HolderAccountController {
    private final QueryService queryService;

    @PostMapping(&quot;/reset&quot;)
    public void reset() {
        queryService.reset();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;6. Service Package 및 Service 클래스를 생성합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;서비스.png&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;592&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bo8ndI/btqA6D92YYs/PHkpreBoEn6rxqeSl63Po1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bo8ndI/btqA6D92YYs/PHkpreBoEn6rxqeSl63Po1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bo8ndI/btqA6D92YYs/PHkpreBoEn6rxqeSl63Po1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbo8ndI%2FbtqA6D92YYs%2FPHkpreBoEn6rxqeSl63Po1%2Fimg.png&quot; data-filename=&quot;서비스.png&quot; data-origin-width=&quot;666&quot; data-origin-height=&quot;592&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;7. 서비스 클래스를 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;QueryService.java&lt;/p&gt;
&lt;pre id=&quot;code_1578830648187&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface QueryService {
    void reset();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;QueryServiceImpl.java&lt;/p&gt;
&lt;pre id=&quot;code_1578830675068&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
@Service
public class QueryServiceImpl implements QueryService{
    private final Configuration configuration;

    @Override
    public void reset() {
        configuration.eventProcessingConfiguration()
                .eventProcessorByProcessingGroup(&quot;accounts&quot;,
                        TrackingEventProcessor.class)
                .ifPresent(trackingEventProcessor -&amp;gt; {
                    trackingEventProcessor.shutDown();
                    trackingEventProcessor.resetTokens(); // (1)
                    trackingEventProcessor.start();
                });
    }
}    &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;실제 Token 초기화는 &lt;b&gt;resetTokens&lt;/b&gt; 메소드를 통해서 이루어집니다. 해당 작업을 위해서는 EventProcessor의 재시작이 필요합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;8. Query App 재기동 후 API 테스트(POST : &lt;span&gt;&lt;b&gt;http://localhost:9090/reset&lt;/b&gt;)&lt;/span&gt;를 수행합니다. Application 로그 및 DB를 확인하면 정상적으로 Replay가 이루어졌음을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 마치며&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;EventHandler 메소드 적용에 따른 Read Model 구현을 완성했습니다. 다음 포스팅에서는 Replay 성능 개선 방법에 대하여 다루도록 하겠습니다.&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>Axon Server</category>
      <category>Axon 프레임워크</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>Event sourcing</category>
      <category>MSA</category>
      <category>마이크로서비스아키텍처</category>
      <category>이벤트소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/16</guid>
      <comments>https://cla9.tistory.com/16#entry16comment</comments>
      <pubDate>Tue, 7 Jan 2020 22:13:25 +0900</pubDate>
    </item>
    <item>
      <title>11. Query 어플리케이션 구현(Event) - 1</title>
      <link>https://cla9.tistory.com/15</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;개요.png&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;745&quot; width=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EnWqC/btqAYpDW7np/qDdzl71csJWxjGJGl6YfO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EnWqC/btqAYpDW7np/qDdzl71csJWxjGJGl6YfO1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EnWqC/btqAYpDW7np/qDdzl71csJWxjGJGl6YfO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEnWqC%2FbtqAYpDW7np%2FqDdzl71csJWxjGJGl6YfO1%2Fimg.png&quot; data-filename=&quot;개요.png&quot; data-origin-width=&quot;1244&quot; data-origin-height=&quot;745&quot; width=&quot;720&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅부터 Query App 구현을 다루겠습니다. Query App은 Event를 수신받아 Read Model에 반영하는 Projection 작업과 Read Model을 읽는 Query 2가지로 기능이 나뉩니다. 기능별로 실제 구현 코드량은 얼마되지 않지만 알아야 하는 개념이 많으므로 먼저 Event 처리 기능 관련하여 다루고 향후에 Query 기능을 다루겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 내용은 EventHandler 구현 실습전 내부 처리 과정을 살펴보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Event 처리 과정&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Token Store&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스트림.png&quot; data-origin-width=&quot;1371&quot; data-origin-height=&quot;783&quot; width=&quot;580&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oAzUZ/btqATgAHQhr/EVxxfJYk6JoECMLHokGIh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oAzUZ/btqATgAHQhr/EVxxfJYk6JoECMLHokGIh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oAzUZ/btqATgAHQhr/EVxxfJYk6JoECMLHokGIh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoAzUZ%2FbtqATgAHQhr%2FEVxxfJYk6JoECMLHokGIh0%2Fimg.png&quot; data-filename=&quot;스트림.png&quot; data-origin-width=&quot;1371&quot; data-origin-height=&quot;783&quot; width=&quot;580&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cla9.tistory.com/14&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;이전 포스팅&lt;/u&gt;&lt;/a&gt;에서 확인 하였듯이 EventStore에는 EventStream의 내용을 순차적으로 적재합니다. 따라서 수신부에서 지금까지 수신된 Event는 어디까지이며, EvenStore에서 어디서부터 Event를 수신 받아야할지에 대한 정보를 가지고 있어야합니다. 해당 정보를 &lt;b&gt;Token&lt;/b&gt;이라고 하며, Token은 Query App과 연관된 DB 내부에 저장하여 &lt;b&gt;영구&lt;/b&gt;적으로 관리합니다.(Token Store)&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;토큰 구조.png&quot; data-origin-width=&quot;1267&quot; data-origin-height=&quot;234&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dg4mbd/btqARyozUWm/ex5nPRkBdnVTN0l3RJhQDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dg4mbd/btqARyozUWm/ex5nPRkBdnVTN0l3RJhQDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dg4mbd/btqARyozUWm/ex5nPRkBdnVTN0l3RJhQDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdg4mbd%2FbtqARyozUWm%2Fex5nPRkBdnVTN0l3RJhQDK%2Fimg.png&quot; data-filename=&quot;토큰 구조.png&quot; data-origin-width=&quot;1267&quot; data-origin-height=&quot;234&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Token 내용&lt;/p&gt;
&lt;pre id=&quot;code_1578223067239&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;org.axonframework.eventhandling.GlobalSequenceTrackingToken&amp;gt;
  &amp;lt;globalIndex&amp;gt;13&amp;lt;/globalIndex&amp;gt;
&amp;lt;/org.axonframework.eventhandling.GlobalSequenceTrackingToken&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 예시는 TokenStore에 저장된 내용입니다.Token 컬럼에는 지금까지 Tracking된 Event의 Global Sequence 값이 들어있으며, 예시를 통해 13번이 마지막으로 수신된 Event임을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;b&gt;Tracking Event Processor&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;TEP.png&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;680&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAZoWk/btqAU6djRdH/SGvUGJzjoECEo1zsiQfUDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAZoWk/btqAU6djRdH/SGvUGJzjoECEo1zsiQfUDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAZoWk/btqAU6djRdH/SGvUGJzjoECEo1zsiQfUDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAZoWk%2FbtqAU6djRdH%2FSGvUGJzjoECEo1zsiQfUDk%2Fimg.png&quot; data-filename=&quot;TEP.png&quot; data-origin-width=&quot;1370&quot; data-origin-height=&quot;680&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;TokenStore를 통해서 마지막 수신 Event 정보를 알 수 있다면, 해당 정보를 토대로 Event 수신 요청 및 처리를 담당하는 중계 역할이 필요합니다. Axon에서 제공하는 Event 처리기는 &lt;span style=&quot;color: #333333;&quot;&gt;Subscribing Event Processor(&lt;b&gt;SEP&lt;/b&gt;), &lt;span&gt;Tracking Event Processor(&lt;b&gt;TEP&lt;/b&gt;) 2&lt;/span&gt;&lt;/span&gt;가지가 있습니다. 두 Event 처리기 차이점은 &lt;b&gt;이벤트 발행 쓰레드에서 Event 처리여부&lt;/b&gt;입니다. SEP는 Event 발행 쓰레드에서 Event 또한 처리하며, TEP는 별도 쓰레드에서 처리합니다. TEP를 이해하는 것이 중요하므로 이를 중점적으로 다루겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Tracking Event Processor&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;내부구조.png&quot; data-origin-width=&quot;2465&quot; data-origin-height=&quot;1322&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yCAvg/btqAU6xGgcy/kWVIOU9jd9xe3AV1tNkR41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yCAvg/btqAU6xGgcy/kWVIOU9jd9xe3AV1tNkR41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yCAvg/btqAU6xGgcy/kWVIOU9jd9xe3AV1tNkR41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyCAvg%2FbtqAU6xGgcy%2FkWVIOU9jd9xe3AV1tNkR41%2Fimg.png&quot; data-filename=&quot;내부구조.png&quot; data-origin-width=&quot;2465&quot; data-origin-height=&quot;1322&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 그림은 Query App이 기동될 때 EventProcessor 생성 및 처리 과정 흐름을 간략하게 표현했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Spring DefaultLifecycleProcessor가 Start 명령을 내립니다.&lt;/li&gt;
&lt;li&gt;Axon의 기본 설정을 담당하는 DefaultConfigurer 클래스 Start 메소드가 호출됩니다. 이후 등록된 Handler에게 수행 명령을 내립니다. 기본으로 등록된 Handler는 2개입니다.(&lt;b&gt;EventProcessingModule, EventProcessorInfoConfiguration&lt;/b&gt;)&lt;/li&gt;
&lt;li&gt;EventProcessingInfoConfiguration의 Start 메소드가 호출됩니다.&lt;/li&gt;
&lt;li&gt;내부 로직을 거쳐 ProcessorInfoSource와 EventProcessorControlService를 구동합니다. ProcessorInfoSource는 AxonServer에게 &lt;b&gt;EventProcessor의 현재 상태&lt;/b&gt;를 주기적(Default 500ms)으로 보내는 역할을 담당합니다. EventProcessorContolService는 AxonServer에서 요청시 &lt;b&gt;EventProcessor를 제어&lt;/b&gt;하는 서비스 역할을 담당합니다. EventPRocessorControlService의 실제 로직 수행은 AxonServerConnectionManager 및 EventProcessorController가 담당합니다.&lt;/li&gt;
&lt;li&gt;EventProcessingModule을 구동합니다. 이 과정에서 EventProcessor 생성을 요청합니다.&lt;/li&gt;
&lt;li&gt;TrackingEventProcessor를 생성합니다. 사용자가 별도 속성 정의를 하지 않았으면, DefaultEventProcessor가 생성됩니다. (※ Thread 수 : 1개, 배치 사이즈 : 1, 최대 Thread 수 : Segment 개수, tokenClaim 주기 : 5000ms)&lt;/li&gt;
&lt;li&gt;생성된 TrackingEventProcessor에게 구동을 요청합니다. 내부적으로 AxonThreadFactory에게 &lt;b&gt;WorkerLauncher&lt;/b&gt; 인스턴스에 대한 Thread 생성을 요청합니다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;TrackingEventProcessor가 구동중이면 내부로직이 반복 수행될 수도록 무한 루프로 구성되어 있습니다. 만약 EventStore Segment에 변경된 내역을 확인하여 처리해야한다면, AxonThreadFactory에게 &lt;b&gt;TrackingSegmentWorker&lt;/b&gt; 인스턴스 생성을 요청합니다.&lt;/li&gt;
&lt;li&gt;Segment에 대한 Event 처리(ProcessingLoop)를 요청합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;9번 Segment Event 처리 과정을 자세히 확인하기 위해 순서도를 그리면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;순서도.png&quot; data-origin-width=&quot;1784&quot; data-origin-height=&quot;2010&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uCwFy/btqAWa0HFya/6nKcKbRo3aOIlCVdCfelN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uCwFy/btqAWa0HFya/6nKcKbRo3aOIlCVdCfelN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uCwFy/btqAWa0HFya/6nKcKbRo3aOIlCVdCfelN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuCwFy%2FbtqAWa0HFya%2F6nKcKbRo3aOIlCVdCfelN1%2Fimg.png&quot; data-filename=&quot;순서도.png&quot; data-origin-width=&quot;1784&quot; data-origin-height=&quot;2010&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;EventStream을 오픈합니다.&lt;/li&gt;
&lt;li&gt;EventStream에서 최신 Event가 존재하는지 확인합니다. 존재하지 않는다면 Token 값을 갱신하고 작업을 종료합니다.&lt;/li&gt;
&lt;li&gt;최신 Event를 수신받은 후 해당 App에서 처리가 가능한 Event인지 확인합니다. 만약 처리할 수 없다면 BlackList 등록이 가능한지 확인하고 이를 등록합니다. 이후 해당 Event는 MessageMonitor에 보고한 뒤 무시합니다.&lt;/li&gt;
&lt;li&gt;처리가 가능한 Event라면 &lt;a href=&quot;https://docs.axoniq.io/reference-guide/configuring-infrastructure-components/messaging-concepts/unit-of-work&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;UnitOfWork&lt;/u&gt;&lt;/a&gt;를 수행합니다. 해당 과정에서 Token 값은 자동 갱신합니다. 이후 EventHandler를 찾아 메소드를 수행합니다.&lt;/li&gt;
&lt;li&gt;Event 처리가 완료되면 작업을 종료합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;내부구조는 복잡하지만 이를 한줄로 요약하자면 &quot;&lt;b&gt;주기적으로 TrackingEventProcessor에서 처리가 가능한 Event가 존재하는지 확인 및 처리(UnitOfWork)하고 Token 갱신하는 작업&lt;/b&gt;&quot;으로 정의할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Axon Server 라우팅기능(Event)&lt;/h4&gt;
&lt;p&gt;이번에는 Client가 아닌 AxonServer 입장에서 Event 전달 흐름을 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;이벤트 스트림 요청.png&quot; data-origin-width=&quot;1097&quot; data-origin-height=&quot;499&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8lR7k/btqASzAF5vL/QUKjY7lkfITwRGjsPbdugk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8lR7k/btqASzAF5vL/QUKjY7lkfITwRGjsPbdugk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8lR7k/btqASzAF5vL/QUKjY7lkfITwRGjsPbdugk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8lR7k%2FbtqASzAF5vL%2FQUKjY7lkfITwRGjsPbdugk%2Fimg.png&quot; data-filename=&quot;이벤트 스트림 요청.png&quot; data-origin-width=&quot;1097&quot; data-origin-height=&quot;499&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Query Application을 구동하면 Token 정보를 읽어옵니다. 이후 EventStore에게 자신이 보유한 Token 정보를 알려준 다음 EventStream을 오픈합니다. 위 예시에는 요청당시 EventStore에는 추가로 유입된 Event가 없으므로 Application의 Token값 변경 후 작업을 종료합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;이벤트 전달.png&quot; data-origin-width=&quot;1765&quot; data-origin-height=&quot;499&quot; width=&quot;860&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sySBs/btqAUvEtXmn/iEskKtjnSiv2YcTjs7yM90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sySBs/btqAUvEtXmn/iEskKtjnSiv2YcTjs7yM90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sySBs/btqAUvEtXmn/iEskKtjnSiv2YcTjs7yM90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsySBs%2FbtqAUvEtXmn%2FiEskKtjnSiv2YcTjs7yM90%2Fimg.png&quot; data-filename=&quot;이벤트 전달.png&quot; data-origin-width=&quot;1765&quot; data-origin-height=&quot;499&quot; width=&quot;860&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 Command App으로부터 Event가 추가된다면, 다음 ProcessingLoop 작업에서 해당 Event를 Query App으로 전달합니다. 수신받은 App에서 해당 Event 처리 가능 여부를 확인하는데, 만약 처리하지 못할 경우는 AxonServer에게 BlackList 추가를 요청합니다. 따라서 이후 신규 적재되는 Event에 대해서는 수신받지 않습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;신규 추가.png&quot; data-origin-width=&quot;1097&quot; data-origin-height=&quot;572&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4WtIl/btqAU5yNzUy/Lha0D1CH9ACxZpBognvbR1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4WtIl/btqAU5yNzUy/Lha0D1CH9ACxZpBognvbR1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4WtIl/btqAU5yNzUy/Lha0D1CH9ACxZpBognvbR1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4WtIl%2FbtqAU5yNzUy%2FLha0D1CH9ACxZpBognvbR1%2Fimg.png&quot; data-filename=&quot;신규 추가.png&quot; data-origin-width=&quot;1097&quot; data-origin-height=&quot;572&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;신규 App이 추가로 기동되어 AxonServer에 등록요청한 상태에서 마지막 수신 Event가 2번이라면, AxonServer에서 3번부터 해당 Event를 App으로 전달합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 마치며&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 포스팅에서는 EventHandler 메소드를 수행하기위해서 알아야할 내부 구조를 알아봤습니다. 다음 포스팅에서는 EventHandler 메소드 구현에 대해 다루어보겠습니다.&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>Axon Server</category>
      <category>Axon 프레임워크</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>Event sourcing</category>
      <category>MSA</category>
      <category>마이크로서비스아키텍처</category>
      <category>이벤트소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/15</guid>
      <comments>https://cla9.tistory.com/15#entry15comment</comments>
      <pubDate>Mon, 6 Jan 2020 00:46:50 +0900</pubDate>
    </item>
    <item>
      <title>10. EventStore</title>
      <link>https://cla9.tistory.com/14</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Query App 관련 포스팅을 진행하기 앞서 Event가 저장되는 EventStore에 대하여 알아보고자 합니다. 이번 포스팅에서 다룰 내용은 EventStore를 위한 필요조건, DB 종류에 따른 EventStore 역할 장&amp;middot;단점 그리고 AxonServer EventStore 저장 구조입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. EventStore 필요 조건&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Event를 읽고 쓰는데 있어 EventStore가 기본적으로 갖춰야할 조건을 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1577803685306&quot; class=&quot;html xml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;1. 이벤트는 추가만 가능하고 입력,삭제,수정이 불가능하다.
2. 여러 이벤트가 하나의 트랜잭션 처리가 되어야 한다면, 트랜잭션 단위로 Commit 혹은 Rollback 되어야한다.
3. Commit된 이벤트는 유실되어서는 안된다
4. 발행된 모든 Event 중 Aggregate 별로 데이터를 읽을 수 있어야한다.
5. 모든 이벤트는 삽입된 순서대로 읽기가 가능해야한다.&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 조건은 대부분의 DBMS라면 충족되는 요건입니다. 그 밖에 EventStore를 구성하는데 있어 요구되는 사항은 무엇이 있을까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;1. SnapShot 저장소 지원&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;720&quot; data-origin-height=&quot;1174&quot; data-origin-width=&quot;1567&quot; data-filename=&quot;스냅샷.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNCT0q/btqAOJJQrlC/TYFxjNeJkeqPKkJPLtUhE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNCT0q/btqAOJJQrlC/TYFxjNeJkeqPKkJPLtUhE0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNCT0q/btqAOJJQrlC/TYFxjNeJkeqPKkJPLtUhE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNCT0q%2FbtqAOJJQrlC%2FTYFxjNeJkeqPKkJPLtUhE0%2Fimg.png&quot; width=&quot;720&quot; data-origin-height=&quot;1174&quot; data-origin-width=&quot;1567&quot; data-filename=&quot;스냅샷.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Command App을 구현하면서 Snapshot 필요성을 인지하였습니다. EventStore에서는 특정 시점에 Aggregate별 Sequence 번호에 해당하는 Snapshot을 별도 공간에 적재하며, Event 로드시에 해당 스냅샷 상태와 스냅샷 이후의 Sequence 번호에 해당되는 Event만을 읽을 수 있도록 지원해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p&gt;&lt;b&gt;2. Event Notification 기능&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;720&quot; data-origin-height=&quot;359&quot; data-origin-width=&quot;1376&quot; data-filename=&quot;polling.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R5uVp/btqAOJwfYoN/AosuoTP9KIvZFmv2ZMYkUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R5uVp/btqAOJwfYoN/AosuoTP9KIvZFmv2ZMYkUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R5uVp/btqAOJwfYoN/AosuoTP9KIvZFmv2ZMYkUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR5uVp%2FbtqAOJwfYoN%2FAosuoTP9KIvZFmv2ZMYkUK%2Fimg.png&quot; width=&quot;720&quot; data-origin-height=&quot;359&quot; data-origin-width=&quot;1376&quot; data-filename=&quot;polling.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;EvenetStore가 신규 추가된 Event를 희망하는 App에게 전파하는 역할을 수행하지 못한다면, Subscriber에서 주기적으로 Polling하여 Event 유입이 있는지 확인하는 작업이 필요합니다. 이는 DB 관점에서 I/O 및 Network 트래픽이 증가하는 요인입니다. 일반적인 RDBMS에서는 이벤트 전파기능이 없기 때문에 위 그림과 같이 메시징 처리를 수행해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;720&quot; data-origin-height=&quot;637&quot; data-origin-width=&quot;1376&quot; data-filename=&quot;큐 사용 문제.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GcpFc/btqAQOwQ7sI/ifol4lrjf2beCsFDKiNCJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GcpFc/btqAQOwQ7sI/ifol4lrjf2beCsFDKiNCJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GcpFc/btqAQOwQ7sI/ifol4lrjf2beCsFDKiNCJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGcpFc%2FbtqAQOwQ7sI%2Fifol4lrjf2beCsFDKiNCJ0%2Fimg.png&quot; width=&quot;720&quot; data-origin-height=&quot;637&quot; data-origin-width=&quot;1376&quot; data-filename=&quot;큐 사용 문제.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;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이러한 문제점을 해결하고자 메시지 큐를 사용해서 EventStore에 적재함과 동시에 큐에도 이벤트를 적재하여 전송하는 방법을 생각할 수 있습니다. 하지만 이는 EventStore에 저장과 큐를 통한 Event 전송 시점에 대한 동기화를 보장할 수 없습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;또한, 기타 이유로 EventStore에 이벤트 삽입이 실패하는 경우 메시지 큐를 통해 이미 전달된 이벤트와의 일관성이 깨지게 됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;따라서 EventStore의 가장 이상적인 구조는 EventStore 자체가 &lt;b&gt;Message Bus &lt;/b&gt;역할을 담당하는 것입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style5&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. EventStore 적합성 비교&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;지금부터 소개드리는 내용은 &lt;u&gt;&lt;a href=&quot;https://youtu.be/zUSWsJteRfw&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;AxonIQ Webinar&lt;/a&gt;&lt;/u&gt;를 참고하여 작성하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;1. RDBMS&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;RDBMS 사용의 장점으로는 Transaction에 대한 지원 및 기술적 성숙도가 높다는 점입니다. 또한 오랜시간동안 사용되었으므로 사용자들에게 친숙하며, 제공되는 Tool이 다양합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 가장 큰 문제점은 확장성입니다. 대량의 데이터 처리보다는 데이터 공간 효율화 및 관계를 통한 데이터 정합성 보장 등에 초점이 맞춰져 있습니다. AxonIQ에서 RDBMS를 EventStore로 사용했을 때의 벤치마크 테스트 결과는 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;640&quot; data-origin-height=&quot;773&quot; data-origin-width=&quot;1422&quot; data-filename=&quot;벤치마크 테스트.png&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oIMmy/btqAQmAB0w9/vjskkFI3tJPMj1crQK5WO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oIMmy/btqAQmAB0w9/vjskkFI3tJPMj1crQK5WO0/img.png&quot; data-alt=&quot;출처 : https://youtu.be/zUSWsJteRfw&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oIMmy/btqAQmAB0w9/vjskkFI3tJPMj1crQK5WO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoIMmy%2FbtqAQmAB0w9%2FvjskkFI3tJPMj1crQK5WO0%2Fimg.png&quot; width=&quot;640&quot; data-origin-height=&quot;773&quot; data-origin-width=&quot;1422&quot; data-filename=&quot;벤치마크 테스트.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;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : https://youtu.be/zUSWsJteRfw&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-origin-height=&quot;315&quot; data-origin-width=&quot;511&quot; data-filename=&quot;download.png&quot; width=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWVceY/btqAOJDAzIY/ygFr1nvMjKZxWtcOdNAk8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWVceY/btqAOJDAzIY/ygFr1nvMjKZxWtcOdNAk8k/img.png&quot; data-alt=&quot;출처 : http://www.dbguide.net/db.db?cmd=view&amp;amp;amp;amp;boardUid=148209&amp;amp;amp;amp;boardConfigUid=9&amp;amp;amp;amp;boardIdx=136&amp;amp;amp;amp;boardStep=1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWVceY/btqAOJDAzIY/ygFr1nvMjKZxWtcOdNAk8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWVceY%2FbtqAOJDAzIY%2FygFr1nvMjKZxWtcOdNAk8k%2Fimg.png&quot; data-origin-height=&quot;315&quot; data-origin-width=&quot;511&quot; data-filename=&quot;download.png&quot; width=&quot;520&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 : http://www.dbguide.net/db.db?cmd=view&amp;amp;boardUid=148209&amp;amp;boardConfigUid=9&amp;amp;boardIdx=136&amp;amp;boardStep=1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;결과를 보면 데이터 양이 증가할 수록 처리량이 떨어지는 것을 확인할 수 있습니다. 다양한 요인이 있을 수 있겠지만, 대표적으로는 대용량 데이터를 기준으로 B-Tree 인덱스를 사용하면, 인덱스 Depth가 깊어지기 때문에 지속 발생하는 인덱스 Split과 더불어 수직적 탐색 비용이 증가합니다. 또한 데이터 특성상 인덱스 우측 Block에 Transaction이 집중적으로 몰리기 때문에 Oracle 기준 핫블록으로 인한 Latch 경합이 발생하여 동시성이 크게 저하될 수 있습니다.(Right Growing Index)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 테스트 결과는 RDBMS 테이블 구조 변경 없이 일반적인 Heap 테이블과 B-Tree 인덱스를 기준으로 진행했습니다. 만약 DBMS가 Oracle이라면 Hash 파티셔닝, Reverse 인덱스, IOT(Index Organized Table) 등을 적절히 사용한다면 개선의 여지는 있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;RDBMS는 Event Notification 기능이 없기 때문에 이를 고려해야합니다. 이전에 설명한 Polling 방식을 개선하기 위해서는 테이블 단위 Audit을 고려해볼 수 있습니다. 즉 Audit 결과를 File로 떨어트리고 해당 로그 tail 값을 AxonFramework에서 요구하는 포맷으로 변경한 다음 메시지 큐를 통해 보내는 방법이 있습니다. 혹은 Trigger를 이용하는 방법도 생각해볼 수 있으나 이는 추천하지 않습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Mongo DB&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;매핑.png&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;103&quot; width=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ECkSW/btqAPdLysgk/zb85kizhrsVaWuRPEbUrF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ECkSW/btqAPdLysgk/zb85kizhrsVaWuRPEbUrF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ECkSW/btqAPdLysgk/zb85kizhrsVaWuRPEbUrF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FECkSW%2FbtqAPdLysgk%2Fzb85kizhrsVaWuRPEbUrF0%2Fimg.png&quot; data-filename=&quot;매핑.png&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;103&quot; width=&quot;480&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Mongo DB는 대표적인 NoSQL로써 하나의 이벤트는 하나의 Document에 속하며, 대량의 데이터 처리에 적합합니다. 하지만 &lt;span style=&quot;color: #333333;&quot;&gt;Transaction 지원 &lt;/span&gt;문제점이 있습니다. 최근에 4.2 버전이 Release되어 Multi Document에 대한 Transaction 기능이 추가되었지만, 단일 Node에서는 Transaction이 불가하는 등의 제약사항이 존재합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&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;u&gt;&lt;a href=&quot;https://medium.com/@marchpig/mongodb-multi-document-transactions-d51e047f811d&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;멀티 Document Transaction 사용하기&lt;/a&gt;&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;&lt;u&gt;&lt;a href=&quot;https://medium.com/@marchpig/mongodb-multi-document-transactions-d51e047f811d&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mongo DB Transaction 설명 블로그&lt;/a&gt;&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;&lt;u&gt;&lt;a href=&quot;https://docs.mongodb.com/manual/core/write-operations-atomicity/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Mongo DB 공식 Document&lt;/a&gt;&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;두 번째 문제점은 MongoDB 3.2 버전부터 Storage Engine으로 &lt;b&gt;Wiredtiger&lt;/b&gt;를 기본적으로 사용하고 있습니다. Wiredtiger 저장 방식은 Btree, 컬럼스토어, LSM 방식이 있습니다. 이중 EventStore에 적합한 방식은 Write 작업에 최적화된 LSM 방식이나 MongoDB에는 LSM을 아직 지원하고 있지 않습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;세 번째 문제점은 RDBMS와 마찬가지로 Event Notification 기능이 없기 때문에, Commit 로그 결과 등을 AxonFramework에서 요구하는 포맷으로 변경한 다음 메시지 큐를 통해 보내는 방식을 고려해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로 모든 Document에 대하여 전역적인 Sequence 기능이 기본적으로 제공되지 않는다는 점입니다. 따라서 이를 해결하기 위해서는 직접 함수를 구현해야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&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;a href=&quot;https://webduck.tistory.com/m/52&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Mongo DB 시퀀스 만들기&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style1&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Kafka&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;필터.png&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;458&quot; width=&quot;520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HuiNU/btqARhTIQXI/8jOay9lSkOECuPsrbd8V9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HuiNU/btqARhTIQXI/8jOay9lSkOECuPsrbd8V9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HuiNU/btqARhTIQXI/8jOay9lSkOECuPsrbd8V9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHuiNU%2FbtqARhTIQXI%2F8jOay9lSkOECuPsrbd8V9K%2Fimg.png&quot; data-filename=&quot;필터.png&quot; data-origin-width=&quot;1010&quot; data-origin-height=&quot;458&quot; width=&quot;520&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;카프카는 대용량 환경에서 Message 전달 역할로 좋은선택입니다. 하지만 EventSourcing에 있어서 좋은 도구는 아닙니다. 그 이유는 위 그림과 같이 Event Stream에서 특정 Aggregate를 추출하기 위해서는 해당 Stream 전체를 읽으면서 그 중 내가 원하는 Aggregate만 필터링하는 작업이 수반되어야 하기 때문입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;토픽.png&quot; data-origin-width=&quot;961&quot; data-origin-height=&quot;344&quot; width=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKRAni/btqARh0tqNd/zGNlk2qXcyfQ2etAorFuQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKRAni/btqARh0tqNd/zGNlk2qXcyfQ2etAorFuQ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKRAni/btqARh0tqNd/zGNlk2qXcyfQ2etAorFuQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKRAni%2FbtqARh0tqNd%2FzGNlk2qXcyfQ2etAorFuQ1%2Fimg.png&quot; data-filename=&quot;토픽.png&quot; data-origin-width=&quot;961&quot; data-origin-height=&quot;344&quot; width=&quot;640&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;물론 Aggregate 별로 Topic을 생성하는 방법등도 고려할 수 있으나 이는 Aggregate 별로 디스크에 적재되는 용량과 I/O 밸런스 등을 고려해야하는 등의 관리 포인트가 급격하게 상승합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&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;u&gt;&lt;a href=&quot;https://medium.com/serialized-io/apache-kafka-is-not-for-event-sourcing-81735c3cf5c&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Apache Kafka is not for Event Sourcing&lt;/a&gt;&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Axon Server(Event Store)&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonServer 내부에는 Event 저장을위한 별도 DB가 없으며, File을 직접 다룹니다. 외부와의 연결은 &lt;b&gt;Rest API&lt;/b&gt; 혹은 &lt;b&gt;gRPC&lt;/b&gt; 방법을 통해 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;EventStore는 오직 데이터 추가만이 가능하도록 설계되었습니다. 따라서 수정, 삭제와 관련된 그 어떠한 API도 제공되지 않습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;세그먼트.png&quot; data-origin-width=&quot;367&quot; data-origin-height=&quot;921&quot; width=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/POcNJ/btqAQlbrX3M/KsLsZ0HaqrvGWP47QDGIuk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/POcNJ/btqAQlbrX3M/KsLsZ0HaqrvGWP47QDGIuk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/POcNJ/btqAQlbrX3M/KsLsZ0HaqrvGWP47QDGIuk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPOcNJ%2FbtqAQlbrX3M%2FKsLsZ0HaqrvGWP47QDGIuk%2Fimg.png&quot; data-filename=&quot;세그먼트.png&quot; data-origin-width=&quot;367&quot; data-origin-height=&quot;921&quot; width=&quot;230&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonServer에서는 EventStream을 일정 크기별로 잘라서 Segment로 매핑합니다. 각 Segment는 하나의 파일이며, 내부에는 Event가 연속적으로 할당되어 있습니다. 생성된 파일은 데이터 Corruption 확인을 위해 CRC 체크하여 파일 손상을 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 Segment에 Event Entry가 가득차게되면, 새로운 파일을 생성하고 이를 가르키도록 Index 정보를 추가합니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스냅샷.png&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;816&quot; width=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kGW9J/btqAOKbIUdx/ILynZZUjCmlwpn7Amccezk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kGW9J/btqAOKbIUdx/ILynZZUjCmlwpn7Amccezk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kGW9J/btqAOKbIUdx/ILynZZUjCmlwpn7Amccezk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkGW9J%2FbtqAOKbIUdx%2FILynZZUjCmlwpn7Amccezk%2Fimg.png&quot; data-filename=&quot;스냅샷.png&quot; data-origin-width=&quot;813&quot; data-origin-height=&quot;816&quot; width=&quot;420&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;EventStore는 Snapshot 저장소를 제공합니다. Snapshot 저장소 또한 Segment 단위로 저장되며, Snapshot Entry는 동일한 Aggregate의 번호가 매핑된 파일을 가르킵니다. 따라서 위 그림에서 A Aggregate를 읽는다고 가정한다면 Snapshot이 가르키는 1번 Segment 이후부터 데이터를 읽기 시작합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 이때 2번 Segment에는 A Aggregate Event가 존재하지 않습니다. 따라서 해당 Segment는 읽는 것이 의미가 없습니다. 따라서 스캔 과정에서 2번은 읽지 않고 Skip할 수 있다면 최소한의 I/O로 성능을 높일 수 있을 것입니다. AxonServer에서는 이를 위해 &lt;b&gt;Bloom Filter&lt;/b&gt;를 도입하였습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;b&gt;Bloom Filter&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Bloom Filter는 찾고자 하는 데이터가 해당 집합에 포함되는지를 판단하는 확률적 자료구조입니다. 주로 &lt;b&gt;DBMS&lt;/b&gt;에서 많이 사용하며, 디스크에서 찾고자하는 값이 존재할 가능성이 있는 경우에만 블록을 읽기 위해 사용됩니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를 들어 설명하겠습니다. Bloom Filter는 &lt;b&gt;N&lt;/b&gt;개의 bit 배열에 대해서 찾고자하는 데이터를 대상으로 &lt;b&gt;H&lt;/b&gt;개의 해시 함수를 적용한 결과를 1로 표시한다음, 대상 집합에도 동일하게 H개의 해시 함수를 적용해 결과가 동일한지를 판단합니다. 만약 동일하다면 찾고자 하는 존재할 수도 있으므로 해당 집합을 탐색합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;정답.png&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;371&quot; width=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blNSJl/btqAPeRdlXZ/VbkJa7kEBcBBtgEV4KM6J1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blNSJl/btqAPeRdlXZ/VbkJa7kEBcBBtgEV4KM6J1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blNSJl/btqAPeRdlXZ/VbkJa7kEBcBBtgEV4KM6J1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblNSJl%2FbtqAPeRdlXZ%2FVbkJa7kEBcBBtgEV4KM6J1%2Fimg.png&quot; data-filename=&quot;정답.png&quot; data-origin-width=&quot;661&quot; data-origin-height=&quot;371&quot; width=&quot;420&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;예를들어 1개의 Segment에는 1개의 Event만 존재하고, 4개 bit 배열 및 1개의 해 시함수(mod 10)을 적용한다고 가정하겠습니다. 이때 찾고자하는 Aggregate 식별자는 14라면 위 해시 함수를 적용했을 때 결과는 4가 나옵니다. 또한 저장된 값 또한 14라면 Bloom Filter 및 찾고자 하는 값이 동일하므로 해당 집합에는 원하는 결과가 존재합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;false positive.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;371&quot; width=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDvpNQ/btqAPd5OGje/CUTKYovIAcrkkqCCQUiO50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDvpNQ/btqAPd5OGje/CUTKYovIAcrkkqCCQUiO50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDvpNQ/btqAPd5OGje/CUTKYovIAcrkkqCCQUiO50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDvpNQ%2FbtqAPd5OGje%2FCUTKYovIAcrkkqCCQUiO50%2Fimg.png&quot; data-filename=&quot;false positive.png&quot; data-origin-width=&quot;688&quot; data-origin-height=&quot;371&quot; width=&quot;420&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 만약 집합속에 있는 값이 24라면, 해시 함수 결과 똑같이 4라는 결과가 나옵니다. 이때는 값이 다른데도 불구하고, 값이 있을 수도 있다고 판정하여 해당 집합을 읽습니다. 따라서 이러한 경우는 비효율적인 I/O가 존재하며 이를 &lt;b&gt;false positive&lt;/b&gt;라고 합니다. 따라서 Bloom Filter에서는 false positive를 줄이기 위해서 bit 배열의 수를 늘리는 것과 해시 함수 개수를 늘려서 동일한 값이 발생하지 않도록해야 불필요한 I/O를 유발하지 않습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&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;a href=&quot;https://d2.naver.com/helloworld/749531&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;확률적 자료구조를 이용한 추정&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스킵하여 읽음.png&quot; data-origin-width=&quot;557&quot; data-origin-height=&quot;1157&quot; width=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PEHOF/btqAPewYiKB/kxzjlxuKDpWDsTYdn9PrVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PEHOF/btqAPewYiKB/kxzjlxuKDpWDsTYdn9PrVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PEHOF/btqAPewYiKB/kxzjlxuKDpWDsTYdn9PrVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPEHOF%2FbtqAPewYiKB%2FkxzjlxuKDpWDsTYdn9PrVk%2Fimg.png&quot; data-filename=&quot;스킵하여 읽음.png&quot; data-origin-width=&quot;557&quot; data-origin-height=&quot;1157&quot; width=&quot;280&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Bloom Filter를 통해서 읽어야할 Segment를 알았다면, 해당 Segment에서 시작점을 찾는 것은 Segment 내부에 저장되어있는 &lt;b&gt;인덱스&lt;/b&gt;를 통해서 Aggregate의 Sequence 번호를 찾아갑니다. 결론적으로 Axon Server의 EventStore에서 Aggregate 데이터를 검색할 때, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;해당 Aggregate의 식별자와 Sequence &lt;/b&gt;&lt;/span&gt;번호를 기준으로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Bloom Filter&lt;/b&gt;&lt;/span&gt;와 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;인덱스&lt;/span&gt;&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;h4 data-ke-size=&quot;size20&quot;&gt;5. 마치며&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번 시간에는 EventStore 역할 비교 및 Axon Server 저장소 구조에 대해서 알아보았습니다. 다음 포스팅부터 Query Application 구현에 대하여 다루도록 하겠습니다.&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>Axon Server</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>Event sourcing</category>
      <category>eventstore</category>
      <category>MSA</category>
      <category>마이크로서비스아키텍처</category>
      <category>이벤트 소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/14</guid>
      <comments>https://cla9.tistory.com/14#entry14comment</comments>
      <pubDate>Thu, 2 Jan 2020 21:13:54 +0900</pubDate>
    </item>
    <item>
      <title>9. Command 어플리케이션 구현 - 4</title>
      <link>https://cla9.tistory.com/13</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;이번 포스팅은 데모 프로젝트 진행에 있어 필수적으로 구현해야하는 코드는 없습니다. 따라서 Skip해도 괜찮습니다.&amp;nbsp; 이번 시간에는 상태를 저장하는 State-Stored Aggregate에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Aggregate 종류&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonFramework 에서 Aggregate의 종류는 크게 두 가지로 분류할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;EventSourced Aggregate&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;State-Stored Aggregate&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;EventSourced Aggregate는 기존 Command 어플리케이션을 제작하는 과정에서 구현한 모델 방식입니다. 즉 EventStore로부터 Event를 재생하면서 모델의 최신상태를 만듭니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;CQRS-ES.png&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;413&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1bcbN/btqAObMjJBk/EPcx4L03RxTDgKNQ8CC3uK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1bcbN/btqAObMjJBk/EPcx4L03RxTDgKNQ8CC3uK/img.png&quot; data-alt=&quot;출처 :&amp;amp;amp;nbsp;https://altkomsoftware.pl/en/blog/cqrs-event-sourcing/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1bcbN/btqAObMjJBk/EPcx4L03RxTDgKNQ8CC3uK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1bcbN%2FbtqAObMjJBk%2FEPcx4L03RxTDgKNQ8CC3uK%2Fimg.png&quot; data-filename=&quot;CQRS-ES.png&quot; data-origin-width=&quot;864&quot; data-origin-height=&quot;413&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 :&amp;nbsp;https://altkomsoftware.pl/en/blog/cqrs-event-sourcing/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이와 반대로 State-Stored Aggregate는 위 그림과 같이 EventStore에 Event를 적재와 별개로 모델 자체에 최신 상태를 DB에 저장합니다. 데모 프로젝트 Aggregate 구조 변경을 통해 Command DB에 모델을 생성하는 방법에 대해서 알아봅시다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. State-Stored Aggregate&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;데모 프로젝트에는 Holder와 Account Entitiy가 존재합니다. Command 모델에서는 해당 Entitiy 관계를 분리하여 표현할 것이며 모델 구현은 JPA를 사용하겠습니다. 먼저 Command와 Query DB에 적재될 테이블 구조를 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;ERD&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;ERD.png&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;129&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bguLzd/btqANCwR3be/WsXN6EuMLFCsbXFybfLTv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bguLzd/btqANCwR3be/WsXN6EuMLFCsbXFybfLTv1/img.png&quot; data-alt=&quot;Command 모델&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bguLzd/btqANCwR3be/WsXN6EuMLFCsbXFybfLTv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbguLzd%2FbtqANCwR3be%2FWsXN6EuMLFCsbXFybfLTv1%2Fimg.png&quot; data-filename=&quot;ERD.png&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;129&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Command 모델&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;erd_1.png&quot; data-origin-width=&quot;269&quot; data-origin-height=&quot;163&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/onHWv/btqAM41r6FP/XKXJTEUyDYnhY1kueUR7T1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/onHWv/btqAM41r6FP/XKXJTEUyDYnhY1kueUR7T1/img.png&quot; data-alt=&quot;Query 모델&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/onHWv/btqAM41r6FP/XKXJTEUyDYnhY1kueUR7T1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FonHWv%2FbtqAM41r6FP%2FXKXJTEUyDYnhY1kueUR7T1%2Fimg.png&quot; data-filename=&quot;erd_1.png&quot; data-origin-width=&quot;269&quot; data-origin-height=&quot;163&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Query 모델&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Aggregate 구현&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;State-Stored Aggregate 구현을 위해 기존 코드를 단계적으로 변경하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. HolderAggregate와 AccountAggregate 구조 변경을 통해 상태를 저장할 수 있도록 구현합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAggregate.java&lt;/p&gt;
&lt;pre id=&quot;code_1577631395936&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@AllArgsConstructor
@NoArgsConstructor
@Aggregate
@Slf4j
@Entity(name = &quot;holder&quot;)
@Table(name = &quot;holder&quot;)
public class HolderAggregate {
    @AggregateIdentifier
    @Id
    @Column(name = &quot;holder_id&quot;)
    @Getter
    private String holderID;
    @Column(name = &quot;holder_name&quot;)
    private String holderName;
    private String tel;
    private String address;

    @OneToMany(mappedBy = &quot;holder&quot;, orphanRemoval = true)
    private List&amp;lt;AccountAggregate&amp;gt; accounts = new ArrayList&amp;lt;&amp;gt;();

    public void registerAccount(AccountAggregate account){
        if(!this.accounts.contains(account))
            this.accounts.add(account);
    }
    public void unRegisterAccount(AccountAggregate account){
        this.accounts.remove(account);
    }

    @CommandHandler
    public HolderAggregate(HolderCreationCommand command) {
        log.debug(&quot;handling {}&quot;, command);

        this.holderID = command.getHolderID();
        this.holderName = command.getHolderName();
        this.tel = command.getTel();
        this.address = command.getAddress();

        apply(new HolderCreationEvent(command.getHolderID(), command.getHolderName(), command.getTel(), command.getAddress()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;@Entity&lt;/b&gt; 어노테이션을 통해 대상 Aggregate가 JPA에서 관리되는 Entity임을 명시했습니다. 또한 Aggregate 식별자에 &lt;b&gt;@Id&lt;/b&gt; 어노테이션을 추가하여 대상 속성이 PK임을 표시합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;HolderAggregate는 AccountAggregate와 &lt;b&gt;1:N 관계&lt;/b&gt;를 맺고 있으므로 양방향 관계 설정 했으며, HolderAggregate가 삭제될 경우 AccountAggregate도 삭제되도록 orphanRemovel 옵션을 추가했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;마지막으로 양방향 관계 설정 시 연관관계 편의 메소드 제공을 위해 registerAccount, unRegisterAccount 메소드를 추가했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;(참고 : &lt;a href=&quot;https://yellowh.tistory.com/136&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;양방향 연관관계 편의 메소드)&lt;/u&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;혹시 위 Aggregate 코드에서 JPA 코드 추가 외에 혹시 이상한 점을 눈치채셨나요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;바로 &lt;b&gt;EventSourcingHandler&lt;/b&gt; 메소드가 사라졌습니다. State-Stored Aggregate 모델은 모델 자체가 최신 상태를 유지하고 있으므로 EventStore로부터 Replay를 할 필요가 없습니다. 따라서 &lt;b&gt;CommandHandler 메소드 내에서 Command 상태를 저장하는 로직을 포함&lt;/b&gt;시켜야 합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이번에는 AccountAggreagte 클래스를 변경하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AccountAggregate.java&lt;/p&gt;
&lt;pre id=&quot;code_1577631806352&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@NoArgsConstructor
@AllArgsConstructor
@Slf4j
@Aggregate
@EqualsAndHashCode
@Entity(name = &quot;account&quot;)
@Table(name = &quot;account&quot;)
public class AccountAggregate {
    @AggregateIdentifier
    @Id
    @Column(name = &quot;account_id&quot;)
    private String accountID;

    @ManyToOne
    @JoinColumn(name = &quot;holder_id&quot;, foreignKey = @ForeignKey(name = &quot;FK_HOLDER&quot;))
    private HolderAggregate holder;
    private Long balance;

    public void registerHolder(HolderAggregate holder){
        if(this.holder != null)
            this.holder.unRegisterAccount(this);
        this.holder = holder;
        this.holder.registerAccount(this);
    }

    @CommandHandler
    public AccountAggregate(AccountCreationCommand command) {
        log.debug(&quot;handling {}&quot;, command);
        this.accountID = command.getAccountID();
        HolderAggregate holder = command.getHolder();
        registerHolder(holder);
        this.balance = 0L;
        apply(new AccountCreationEvent(holder.getHolderID(),command.getAccountID()));
    }
    @CommandHandler
    protected void depositMoney(DepositMoneyCommand command){
        log.debug(&quot;handling {}&quot;, command);
        if(command.getAmount() &amp;lt;= 0) throw new IllegalStateException(&quot;amount &amp;gt;= 0&quot;);
        this.balance += command.getAmount();
        log.debug(&quot;balance {}&quot;, this.balance);
        apply(new DepositMoneyEvent(command.getHolderID(), command.getAccountID(), command.getAmount()));
    }
    @CommandHandler
    protected void withdrawMoney(WithdrawMoneyCommand command){
        log.debug(&quot;handling {}&quot;, command);
        if(this.balance - command.getAmount() &amp;lt; 0) throw new IllegalStateException(&quot;잔고가 부족합니다.&quot;);
        else if(command.getAmount() &amp;lt;= 0 ) throw new IllegalStateException(&quot;amount &amp;gt;= 0&quot;);
        this.balance -= command.getAmount();
        log.debug(&quot;balance {}&quot;, this.balance);
        apply(new WithdrawMoneyEvent(command.getHolderID(), command.getAccountID(), command.getAmount()));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;HolderAggregate에서 설명한 부분은 제외하고 추가된 점은&amp;nbsp;&lt;/span&gt;Account 모델이 Holder 모델에 대하여 FK를 지니고 있으므로 관계상에서 FK를 명시하였습니다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2. HolderAggregate 클래스 생성을 위한 Repository 패키지 및 클래스를 생성합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;레파지토리.png&quot; data-origin-width=&quot;469&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m43ef/btqAQODhmYr/pV0jcuMzk9Nbv5YMjJ1lIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m43ef/btqAQODhmYr/pV0jcuMzk9Nbv5YMjJ1lIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m43ef/btqAQODhmYr/pV0jcuMzk9Nbv5YMjJ1lIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm43ef%2FbtqAQODhmYr%2FpV0jcuMzk9Nbv5YMjJ1lIK%2Fimg.png&quot; data-filename=&quot;레파지토리.png&quot; data-origin-width=&quot;469&quot; data-origin-height=&quot;348&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3. HolderRepository 클래스를 구현합니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1577763302578&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Repository
public interface HolderRepository extends JpaRepository&amp;lt;HolderAggregate,String&amp;gt; {
    Optional&amp;lt;HolderAggregate&amp;gt; findHolderAggregateByHolderID(String id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;4. 객체 대상으로 연관관계를 변경하였기 때문에 AccountCreateCommand 클래스를 수정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;코드 수정.png&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;375&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6nJmh/btqAN4tKkIm/J51FcduReMUAklhqENxvkK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6nJmh/btqAN4tKkIm/J51FcduReMUAklhqENxvkK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6nJmh/btqAN4tKkIm/J51FcduReMUAklhqENxvkK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6nJmh%2FbtqAN4tKkIm%2FJ51FcduReMUAklhqENxvkK%2Fimg.png&quot; data-filename=&quot;코드 수정.png&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;375&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;5. Service 클래스를 수정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1577763720607&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class TransactionServiceImpl implements TransactionService {
    private final CommandGateway commandGateway;
    private final HolderRepository holders;

...(중략)...

    @Override
    public CompletableFuture&amp;lt;String&amp;gt; createAccount(AccountDTO accountDTO) {
        HolderAggregate holder = holders.findHolderAggregateByHolderID(accountDTO.getHolderID())
                                       .orElseThrow( () -&amp;gt; new IllegalAccessError(&quot;계정 ID가 올바르지 않습니다.&quot;));
        return commandGateway.send(new AccountCreationCommand(UUID.randomUUID().toString(),holder));
    }

...(중략)...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;6. Snapshot 설정을 위해 생성한 Configuration 속성을 적용하지 않도록 AxonConfig 클래스 @Configuration 어노테이션을 주석처리 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1577763790669&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//@Configuration
//@AutoConfigureAfter(AxonAutoConfiguration.class)
public class AxonConfig {
...(중략)...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이로써 State-Stored Aggregate 구현에 대한 코드 변경은 끝났습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 테스트&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Application 실행 후 계정 생성 API 테스트를 진행합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1577633357295&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST http://localhost:8080/holder
Content-Type: application/json

{
	&quot;holderName&quot; : &quot;kevin&quot;,
	&quot;tel&quot; : &quot;02-1234-5678&quot;,
	&quot;address&quot; : &quot;OO시 OO구&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;2. DB에서 계정 데이터 생성 여부를 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;holder 생성.png&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSrrLt/btqAObTJuJU/4NIK3VGGf2Ydq93mW13VA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSrrLt/btqAObTJuJU/4NIK3VGGf2Ydq93mW13VA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSrrLt/btqAObTJuJU/4NIK3VGGf2Ydq93mW13VA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSrrLt%2FbtqAObTJuJU%2F4NIK3VGGf2Ydq93mW13VA1%2Fimg.png&quot; data-filename=&quot;holder 생성.png&quot; data-origin-width=&quot;658&quot; data-origin-height=&quot;250&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;3. 계좌 생성 API를 호출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1577633469445&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST http://localhost:8080/account
Content-Type: application/json

{
  &quot;holderID&quot; : &quot;486832c2-b606-470d-949a-9f9d8613b112&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4. DB에서 계좌 데이터 생성 여부를 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;계좌 생성.png&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbTNPH/btqAMjSYBRZ/gdpDJdY6yBP0rmgzLCWWd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbTNPH/btqAMjSYBRZ/gdpDJdY6yBP0rmgzLCWWd1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbTNPH/btqAMjSYBRZ/gdpDJdY6yBP0rmgzLCWWd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbTNPH%2FbtqAMjSYBRZ%2FgdpDJdY6yBP0rmgzLCWWd1%2Fimg.png&quot; data-filename=&quot;계좌 생성.png&quot; data-origin-width=&quot;711&quot; data-origin-height=&quot;262&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;5. 입금 API를 호출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1577762328437&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST http://localhost:8080/deposit
Content-Type: application/json

{
  &quot;accountID&quot; : &quot;9274bb43-ca87-4aa4-b1b4-0363382ad6fb&quot;,
  &quot;holderID&quot; : &quot;486832c2-b606-470d-949a-9f9d8613b112&quot;,
  &quot;amount&quot; : 300
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;6. DB에서 해당 계정 잔고를 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;입금 잔고 확인.png&quot; data-origin-width=&quot;715&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddtnpX/btqANDb8Vyd/DYT60KULnKE4btuOgBPTi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddtnpX/btqANDb8Vyd/DYT60KULnKE4btuOgBPTi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddtnpX/btqANDb8Vyd/DYT60KULnKE4btuOgBPTi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddtnpX%2FbtqANDb8Vyd%2FDYT60KULnKE4btuOgBPTi1%2Fimg.png&quot; data-filename=&quot;입금 잔고 확인.png&quot; data-origin-width=&quot;715&quot; data-origin-height=&quot;270&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정상적으로 입금된 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;7. 출금 API를 4번 연속으로 호출합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1577762660013&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;POST http://localhost:8080/withdrawal
Content-Type: application/json

{
  &quot;accountID&quot; : &quot;9274bb43-ca87-4aa4-b1b4-0363382ad6fb&quot;,
  &quot;holderID&quot; : &quot;486832c2-b606-470d-949a-9f9d8613b112&quot;,
  &quot;amount&quot; : 1
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;8. DB에서 출금 내역을 확인합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;출금 잔고.png&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3mM31/btqAOJitbb0/HwvmidTBAGndaTqhuiqmQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3mM31/btqAOJitbb0/HwvmidTBAGndaTqhuiqmQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3mM31/btqAOJitbb0/HwvmidTBAGndaTqhuiqmQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3mM31%2FbtqAOJitbb0%2FHwvmidTBAGndaTqhuiqmQk%2Fimg.png&quot; data-filename=&quot;출금 잔고.png&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;274&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;정상적으로 출금된 것을 확인할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1577763103261&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;o.a.commandhandling.SimpleCommandBus     : Handling command [com.cqrs.command.commands.WithdrawMoneyCommand]
c.c.command.aggregate.AccountAggregate   : handling WithdrawMoneyCommand(accountID=9274bb43-ca87-4aa4-b1b4-0363382ad6fb, holderID=486832c2-b606-470d-949a-9f9d8613b112, amount=1)
c.c.command.aggregate.AccountAggregate   : balance 296
org.axonframework.messaging.Scope        : Clearing out ThreadLocal current Scope, as no Scopes are present
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;4번 연속 출금 후 Application의 로그를 일부 발췌하였습니다. 기존과 다른점은 EventSourced Aggregate의 경우에는 Replay를 위해 EventStore의 I/O 과정이 필요했지만 State-Stored Aggregate는 상태를 보관하므로 상태 복원 과정이 없습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 마치며&lt;/h4&gt;
&lt;p&gt;&lt;b&gt;State-Stored Aggreagte는 Command DB에 최신 상태&lt;/b&gt;를 보관합니다. 이로인해 매번 EventStore를 통해서 Replay를 하지 않아도 되는 점은 장점입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;하지만 만약에 테이블 데이터가 손실이 되어서 복구가 필요한 경우 Replay를 자동으로 수행되지 않으므로 &lt;b&gt;별도로 EventSourcing 하는 작업을 구현&lt;/b&gt;해야 할 수 있습니다. 물론 DBMS 자체 복구 기능을 이용할 수도 있습니다. 하지만 Media Recovery가 불가피하다면 DB 서비스 중단이 발생합니다. 따라서 Aggregate별 사용 장단점을 인지한 다음 적절한 사용이 필요합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;이상으로 Command Application 구현 포스팅 마치도록 하겠습니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>Axon Server</category>
      <category>Axon 프레임워크</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>EventSourcing</category>
      <category>MSA</category>
      <category>마이크로서비스아키텍처</category>
      <category>이벤트 소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/13</guid>
      <comments>https://cla9.tistory.com/13#entry13comment</comments>
      <pubDate>Tue, 31 Dec 2019 12:50:24 +0900</pubDate>
    </item>
    <item>
      <title>8. Command 어플리케이션 구현 - 3</title>
      <link>https://cla9.tistory.com/12</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서론&lt;/h4&gt;
&lt;p&gt;지난 포스팅에서 Command Application에 대한 전반적인 구현을 마무리했습니다. 하지만 해당 프로그램은 근본적인 문제점을 안고 있습니다. 이번 포스팅에서는 발생되는 문제점과 이를 해결하기 위한 방법에 대하여 살펴보겠습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 문제점 도출&lt;/h4&gt;
&lt;p&gt;아래 테이블은 계정 생성 Command를 실행했을 때 EventStore에 저장되는 데이터 중 일부를 발췌한 것입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 0%; height: 74px;&quot; border=&quot;1&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 54px;&quot;&gt;
&lt;td style=&quot;width: 13.1977%; height: 54px;&quot;&gt;글로벌 인덱스&lt;/td&gt;
&lt;td style=&quot;width: 9.94184%; height: 54px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Payload&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.4069%; height: 54px;&quot;&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Payload&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;종류&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 11.9186%; height: 54px;&quot;&gt;발생 시간&lt;/td&gt;
&lt;td style=&quot;width: 20.7559%; height: 54px;&quot;&gt;Aggregate 식별자&lt;/td&gt;
&lt;td style=&quot;width: 12.035%; height: 54px;&quot;&gt;시퀀스 번호&lt;/td&gt;
&lt;td style=&quot;width: 15.9884%; height: 54px;&quot;&gt;타입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 13.1977%; height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 9.94184%; height: 20px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 15.4069%; height: 20px;&quot;&gt;&lt;a href=&quot;com.cqrs.events.HolderCreationEvent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;com.cqrs.events.HolderCreationEvent&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.9186%; height: 20px;&quot;&gt;2019-12-29T05:34:49.2527378Z&lt;/td&gt;
&lt;td style=&quot;width: 20.7559%; height: 20px;&quot;&gt;70f956e3-069c-4666-b0f4-324dfb0a807e&lt;/td&gt;
&lt;td style=&quot;width: 12.035%; height: 20px;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 15.9884%; height: 20px;&quot;&gt;HolderAggregate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;공간 부족으로 위 데이터에서 Payload 데이터만 따로 뽑아보면 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1577599113455&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;com.cqrs.events.HolderCreationEvent&amp;gt;
  &amp;lt;holderID&amp;gt;70f956e3-069c-4666-b0f4-324dfb0a807e&amp;lt;/holderID&amp;gt;
  &amp;lt;holderName&amp;gt;kevin&amp;lt;/holderName&amp;gt;
  &amp;lt;tel&amp;gt;02-1234-5678&amp;lt;/tel&amp;gt;
  &amp;lt;address&amp;gt;OO시 OO구/address&amp;gt;
&amp;lt;/com.cqrs.events.HolderCreationEvent&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Payload에는 Event 내용이 담겨있습니다. 따라서 EventSourcing 및 Event Handler에서는 Payload 내용을 기준으로 Event를 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;계정 생성만 완료된 상황에서 추가로 계좌 생성 &amp;gt; 계좌 입금(300원) &amp;gt; 인출 5회(1원씩)을 진행한 후 EventStore를 살펴보면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 334px;&quot; border=&quot;1&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 54px;&quot;&gt;
&lt;td style=&quot;width: 10.9303%; height: 54px;&quot;&gt;글로벌 인덱스&lt;/td&gt;
&lt;td style=&quot;width: 28.1394%; height: 54px;&quot;&gt;
&lt;p&gt;Payload 종류&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 54px;&quot;&gt;Aggregate 식별자&lt;/td&gt;
&lt;td style=&quot;width: 11.0466%; height: 54px;&quot;&gt;시퀀스 번호&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%; height: 54px;&quot;&gt;타입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 10.9303%; height: 40px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 28.1394%; height: 40px;&quot;&gt;&lt;a href=&quot;com.cqrs.events.HolderCreationEvent&quot;&gt;com.cqrs.events.HolderCreationEvent&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 40px;&quot;&gt;70f956e3-069c-4666-b0f4-324dfb0a807e&lt;/td&gt;
&lt;td style=&quot;width: 11.0466%; height: 40px;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%; height: 40px;&quot;&gt;HolderAggregate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 10.9303%; height: 40px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 28.1394%; height: 40px;&quot;&gt;&lt;a href=&quot;com.cqrs.events.AccountCreationEvent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;com.cqrs.events.AccountCreationEvent&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 40px;&quot;&gt;c65f80c3-9c44-4ca6-a977-72983a675203&lt;/td&gt;
&lt;td style=&quot;width: 11.0466%; height: 40px;&quot;&gt;0&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%; height: 40px;&quot;&gt;AccountAggregate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 10.9303%; height: 40px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 28.1394%; height: 40px;&quot;&gt;&lt;a href=&quot;com.cqrs.events.DepositMoneyEvent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;com.cqrs.events.DepositMoneyEvent&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 40px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;c65f80c3-9c44-4ca6-a977-72983a675203&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0466%; height: 40px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%; height: 40px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;AccountAggregate&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 10.9303%; height: 40px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 28.1394%; height: 40px;&quot;&gt;&lt;a href=&quot;com.cqrs.events.WithdrawMoneyEvent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;com.cqrs.events.WithdrawMoneyEvent&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 40px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;c65f80c3-9c44-4ca6-a977-72983a675203&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0466%; height: 40px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%; height: 40px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;AccountAggregate&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 10.9303%; height: 40px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;width: 28.1394%; height: 40px;&quot;&gt;&lt;a href=&quot;com.cqrs.events.WithdrawMoneyEvent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;com.cqrs.events.WithdrawMoneyEvent&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 40px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;c65f80c3-9c44-4ca6-a977-72983a675203&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0466%; height: 40px;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%; height: 40px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;AccountAggregate&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 10.9303%; height: 40px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 28.1394%; height: 40px;&quot;&gt;&lt;a href=&quot;com.cqrs.events.WithdrawMoneyEvent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;com.cqrs.events.WithdrawMoneyEvent&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 40px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;c65f80c3-9c44-4ca6-a977-72983a675203&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0466%; height: 40px;&quot;&gt;4&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%; height: 40px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;AccountAggregate&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 10.9303%; height: 20px;&quot;&gt;7&lt;/td&gt;
&lt;td style=&quot;width: 28.1394%; height: 20px;&quot;&gt;&lt;a href=&quot;com.cqrs.events.WithdrawMoneyEvent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;com.cqrs.events.WithdrawMoneyEvent&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;c65f80c3-9c44-4ca6-a977-72983a675203&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0466%; height: 20px;&quot;&gt;5&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;AccountAggregate&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 10.9303%; height: 20px;&quot;&gt;8&lt;/td&gt;
&lt;td style=&quot;width: 28.1394%; height: 20px;&quot;&gt;&lt;a href=&quot;com.cqrs.events.WithdrawMoneyEvent&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;com.cqrs.events.WithdrawMoneyEvent&lt;/a&gt;&lt;/td&gt;
&lt;td style=&quot;width: 18.8372%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;c65f80c3-9c44-4ca6-a977-72983a675203&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0466%; height: 20px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 14.3023%; height: 20px;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;AccountAggregate&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;(※ 공간 부족으로 Payload 및 이벤트 발생 시간 등은 제외하였습니다.)&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;만약 이러한 상황에서&amp;nbsp; &lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;c65f80c3-9c44-4ca6-a977-72983a675203 &lt;/span&gt;&lt;/b&gt;식별자를 지닌 &lt;b&gt;AccountAggregate&lt;/b&gt; 에서 1원을 인출하는 명령이 발생된다면 내부적으로는 어떠한 과정을 거칠까요?&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;AggregateLoading.png&quot; data-origin-width=&quot;2527&quot; data-origin-height=&quot;1516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Z0kpl/btqAMPQOgTB/LG8ApuQo2GkwTsNeTAi0z1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Z0kpl/btqAMPQOgTB/LG8ApuQo2GkwTsNeTAi0z1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Z0kpl/btqAMPQOgTB/LG8ApuQo2GkwTsNeTAi0z1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZ0kpl%2FbtqAMPQOgTB%2FLG8ApuQo2GkwTsNeTAi0z1%2Fimg.png&quot; data-filename=&quot;AggregateLoading.png&quot; data-origin-width=&quot;2527&quot; data-origin-height=&quot;1516&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://cla9.tistory.com/10?category=814447&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&lt;u&gt;Command 어플리케이션 구현 - 1&lt;/u&gt;&lt;/a&gt; 포스팅에서 소개한 Command 이벤트 수행 내부 흐름도입니다. 당시 4번 과정에 대해서 다음과 같이 소개했습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 100%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. UnitOfWork&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;수행합니다. 이 과정에서 &lt;/span&gt;Chain으로 연결된 handler 들을 거치면서 대상 Aggregate에 대하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;EventStore로부터 과거 이벤트들을 Loading 하여 최신 상태로 만듭니다&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;.&lt;/span&gt; 이후 해당 Command와 연결된 Handler 메소드를 Reflection을 활용하여 호출합니다.&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;즉 새로운 명령을 수행하기 위해서는 &lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;c65f80c3-9c44-4ca6-a977-72983a675203 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;식별자를 지닌 Aggregate를 대상으로 기존에&lt;/span&gt; 발행된 7개의 이벤트를 EventStore에서 읽어와 Loading하는 작업이 선행됩니다. 그 결과 최신 상태로 Aggregate를 만든 이후에 새 Command 적용 및 Event를 발생시킵니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1577601157466&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;o.a.commandhandling.SimpleCommandBus     : Handling command [com.cqrs.command.commands.WithdrawMoneyCommand]
c.c.command.aggregate.AccountAggregate   : applying AccountCreationEvent(holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, accountID=c65f80c3-9c44-4ca6-a977-72983a675203)
c.c.command.aggregate.AccountAggregate   : applying DepositMoneyEvent(holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, accountID=c65f80c3-9c44-4ca6-a977-72983a675203, amount=300)
c.c.command.aggregate.AccountAggregate   : balance 300
c.c.command.aggregate.AccountAggregate   : applying WithdrawMoneyEvent(holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, accountID=c65f80c3-9c44-4ca6-a977-72983a675203, amount=1)
c.c.command.aggregate.AccountAggregate   : balance 299
c.c.command.aggregate.AccountAggregate   : applying WithdrawMoneyEvent(holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, accountID=c65f80c3-9c44-4ca6-a977-72983a675203, amount=1)
c.c.command.aggregate.AccountAggregate   : balance 298
c.c.command.aggregate.AccountAggregate   : applying WithdrawMoneyEvent(holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, accountID=c65f80c3-9c44-4ca6-a977-72983a675203, amount=1)
c.c.command.aggregate.AccountAggregate   : balance 297
c.c.command.aggregate.AccountAggregate   : applying WithdrawMoneyEvent(holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, accountID=c65f80c3-9c44-4ca6-a977-72983a675203, amount=1)
c.c.command.aggregate.AccountAggregate   : balance 296
c.c.command.aggregate.AccountAggregate   : applying WithdrawMoneyEvent(holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, accountID=c65f80c3-9c44-4ca6-a977-72983a675203, amount=1)
c.c.command.aggregate.AccountAggregate   : balance 295
org.axonframework.messaging.Scope        : Clearing out ThreadLocal current Scope, as no Scopes are present
c.c.command.aggregate.AccountAggregate   : handling WithdrawMoneyCommand(accountID=c65f80c3-9c44-4ca6-a977-72983a675203, holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, amount=1)
c.c.command.aggregate.AccountAggregate   : applying WithdrawMoneyEvent(holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, accountID=c65f80c3-9c44-4ca6-a977-72983a675203, amount=1)
c.c.command.aggregate.AccountAggregate   : balance 294&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드는 Application에서 수행된 로그 중 일부를 발췌한 내용입니다.&lt;/p&gt;
&lt;p&gt;이를 통해 알 수 있는 사실은 동일 Aggregate에 대해서 Event 갯수가 늘어날 수록 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;새로운 Command를 적용하는데 오랜 시간이 소요&lt;/b&gt;&lt;/span&gt;된다는 점입니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 개선 방안(Snapshot)&lt;/h4&gt;
&lt;p&gt;EventSourcing 패턴을 적용하는 Application에는 이전 단계에서 확인한 근본적인 문제점을 안고 있습니다. 따라서 이를 완화하기 위해서 일정 주기별로 Aggregate에 대한 &lt;b&gt;Snapshot&lt;/b&gt;을 생성해야합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;스냅샷.png&quot; data-origin-width=&quot;1567&quot; data-origin-height=&quot;1174&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S9FGF/btqAOJaPeHL/POGTWYWqOmHDTGPSn539Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S9FGF/btqAOJaPeHL/POGTWYWqOmHDTGPSn539Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S9FGF/btqAOJaPeHL/POGTWYWqOmHDTGPSn539Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS9FGF%2FbtqAOJaPeHL%2FPOGTWYWqOmHDTGPSn539Dk%2Fimg.png&quot; data-filename=&quot;스냅샷.png&quot; data-origin-width=&quot;1567&quot; data-origin-height=&quot;1174&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Snapshot이란 &lt;b&gt;특정 시점의 Aggregate의 상태&lt;/b&gt;를 말합니다. 일반적으로 EventStore에는 Aggregate의 상태를 저장하지 않고 이벤트만 저장합니다. 하지만 특정 시점의 Aggregate의 상태를 저장하여 Loading 과정에서 Snapshot 이후 Event만 Replay하여 빠르게 Aggregate Loading이 가능합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonFramework에서도 &lt;b&gt;Configuration&lt;/b&gt; 설정을 통해서 Aggregate 별로 Snapshot 설정이 가능합니다. Snapshot 설정에는 특정 Threshold를 넘어가면 생성되며, Snapshot 적용 예제를 통해 문제점을 완화해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;1. Command 모듈 &lt;b&gt;AxonConfig &lt;/b&gt;파일을 오픈합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;AxonConfig.png&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cotBLN/btqAMj5JDMt/wtkVRH5xLxGj23SN8xbig1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cotBLN/btqAMj5JDMt/wtkVRH5xLxGj23SN8xbig1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cotBLN/btqAMj5JDMt/wtkVRH5xLxGj23SN8xbig1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcotBLN%2FbtqAMj5JDMt%2FwtkVRH5xLxGj23SN8xbig1%2Fimg.png&quot; data-filename=&quot;AxonConfig.png&quot; data-origin-width=&quot;490&quot; data-origin-height=&quot;436&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&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;style1&quot; /&gt;
&lt;p&gt;2. AxonConfig 클래스에 내용을 추가합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonConfig.java&lt;/p&gt;
&lt;pre id=&quot;code_1577605296727&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@AutoConfigureAfter(AxonAutoConfiguration.class)
public class AxonConfig {
    @Bean
    SimpleCommandBus commandBus(TransactionManager transactionManager){
        return  SimpleCommandBus.builder().transactionManager(transactionManager).build();
    }
    @Bean
    public AggregateFactory&amp;lt;AccountAggregate&amp;gt; aggregateFactory(){
        return new GenericAggregateFactory&amp;lt;&amp;gt;(AccountAggregate.class);
    }
    @Bean
    public Snapshotter snapshotter(EventStore eventStore, TransactionManager transactionManager){
        return AggregateSnapshotter
                .builder()
                    .eventStore(eventStore)
                    .aggregateFactories(aggregateFactory())
                    .transactionManager(transactionManager)
                .build();
    }
    @Bean
    public SnapshotTriggerDefinition snapshotTriggerDefinition(EventStore eventStore, TransactionManager transactionManager){
        final int SNAPSHOT_TRHRESHOLD = 5;
        return new EventCountSnapshotTriggerDefinition(snapshotter(eventStore,transactionManager),SNAPSHOT_TRHRESHOLD);
    }

    @Bean
    public Repository&amp;lt;AccountAggregate&amp;gt; accountAggregateRepository(EventStore eventStore, SnapshotTriggerDefinition snapshotTriggerDefinition){
        return EventSourcingRepository
                .builder(AccountAggregate.class)
                    .eventStore(eventStore)
                    .snapshotTriggerDefinition(snapshotTriggerDefinition)
                .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;위 코드는 AccountAggregate 기준으로 Snapshot을 설정하도록 작성된 코드입니다. &lt;b&gt;SnapshotTriggerDefinition&lt;/b&gt;을 통하여 Aggregate의 발행된 Event가 5개 이상일 경우 Snapshot을 생성하도록 지정하였습니다. Threshold 값에는 얼마를 지정 해야한다는 기준은 없으며, 비즈니스 로직에 따라 생성 주기를 조절하면 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;Snapshot 설정을 완료한 다음 Application을 재시작 한다음 다시 API 테스트를 하면 수행 당시에는 Snapshot이 존재하지 않기 때문에 전체를 Loading 합니다. 이때 Event를 적용하는 과정에서 Threshold 값을 넘었기 때문에 EventStore에 Snapshot을 새롭게 생성합니다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 54px;&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Aggregate 식별자&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span&gt;시퀀스 번호&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;
&lt;p&gt;타입&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;Payload&lt;/td&gt;
&lt;td&gt;Payload 타입&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td&gt;&lt;span style=&quot;color: #333333;&quot;&gt;c65f80c3-9c44-4ca6-a977-72983a675203&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;AccountAggregate&lt;/td&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;com.cqrs.command.aggregate.AccountAggregate&quot;&gt;com.cqrs.command.aggregate.AccountAggregate&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;생성된 Snapshot 데이터 중 일부를 발췌했습니다. 특정 시퀀스 번호에 해당되는 Aggregate에 대한 상태 정보가 기입되었으며, 상태정보는 Payload에 담겨있습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Payload 내용&lt;/p&gt;
&lt;pre id=&quot;code_1577604644971&quot; class=&quot;html xml&quot; style=&quot;margin: 20px auto 0px; display: block; overflow: auto; padding: 15px; color: #383a42; background: #f6f7f8; font-size: 14px; border-radius: 3px; font-family: Menlo, Consolas, Monaco, monospace; border: 1px solid #dddddd; cursor: default; z-index: 1;&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;com.cqrs.command.aggregate.AccountAggregate&amp;gt;
  &amp;lt;accountID&amp;gt;c65f80c3-9c44-4ca6-a977-72983a675203&amp;lt;/accountID&amp;gt;
  &amp;lt;holderID&amp;gt;70f956e3-069c-4666-b0f4-324dfb0a807e&amp;lt;/holderID&amp;gt;
  &amp;lt;balance&amp;gt;293&amp;lt;/balance&amp;gt;
&amp;lt;/com.cqrs.command.aggregate.AccountAggregate&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Snapshot 생성 이후 다시 1원을 인출하게되면, 이전 시퀀스 번호인 8번 Snaphot이 존재하므로 전체 Event를 읽어오지 않고 Snapshot정보를 읽어온 다음 Command 명령을 수행합니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1577606278784&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;o.a.commandhandling.SimpleCommandBus     : Handling command [com.cqrs.command.commands.WithdrawMoneyCommand]
org.axonframework.messaging.Scope        : Clearing out ThreadLocal current Scope, as no Scopes are present
c.c.command.aggregate.AccountAggregate   : handling WithdrawMoneyCommand(accountID=c65f80c3-9c44-4ca6-a977-72983a675203, holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, amount=1)
c.c.command.aggregate.AccountAggregate   : applying WithdrawMoneyEvent(holderID=70f956e3-069c-4666-b0f4-324dfb0a807e, accountID=c65f80c3-9c44-4ca6-a977-72983a675203, amount=1)
c.c.command.aggregate.AccountAggregate   : balance 292
org.axonframework.messaging.Scope        : Clearing out ThreadLocal current Scope, as no Scopes are present&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Snapshot 생성 이후 5번의 Event 발생까지는 Snapshot 시점 이전부터 생성된 Event가 재생됩니다. 만약 다시 Threshold를 넘어서게 되면 새로운 Snapshot이 생성되고 그 이후부터는 새로운 Snapshot 이후 Event가 재생됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 성능개선&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;Aggregate를 매번 로딩하면 이를 복원하는데 드는 비용이 지속 수반됩니다. 따라서 자주 사용하는 Aggregate는 Cache를 적용하면 Loading 비용이 줄어들 것입니다. Axon에서 이를 위해 기본적으로 WeakReferenceCache를 제공하며 이를 적용한 Configuration은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;AxonConfig.java&lt;/p&gt;
&lt;pre id=&quot;code_1577609682810&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@AutoConfigureAfter(AxonAutoConfiguration.class)
public class AxonConfig {
    @Bean
    public AggregateFactory&amp;lt;AccountAggregate&amp;gt; aggregateFactory(){
        return new GenericAggregateFactory&amp;lt;&amp;gt;(AccountAggregate.class);
    }
    @Bean
    public Snapshotter snapshotter(EventStore eventStore, TransactionManager transactionManager){
        return AggregateSnapshotter
                .builder()
                    .eventStore(eventStore)
                    .aggregateFactories(aggregateFactory())
                    .transactionManager(transactionManager)
                .build();
    }
    @Bean
    public SnapshotTriggerDefinition snapshotTriggerDefinition(EventStore eventStore, TransactionManager transactionManager){
        final int SNAPSHOT_TRHRESHOLD = 5;
        return new EventCountSnapshotTriggerDefinition(snapshotter(eventStore,transactionManager),SNAPSHOT_TRHRESHOLD);
    }

    @Bean
    public Cache cache(){
        return new WeakReferenceCache();
    }

    @Bean
    public Repository&amp;lt;AccountAggregate&amp;gt; accountAggregateRepository(EventStore eventStore, SnapshotTriggerDefinition snapshotTriggerDefinition, Cache cache){
        return CachingEventSourcingRepository
                .builder(AccountAggregate.class)
                    .eventStore(eventStore)
                    .snapshotTriggerDefinition(snapshotTriggerDefinition)
                    .cache(cache)
                .build();
    }
}&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;h4 data-ke-size=&quot;size20&quot;&gt;5. 마치며&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;3개의 포스팅을 통해 Command Application을 구현하는 방법에 대해서 살펴보았습니다. 다음 포스팅은 Aggregate 관련 번외편을 진행할 예정입니다. 따라서 데모 프로젝트를 위한 Application 구현은 이번 포스팅이 마지막입니다.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>MSA/AxonFramework</category>
      <category>axon</category>
      <category>Axon Framework</category>
      <category>Axon Server</category>
      <category>Axon 서버</category>
      <category>CQRS</category>
      <category>ddd</category>
      <category>Event sourcing</category>
      <category>MSA</category>
      <category>마이크로서비스아키텍처</category>
      <category>이벤트소싱</category>
      <author>cla9</author>
      <guid isPermaLink="true">https://cla9.tistory.com/12</guid>
      <comments>https://cla9.tistory.com/12#entry12comment</comments>
      <pubDate>Sun, 29 Dec 2019 18:00:29 +0900</pubDate>
    </item>
  </channel>
</rss>