<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>거북이 developer</title>
    <link>https://yang1s.tistory.com/</link>
    <description>제가 경험한 개발 지식을 공유드립니다</description>
    <language>ko</language>
    <pubDate>Sat, 1 Aug 2026 09:59:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>흥부가귀막혀</managingEditor>
    <image>
      <title>거북이 developer</title>
      <url>https://tistory1.daumcdn.net/tistory/3221335/attach/d67243e1ca644bedb13dc269195e35ba</url>
      <link>https://yang1s.tistory.com</link>
    </image>
    <item>
      <title>[Kubernetes] k8s 환경에서 springboot graceful shutdown 방법</title>
      <link>https://yang1s.tistory.com/33</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 상황&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1) request 처리 도중 Pod 이 종료되면 Request 가 비정상적으로 처리될 수 있음&lt;/li&gt;
&lt;li&gt;(2) Pod 이 종료될 때 종료 중인 Pod 에 request 가 들어오면 클라이언트는 &lt;code&gt;connection refused&lt;/code&gt; 에러를 만나게 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 해결을 위한 과정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(1)번 이슈는 Spring 에서 제공해주는 Graceful Shutdown 설정을 통해 해결 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;server:
    shutdown: graceful&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;(2)번 상황을 막기 위해 Readiness Probe 를 &lt;code&gt;/actuator/health&lt;/code&gt; endpoint 로 잡아주고 Custom Health Indicator 를 구현하여 API 로 유입된 값에 따라 Application 상태를 Active/Inactive 하게 변경할 수 있게 하였다.&lt;/li&gt;
&lt;li&gt;이렇게 하면 &lt;code&gt;prestop&lt;/code&gt; 훅에 Application 상태를 Inactive 로 변경하여 Readiness Probe 를 통해 Service 에서 제외하고 Graceful Shutdown 을 하여 (2) 번 상황을 해결할 수 있을거라 판단하였다.&lt;/li&gt;
&lt;li&gt;내가 진행한 방법이 Best Practice 일거라 생각했지만, &lt;a href=&quot;https://spring.io/guides/topicals/spring-on-kubernetes/&quot;&gt;스프링 공식 가이드&lt;/a&gt; 문서를 보니 이게 왠걸.. 단순히 sleep 만 걸어주면 된다고 정리되어 있었다.&lt;/li&gt;
&lt;li&gt;그래서 이게 왜 그런지 확인해보았다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Pod 종료 절차&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3284&quot; data-origin-height=&quot;1426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnewAi/btsD6yxidjA/2Ogj8LXjSXc4n0Uk35bAU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnewAi/btsD6yxidjA/2Ogj8LXjSXc4n0Uk35bAU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnewAi/btsD6yxidjA/2Ogj8LXjSXc4n0Uk35bAU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnewAi%2FbtsD6yxidjA%2F2Ogj8LXjSXc4n0Uk35bAU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3284&quot; height=&quot;1426&quot; data-origin-width=&quot;3284&quot; data-origin-height=&quot;1426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Pod 를 종료하면 위와 같은 절차가 진행되게 된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A : Pod 종료 sequence&lt;/li&gt;
&lt;li&gt;B : 종료되는 Pod 으로 Request 가 가지 않도록 IP 를 변경하는 sequence
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;B2 : 삭제되는 Pod 이 속한 Service 의 엔드포인트에서 삭제될 Pod 제거 (엔드포인트 API 오브젝트 수정)&lt;/li&gt;
&lt;li&gt;B3 : 각 노드에서 &lt;code&gt;iptables&lt;/code&gt; 를 업데이트해서 연결이 종료되는 Pod 으로 새로운 커넥션이 전달되지 않도록 함&lt;/li&gt;
&lt;li&gt;보다 완전한 이해를 위해서는 kube-proxy, &lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/services-networking/service/#proxy-mode-iptables&quot;&gt;iptables&lt;/a&gt; 등 개념에 대한 학습이 필요할 듯 하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;734&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ORHX7/btsD4kmdIQH/xo7KFk9zxJfKVrDKAXP0sk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ORHX7/btsD4kmdIQH/xo7KFk9zxJfKVrDKAXP0sk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ORHX7/btsD4kmdIQH/xo7KFk9zxJfKVrDKAXP0sk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FORHX7%2FbtsD4kmdIQH%2Fxo7KFk9zxJfKVrDKAXP0sk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1454&quot; height=&quot;734&quot; data-origin-width=&quot;1454&quot; data-origin-height=&quot;734&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도 작업을 해주지 않으면 B 의 sequence 가 더 오래 걸린다. 따라서 위와 같이 &lt;code&gt;connection refused&lt;/code&gt; 되는 request 가 생겨날 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Sleep 을 하면 어떻게 될까?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d9Wuxt/btsEaVyEHBQ/SdzMpW9kMOIckISkqdVlYK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d9Wuxt/btsEaVyEHBQ/SdzMpW9kMOIckISkqdVlYK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d9Wuxt/btsEaVyEHBQ/SdzMpW9kMOIckISkqdVlYK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd9Wuxt%2FbtsEaVyEHBQ%2FSdzMpW9kMOIckISkqdVlYK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1404&quot; height=&quot;746&quot; data-origin-width=&quot;1404&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;prestop&lt;/code&gt; 훅에 sleep 추가&lt;/li&gt;
&lt;li&gt;sleep 을 통해 iptables 가 변경되기 위해 필요한 시간을 확보한다.&lt;/li&gt;
&lt;li&gt;iptables 가 변경될때까지 Sleep 을 통해 Application 이 종료되지 않기 때문에 자연스럽게 Graceful Shutdown 을 수행할 수 있게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내가 진행했던 Custom Headlth Indicator 와 Readness Probe 를 사용하는 전략도 이슈는 없지만 굳이 그렇게 할 필요는 없다.&lt;/li&gt;
&lt;li&gt;k8s 환경에서 spring boot 의 graceful shutdown 을 위해서는 아래 두 가지 설정이 필요하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Application Properties 에 graceful shutdown 설정 추가&lt;/li&gt;
&lt;li&gt;prestop hook 에 &lt;code&gt;sleep&lt;/code&gt; 커맨드 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;어설픈 지식보단 공식문서와 전체적인 개념을 잘 파악하자&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;References&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/deployment.html#deployment.cloud.kubernetes.container-lifecycle&quot;&gt;스프링부트 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spring.io/guides/topicals/spring-on-kubernetes/&quot;&gt;스프링 공식 가이드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://yashwanth-nimmala.medium.com/kubernetes-graceful-shutdown-73bb23af2abd&quot;&gt;블로그 글&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/spring-boot-health-indicators&quot;&gt;Spring Boot Health Indicators&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;쿠버네티스 인 액션 17.3 절&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Kubernetes</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/33</guid>
      <comments>https://yang1s.tistory.com/33#entry33comment</comments>
      <pubDate>Mon, 29 Jan 2024 15:57:59 +0900</pubDate>
    </item>
    <item>
      <title>Kubernetes 구축</title>
      <link>https://yang1s.tistory.com/32</link>
      <description>&lt;h1&gt;공통(Master/Worker Node)&lt;/h1&gt;
&lt;h2&gt;Docker 설치&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/engine/install/&quot;&gt;Docker Engine Install&lt;/a&gt; 을 참고하여 사용하는 OS 에 맞게 설치 진행한다.&lt;/li&gt;
&lt;li&gt;centos 버전으로 설치 진행&lt;/li&gt;
&lt;li&gt;root 권한이 필요하기 때문에 &lt;code&gt;root&lt;/code&gt; 계정으로 진행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;yum-utils&lt;/code&gt; 설치 및 docker repository 등록&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;sudo yum install -y yum-utils
sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;필요한 Docker Package 설치(특정 버전을 설치하고자 한다면, &lt;code&gt;yum list docker-ce --showduplicates | sort -r&lt;/code&gt; 로 원하는 버전 확인 후 설치를 진행한다)&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;sudo yum install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;Docker 실행&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;sudo systemctl start docker&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Kubernetes 설치&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&quot;&gt;kubeadm 설치&lt;/a&gt; 를 참고하여 진행한다.&lt;/li&gt;
&lt;li&gt;root 권한이 필요하기 때문에 &lt;code&gt;root&lt;/code&gt; 계정으로 진행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. swap 비활성화&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo /sbin/swapoff -a &amp;amp;&amp;amp; sudo sed -i &amp;#39;/swap/s/^/#/&amp;#39; /etc/fstab&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;2. IPv4 포워딩 및 iptables에서 브리지된 트래픽 확인&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/container-runtimes/&quot;&gt;Container Runtime&lt;/a&gt; 가이드에 따라 IPv4 포워딩 및 iptables에서 브리지된 트래픽 확인을 진행한다.&lt;/li&gt;
&lt;li&gt;root 권한이 필요하기 때문에 &lt;code&gt;root&lt;/code&gt; 계정으로 진행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

# sysctl params required by setup, params persist across reboots
cat &amp;lt;&amp;lt;EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF

# Apply sysctl params without reboot
sudo /sbin/sysctl --system&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;3. Container Runtime Interface 설치&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Kubernetes 와 Container Runtime 을 연결해주기 위한 &lt;a href=&quot;https://kubernetes.io/docs/concepts/overview/components/#container-runtime&quot;&gt;Container Runtime Interface&lt;/a&gt; 가 설치되야 하는데 종류가 몇가지 있다.&lt;/li&gt;
&lt;li&gt;현재 구축한 kubernetes 는 &lt;code&gt;cri-dockerd&lt;/code&gt; 로 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;1) Containerd&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Docker 설치 과정에서 설치된다. 다만, 정상적인 동작을 위해 &lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/container-runtimes/#containerd&quot;&gt;Kubernetes 에서 제공하는 containerd 가이드&lt;/a&gt; 에 따라 추가로 작업을 진행해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;/etc/containerd/config.toml&lt;/code&gt; 설정 파일에서 cri disabled 제거&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;sed -i &amp;#39;/&amp;quot;cri&amp;quot;/ s/^/#/&amp;#39; /etc/containerd/config.toml&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;&lt;code&gt;/etc/containerd/config.toml&lt;/code&gt; 설정 파일에서 runc 설정 추가&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;vi /etc/containerd/config.toml
[plugins.&amp;quot;io.containerd.grpc.v1.cri&amp;quot;.containerd.runtimes.runc]
  [plugins.&amp;quot;io.containerd.grpc.v1.cri&amp;quot;.containerd.runtimes.runc.options]
    SystemdCgroup = true&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;2) cri-dockerd&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Mirantis/cri-dockerd&quot;&gt;cri-dockerd&lt;/a&gt; README 를 참고하여 설치를 진행한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;# Run these commands as root ###Install GO### wget https://storage.googleapis.com/golang/getgo/installer_linux chmod +x ./installer_linux ./installer_linux source ~/.bash_profile&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;git clone [https://github.com/Mirantis/cri-dockerd.git](https://github.com/Mirantis/cri-dockerd.git)  
cd cri-dockerd  
mkdir bin  
go build -o bin/cri-dockerd  
sudo mkdir -p /usr/local/bin  
sudo install -o root -g root -m 0755 bin/cri-dockerd /usr/local/bin/cri-dockerd  
sudo cp -a packaging/systemd/\* /etc/systemd/system  
sudo sed -i -e &amp;#39;s,/usr/bin/cri-dockerd,/usr/local/bin/cri-dockerd,&amp;#39; /etc/systemd/system/cri-docker.service  
sudo systemctl daemon-reload  
sudo systemctl enable cri-docker.service  
sudo systemctl enable --now cri-docker.socket&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;4. kubeadm, kubelet, kubectl 설치&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;kubernetes repository 등록&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-\$basearch
enabled=1
gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kubelet kubeadm kubectl
EOF&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;SELinux mode 변경&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;# Set SELinux in permissive mode (effectively disabling it)
sudo /sbin/setenforce 0
# 가이드에서는 permissive 모드로 변경하였지만, 효과적으로 하려면 disabled 로 설정해도 된다.(현재 구축된 kubernetes 는 disabled 로 설정)
# sudo sed -i &amp;#39;s/^SELINUX=enforcing$/SELINUX=permissive/&amp;#39; /etc/selinux/config     # permissive 로 변경 
sudo sed -i &amp;#39;s/^SELINUX=enforcing$/SELINUX=disabled/&amp;#39; /etc/selinux/config      # disabled 로 변경 &lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;kubeadm, kubelet, kubectl 설치 및 kubelet 실행&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;sudo yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes

sudo systemctl enable --now kubelet&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Master Node 구축(⚠️ Worker 노드에서는 하면 안됨!)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/&quot;&gt;Creating a cluster with kubeadm&lt;/a&gt; 을 참고하여 Master Node 를 구축한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;1. kubeadm init 실행&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/&quot;&gt;Container Network Interface&lt;/a&gt; 를 어떤걸 쓰냐에 따라 init 수행시 &lt;code&gt;--pod-network-cidr&lt;/code&gt; 설정을 다르게 가야한다.&lt;/li&gt;
&lt;li&gt;현재 구축된 kubernetes 는 &lt;a href=&quot;https://github.com/flannel-io/flannel&quot;&gt;flannel&lt;/a&gt; 을 설치하였고 이에 맞게 init 설정을 진행함&lt;/li&gt;
&lt;li&gt;원래는 &lt;a href=&quot;https://github.com/projectcalico/calico&quot;&gt;calico&lt;/a&gt; 를 초기에 셋팅했었으나, ncloud 장비의 bpf 관련 이슈로 정상적으로 동작하지 않아 변경함.&lt;/li&gt;
&lt;li&gt;CRI 로 &lt;code&gt;Containerd&lt;/code&gt;, &lt;code&gt;cri-dockerd&lt;/code&gt; 둘다 설치했을 경우 어떤 CRI 를 사용할 것인지 정해줘야 한다. init 실행시 &lt;code&gt;--cri-socket&lt;/code&gt; 옵션을 추가하여 사용할 CRI 가 어떤건지 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;init command 실행&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;# flannel 의 기본 cidr 설정값인 10.244.0.0/16 을 설정한다.
sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --cri-socket=unix:///var/run/cri-dockerd.sock&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;init 호출 후 stdout 에 출력된 Worker node 추가에 필요한 join command 내용을 잘 기록해둔다. 이거 안하면 Worker Node 추가가 불가능하기 때문에 다시 reset 해야함.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;vi token.txt

kubeadm join &amp;lt;control-plane-host&amp;gt;:&amp;lt;control-plane-port&amp;gt; --token &amp;lt;token&amp;gt; --discovery-token-ca-cert-hash sha256:&amp;lt;hash&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;sudo 권한 없이 kubectl 사용을 위한 설정 추가&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;❔ 혹시나 init 과정에서 실패가 발생했거나 master node 설정을 초기화 하고 싶다면? reset command 를 호출한다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;# 어떤 CRI 로 구축했는지 알아야 해서 --cri-socket 옵션을 넣어준다. 
sudo kubeadm reset --cri-socket=unix:///var/run/cri-dockerd.sock &lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;2. CNI 설치(flannel)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;다음 명령어를 통해 flannel 을 설치한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;간혹 flannel 설치 이후 &lt;code&gt;/run/flannel&lt;/code&gt; 디렉토리 안 &lt;code&gt;subnet.env&lt;/code&gt; 파일이 생성되지 않아 오류가 발생할 수 있다. 파일이 생성되었는지 확인하고 혹시 생성이 안되어있다면 다음과 같이 생성해준다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo vi /run/flannel/subnet.env&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;FLANNEL\_NETWORK=10.244.0.0/16  
FLANNEL\_SUBNET=10.244.0.1/24  
FLANNEL\_MTU=1450  
FLANNEL\_IPMASQ=true&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;3. Master Node 에 필요한 pod 이 정상인지 확인&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;kubectl 을 통해 node 상태 및 control-plane 이 정상적으로 운영중인지 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;kubectl get nodes
NAME STATUS ROLES AGE VERSION  
dev-k8s-node-002 Ready control-plane 179m v1.26.1
&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;혹여 node 의 STATUS 가 NotReady 일 경우 정확한 확인을 위해 pod 상태를 확인해본다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kubectl get pod -n kube-system&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;NAME READY STATUS RESTARTS AGE  
coredns-787d4945fb-2f9f7 1/1 Running 0 179m  
coredns-787d4945fb-ksp7d 1/1 Running 0 179m  
etcd-dev-k8s-node-002 1/1 Running 0 3h  
kube-apiserver-dev-k8s-node-002 1/1 Running 0 3h  
kube-controller-manager-dev-k8s-node-002 1/1 Running 0 3h  
kube-proxy-98thd 1/1 Running 0 174m  
kube-proxy-d5ddx 1/1 Running 0 179m  
kube-scheduler-dev-k8s-node-002 1/1 Running 0 3h&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;STATUS 가 Running 이 아닌 Pod 이 있을 경우 다음 명령어를 통해 log 및 pod event 내용을 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;log 확인&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;kubectl logs coredns-787d4945fb-2f9f7 -n kube-system&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;pod event 확인&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;kubectl describe pod coredns-787d4945fb-2f9f7 -n kube-system

Name:                 coredns-787d4945fb-2f9f7
Namespace:            kube-system
Priority:             2000000000
Priority Class Name:  system-cluster-critical
Service Account:      coredns
Node:                 dev-k8s-node-002/10.113.137.19
Start Time:           Thu, 02 Feb 2023 12:09:20 +0900
Labels:               k8s-app=kube-dns
                      pod-template-hash=787d4945fb
Annotations:          &amp;lt;none&amp;gt;
Status:               Running
IP:                   10.244.0.3
IPs:
  IP:           10.244.0.3
Controlled By:  ReplicaSet/coredns-787d4945fb
Containers:
  coredns:
    Container ID:  docker://bb053af27773abb6622792d75f56835f1530ca435601337b9afd68910309790c
    Image:         registry.k8s.io/coredns/coredns:v1.9.3
    Image ID:      docker-pullable://registry.k8s.io/coredns/coredns@sha256:8e352a029d304ca7431c6507b56800636c321cb52289686a581ab70aaa8a2e2a
    Ports:         53/UDP, 53/TCP, 9153/TCP
    Host Ports:    0/UDP, 0/TCP, 0/TCP
    Args:
      -conf
      /etc/coredns/Corefile
    State:          Running
      Started:      Thu, 02 Feb 2023 12:09:21 +0900
    Ready:          True
    Restart Count:  0
    Limits:
      memory:  170Mi
    Requests:
      cpu:        100m
      memory:     70Mi
    Liveness:     http-get http://:8080/health delay=60s timeout=5s period=10s #success=1 #failure=5
    Readiness:    http-get http://:8181/ready delay=0s timeout=1s period=10s #success=1 #failure=3
    Environment:  &amp;lt;none&amp;gt;
    Mounts:
      /etc/coredns from config-volume (ro)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-4xgx5 (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  config-volume:
    Type:      ConfigMap (a volume populated by a ConfigMap)
    Name:      coredns
    Optional:  false
  kube-api-access-4xgx5:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  3607
    ConfigMapName:           kube-root-ca.crt
    ConfigMapOptional:       &amp;lt;nil&amp;gt;
    DownwardAPI:             true
QoS Class:                   Burstable
Node-Selectors:              kubernetes.io/os=linux
Tolerations:                 CriticalAddonsOnly op=Exists
                             node-role.kubernetes.io/control-plane:NoSchedule
                             node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                             node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:                      &amp;lt;none&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Worker Node 구축&lt;/h2&gt;
&lt;h3&gt;1. Master Node 구축시 출력된 join 명령어를 수행한다.&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# CRI 가 여러개 설치되었을 경우 join 명령어 수행시 --cri-socket 설정을 추가하여 CRI 를 설정한다.
sudo kubeadm join &amp;lt;control-plane-host&amp;gt;:&amp;lt;control-plane-port&amp;gt; --token &amp;lt;token&amp;gt; --discovery-token-ca-cert-hash sha256:&amp;lt;hash&amp;gt; --cri-socket=unix:///var/run/cri-dockerd.sock &lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;2. Master Node 에서 Worker Node 가 정상적으로 Join 되었는지 확인한다.&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;kubectl get nodes

NAME               STATUS   ROLES           AGE    VERSION
dev-k8s-node-002   Ready    control-plane   3h9m   v1.26.1
dev-k8s-node-003   Ready    &amp;lt;none&amp;gt;          3h3m   v1.26.1&lt;/code&gt;&lt;/pre&gt;&lt;h1&gt;Reference&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://terianp.tistory.com/177&quot;&gt;https://terianp.tistory.com/177&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/flannel-io/flannel#deploying-flannel-manually&quot;&gt;https://github.com/flannel-io/flannel#deploying-flannel-manually&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Kubernetes</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/32</guid>
      <comments>https://yang1s.tistory.com/32#entry32comment</comments>
      <pubDate>Mon, 29 Jan 2024 15:25:58 +0900</pubDate>
    </item>
    <item>
      <title>Java8 이후로 달라진 기능들</title>
      <link>https://yang1s.tistory.com/31</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;LTS 버전중에 현재 가장 점유율이 높고 사랑받는(?) 버전은 Java 8 과 11 인듯 싶다.(관련 통계: &lt;a href=&quot;https://www.jetbrains.com/ko-kr/lp/devecosystem-2022/java/#Java_which-versions-of-java-do-you-regularly-use&quot;&gt;https://www.jetbrains.com/ko-kr/lp/devecosystem-2022/java/#Java_which-versions-of-java-do-you-regularly-use&lt;/a&gt; 참고)&lt;/li&gt;
&lt;li&gt;최근 몇년간 신규 Java 버전이 계속 릴리즈 되었고 이와 관련되서 새로운 기능들이 많아졌다.&lt;/li&gt;
&lt;li&gt;통계적으로 가장 많이 사용하고 있는 Java 8 이후에 어떤 기능들이 추가되었는지 살펴보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Java 21(2023. 09 월 release 예정) 까지의 주요 업데이트&lt;/h2&gt;
&lt;h3&gt;  ZGC Garbage Collector 추가&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JDK 11부터 공개되었고, “Stop-The-World”로 인한 성능저하를 개선하기 위한 목적을 가지고 개발되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;ZGC 동작 원리 (&lt;a href=&quot;https://wiki.openjdk.org/display/zgc/Main&quot;&gt;Wiki&lt;/a&gt;, &lt;a href=&quot;https://github.com/openjdk/zgc&quot;&gt;GitHub&lt;/a&gt;)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Pause Mark Start: Colored pointers 알고리즘을 기반해 ZGC Root에서 가리키는 객체의 상태값을 Mark(저장) 합니다.&lt;/li&gt;
&lt;li&gt;Pause Mark End: 새로 들어온 객체를 대상으로 Mark가 일어나고, ZPage(ZGC에서 다루는 영역)를 찾아 RelocationSet()에 배치합니다.&lt;/li&gt;
&lt;li&gt;Pause Relocate Start: Root 참조 객체에 대한 재배치를 하며, 이후 Load barriers 알고리즘을 통해 모든 객체를 안전하게 업데이트합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;특징&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;대기 시간이 짧은 Application에 적합한 Garbage Collection입니다.&lt;/li&gt;
&lt;li&gt;Thread 가 실행 중일 때 동시 작업을 수행하기에 모든 작업을 동시에 수행합니다. (병렬 처리)&lt;/li&gt;
&lt;li&gt;처리 시간이 10ms를 초과하지 않아 짧은 지연시간을 보장합니다.&lt;/li&gt;
&lt;li&gt;8MB부터 16TB까지의 Heap 크기를 지원합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;적용 방법&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Java Application 실행 시 다음 옵션 실행 (기본 설정: G1GC)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;java -XX:+UseZGC -jar Application.java&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Reference&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.blog-dreamus.com/post/zgc%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C&quot;&gt;https://www.blog-dreamus.com/post/zgc%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;  Collection Factory Method 강화&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Set, List, Map 인터페이스에 Immutable 성격의 Collection 을 생성할 수 있는 메서드가 추가 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt; // AS-IS
 Set&amp;lt;String&amp;gt; set = new HashSet&amp;lt;&amp;gt;();
 set.add(&amp;quot;제이든&amp;quot;);
 set.add(&amp;quot;Jayden&amp;quot;);

 List&amp;lt;String&amp;gt; list = new ArrayList&amp;lt;&amp;gt;();
 list.add(&amp;quot;제이든&amp;quot;);
 list.add(&amp;quot;버나드&amp;quot;);
 list.add(&amp;quot;자이노&amp;quot;);
 list.add(&amp;quot;메이슨&amp;quot;);
 list.add(&amp;quot;엘빈&amp;quot;);

 Map&amp;lt;String, String&amp;gt; map = new HashMap&amp;lt;&amp;gt;();
 map.put(&amp;quot;J&amp;quot;,&amp;quot;Jayden&amp;quot;);
 map.put(&amp;quot;B&amp;quot;,&amp;quot;Bernard&amp;quot;);
 map.put(&amp;quot;Z&amp;quot;,&amp;quot;Zino&amp;quot;);
 map.put(&amp;quot;M&amp;quot;,&amp;quot;Mason&amp;quot;);
 map.put(&amp;quot;E&amp;quot;,&amp;quot;Elvin&amp;quot;);&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt; // TO-BE
 Set&amp;lt;String&amp;gt; set = Set.of(&amp;quot;제이든&amp;quot;, &amp;quot;Jayden&amp;quot;, &amp;quot;개발3팀&amp;quot;);

 List&amp;lt;String&amp;gt; list = List.of(&amp;quot;제이든&amp;quot;, &amp;quot;버나드&amp;quot;, &amp;quot;자이노&amp;quot;, &amp;quot;메이슨&amp;quot;, &amp;quot;엘빈&amp;quot;);

 Map&amp;lt;String, String&amp;gt; map
    = Map.of(
    &amp;quot;J&amp;quot;, &amp;quot;Jayden&amp;quot;,
    &amp;quot;B&amp;quot;, &amp;quot;Bernard&amp;quot;,
    &amp;quot;Z&amp;quot;, &amp;quot;Zino&amp;quot;,
    &amp;quot;M&amp;quot;, &amp;quot;Mason&amp;quot;,
    &amp;quot;E&amp;quot;, &amp;quot;Elvin&amp;quot;);&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;  “var” 변수 타입 지원&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;로컬 변수 선언 시, “타입 추론”을 이용하여 명시적 타입 선언 없이도 변수 선언이 가능하도록 지원하는 신규 Keyword.&lt;/li&gt;
&lt;li&gt;JDK 10 버전부터 공식 지원하기 시작하였고 이후 LTS 버전인 JDK 11 부터는 람다 타입에서도 사용 가능하도록 지원.&lt;/li&gt;
&lt;li&gt;컴파일 시 변수 타입을 추론하기 때문에 성능에 영향을 주지는 않지만 가독성 높은 코드 작성을 위해 무분별한 “var” Keyword 이용은 지양하는 것이 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;제약&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;멤버 변수 또는 Method 파라미터로 선언은 불가능하며, 오로지 Method 내 로컬 변수로만 선언 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 가능한 사항
public void test() {
  var strData = &amp;quot;Jayden&amp;quot;;
  var doubleData = 1.5;
  var intData = 100;

  var datas = List.of(strData, doubleData, intData);

  for (var data : datas) {
     ...
  }
}

// 불가능한 사항
class MyModel {
  private var test; // X
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;  신규 String Method 추가&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;문자열 내 공백 확인/제거 등 JDK 11 버전부터 String에 여러 편리한 기능을 지원하는 새로운 Method 들이 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;신규 method&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;isBlank: 문자열이 비어있거나 공백이면 True 반환&lt;/li&gt;
&lt;li&gt;lines: 줄 단위로 나뉘어 있는 문자를 배열로 반환&lt;/li&gt;
&lt;li&gt;strip: 문자열 공백 제거 (기존 “trim()”이 ‘\u0020’ 이하 공백만을 제거 하였다면, “strip()”은 유니코드의 공백들을 전부 제거)&lt;/li&gt;
&lt;li&gt;stripLeading: 문자열 앞의 공백을 제거&lt;/li&gt;
&lt;li&gt;stripTrailing: 문자열 뒤의 공백을 제거&lt;/li&gt;
&lt;li&gt;repeat: 문자열을 파라미터로 주어진 수 만큼 반복&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public static void isBlankMethod() {
   &amp;quot;&amp;quot;.isBlank();        // Output :: true
   &amp;quot;Jayden&amp;quot;.isBlank();  // Output :: false
}

public static void linesMethod() {
   &amp;quot;Jayden\nBernard\nZino\nElvin\nMason&amp;quot;.lines().toArray();
   // Output :: [Jayden, Bernard, Zino, Elvin, Mason]
}

public static void stripMethod() {
   &amp;quot; 여기어때컴퍼니 &amp;quot;.strip();         // Output :: (여기어때컴퍼니)
   &amp;quot; 여기어때컴퍼니 &amp;quot;.stripLeading();  // Output :: (여기어때컴퍼니  )
   &amp;quot; 여기어때컴퍼니 &amp;quot;.stripTrailing(); // Output :: (  여기어때컴퍼니)
}

public static void repeatMethod() {
   &amp;quot;Jayden &amp;quot;.repeat(3); // Output :: Jayden Jayden Jayden 
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;  텍스트 블록&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JDK 15부터 정식 지원하는 새로운 기능.&lt;/li&gt;
&lt;li&gt;“”” {String 문자열 } “”” 형식을 이용해 Java 문자열을 보다 가독성있게 작성하도록 도와준다.&lt;/li&gt;
&lt;li&gt;TextBlock 내 데이터를 동적으로 다루게 될 경우 %s 문자와 format Method 를 이용하여 구현할 수 있고, 또한 블록 내 “+” 연산자를 이용하여 구현할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;String message = &amp;quot;&amp;quot;&amp;quot;
      [
         {
            &amp;quot;type&amp;quot;: &amp;quot;header&amp;quot;,
            &amp;quot;text&amp;quot;: {
                  &amp;quot;type&amp;quot;: &amp;quot;plain_text&amp;quot;,
                  &amp;quot;text&amp;quot;: &amp;quot;%s :mag:&amp;quot;,
                  &amp;quot;emoji&amp;quot;: true
            }
         },
         {
            &amp;quot;type&amp;quot;: &amp;quot;section&amp;quot;,
            &amp;quot;text&amp;quot;: {
                  &amp;quot;type&amp;quot;: &amp;quot;mrkdwn&amp;quot;,
                  &amp;quot;text&amp;quot;: &amp;quot;*API*: %s\\n*remoteIP*: %s\\n*serverIP*: %s&amp;quot;
            }
         },
         {
            &amp;quot;type&amp;quot;: &amp;quot;divider&amp;quot;
         }
      ]
         &amp;quot;&amp;quot;&amp;quot;;&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;  Switch 표현식 기능 향상&lt;/h3&gt;
&lt;h4&gt;Case문 람다식 지원&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;static void  test(Day day){
        switch (day) {
             case MONDAY, FRIDAY, SUNDAY -&amp;gt; System.out.println(6);
             case TUESDAY                -&amp;gt; System.out.println(7);
             case THURSDAY, SATURDAY     -&amp;gt; System.out.println(8);
             case WEDNESDAY              -&amp;gt; System.out.println(9);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Switch 문 값 직접 반환&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public void test(Day day) {
        System.out.println(
                switch (day) {
                    case MONDAY, FRIDAY, SUNDAY -&amp;gt; 6;
                    case TUESDAY                -&amp;gt; 7;
                    case THURSDAY, SATURDAY     -&amp;gt; 8;
                    case WEDNESDAY              -&amp;gt; 9;
            });
}&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;yield 예약어 이용한 값 리턴 방식 추가&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;public void test(Day day) {
        int cnt = switch (day) {
            case MONDAY -&amp;gt; 0;
            case TUESDAY -&amp;gt; 1; 
            case WEDNESDAY -&amp;gt; {
                int k = day.toString().length();
                int result = k+5;
        yield result;
                //break result; &amp;lt;----- java 12 Switch Expression
            }
            default -&amp;gt; 0;
        };
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;yield 키워드는 항상 Switch 블록 내부에서만 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public void test(Day day) {
        int cnt = switch (day) {
            case MONDAY -&amp;gt; 0;
            case TUESDAY -&amp;gt; yield 1; // error! yield는 block 안에서만 유효다
            case WEDNESDAY -&amp;gt; { yield 2;} // ok
            default -&amp;gt; 0;
        }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;추가로 보통 java 예약어는 변수명으로 지정이 불가능하지만 yield 키워드는 변수명으로 사용이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;int volatile = 3; // error!
int yield = 3; // ok&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;default 선언 여부에 따른 compile 에러&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;enum Test {
  FIRST,
  SECOND,
  THIRD;
}

Test test = getTest();

// default 를 선언하지 않아 compile 에러가 발생
switch(test) {
  case FIRST -&amp;gt; 1;
  case SECOND -&amp;gt; 2; 
}

// default 를 선언하여 case 에 모든 조건을 추가하지 않아도 compile 에러가 발생하지 않음.
switch(test) {
  case FIRST -&amp;gt; 1;
  default -&amp;gt; 0; 
}&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Pattern Matching for switch&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;아직 Preview 기능이라 정식 지원되는 기능은 아님 (&lt;a href=&quot;https://openjdk.org/jeps/433&quot;&gt;https://openjdk.org/jeps/433&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;instanceof 분기처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// if
static String formatter(Object obj) {
    String formatted = &amp;quot;unknown&amp;quot;;
    if (obj instanceof Integer i) {
        formatted = String.format(&amp;quot;int %d&amp;quot;, i);
    } else if (obj instanceof Long l) {
        formatted = String.format(&amp;quot;long %d&amp;quot;, l);
    } else if (obj instanceof Double d) {
        formatted = String.format(&amp;quot;double %f&amp;quot;, d);
    } else if (obj instanceof String s) {
        formatted = String.format(&amp;quot;String %s&amp;quot;, s);
    }
    return formatted;
}

// switch
static String formatterPatternSwitch(Object obj) {
    return switch (obj) {
        case Integer i -&amp;gt; String.format(&amp;quot;int %d&amp;quot;, i);
        case Long l    -&amp;gt; String.format(&amp;quot;long %d&amp;quot;, l);
        case Double d  -&amp;gt; String.format(&amp;quot;double %f&amp;quot;, d);
        case String s  -&amp;gt; String.format(&amp;quot;String %s&amp;quot;, s);
        default        -&amp;gt; obj.toString();
    };
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;null 분기처리&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// if
static void testFooBar(String s) {
    if (s == null) {
        System.out.println(&amp;quot;Oops!&amp;quot;);
        return;
    }
    switch (s) {
        case &amp;quot;Foo&amp;quot;, &amp;quot;Bar&amp;quot; -&amp;gt; System.out.println(&amp;quot;Great&amp;quot;);
        default           -&amp;gt; System.out.println(&amp;quot;Ok&amp;quot;);
    }
}

// switch
static void testFooBar(String s) {
    switch (s) {
        case null         -&amp;gt; System.out.println(&amp;quot;Oops&amp;quot;);
        case &amp;quot;Foo&amp;quot;, &amp;quot;Bar&amp;quot; -&amp;gt; System.out.println(&amp;quot;Great&amp;quot;);
        default           -&amp;gt; System.out.println(&amp;quot;Ok&amp;quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;when 예약어 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// AS-IS
static void testTriangle(Shape s) {
    switch (s) {
        case null:
            break;
        case Triangle t:
            if (t.calculateArea() &amp;gt; 100) {
                System.out.println(&amp;quot;Large triangle&amp;quot;);
                break;
            }
        default:
            System.out.println(&amp;quot;A shape, possibly a small triangle&amp;quot;);
    }
}

// TO-BE
static void testTriangle(Shape s) {
    switch (s) {
        case null -&amp;gt; 
            { break; }
        case Triangle t
        when t.calculateArea() &amp;gt; 100 -&amp;gt;
            System.out.println(&amp;quot;Large triangle&amp;quot;);
        case Triangle t -&amp;gt;
            System.out.println(&amp;quot;Small triangle&amp;quot;);
        default -&amp;gt;
            System.out.println(&amp;quot;Non-triangle&amp;quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;  instanceof 연산자에 대한 Pattern Matching&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Instanceof 내부에서 객체를 캐스팅 하는 과정이 필요하였으나, 캐스팅 과정을 내부에서 지원할 수 있도록 변경&lt;/li&gt;
&lt;li&gt;JDK 16 부터 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 기존 Java 8 코드
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Student) {
            Student student = (Student) obj;
            if (this.studentNumber == student.studentNumber) {
                return true;
            } else {
                return false;
            }
        }
        return false;
    }

// Java 16 instanceof 연산자에 대한 pattern matching
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Student student) {
            if (this.studentNumber == student.studentNumber) {
                return true;
            } else {
                return false;
            }
        }
        return false;
    }&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;  Record Data Class&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JDK 14 버전부터 공개된 Immutable 객체를 생성하는 새로운 유형의 클래스.&lt;/li&gt;
&lt;li&gt;Record 선언을 하게 되면 기존 toString, equals, hashCode 메소드를 자동으로 구현해주며, 모든 인스턴스 필드를 초기화해주는 생성자가 생성이 됩니다.&lt;/li&gt;
&lt;li&gt;Immutable 객체이기 때문에 모든 값은 생성자를 통해 설정되어야 합니다.&lt;/li&gt;
&lt;li&gt;DTO 와 같은 Data Object 용도로 활용 시 보다 편리하고 간결하게 구분할 수 있습니다.&lt;/li&gt;
&lt;li&gt;setter 를 통해 값을 주입하는 라이브러리 혹은 기능을 구현한다면 Record Class 를 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;Record Class는 상속이 불가능하다. (모든 필드는 “private final ,,”로 선언이 되기에,,)&lt;/li&gt;
&lt;li&gt;단순 선언&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;   public record RecordUserData(String name, int weight) {

   }

   ...

   RecordUserData userRecord = new RecordUserData(&amp;quot;Jayden&amp;quot;, 110);

   // Output :: Jayden
   System.out.println(userRecord.name());

   // Output :: 110
   System.out.println(userRecord.weight());

   // Output :: false
   System.out.println(userRecord.equals(new RecordUserData(&amp;quot;Jayden&amp;quot;, 90)));

   // Output :: true
   System.out.println(userRecord.equals(new RecordUserData(&amp;quot;Jayden&amp;quot;, 110)));

   // Output :: RecordUserData[name=Jayden, weight=110]
   System.out.println(userRecord);&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;Static 변수와 Method 선언&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public record Citizen(String name, String country) {
    public static String UNKNOWN_COUNTRY = &amp;quot;Unknown&amp;quot;;
}

public record Citizen(String name, String country) {
    public static Citizen justCountry(String country) {
        return new Citizen(&amp;quot;&amp;quot;, country);
    }
}

String country = Citizen.UNKNOWN_COUNTRY;
Citizen citizen = Citizen.justCountry(&amp;quot;korea&amp;quot;);&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;  Interface Private Method 지원&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;JDK9 부터 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// example
public interface Foo {

    default void bar() {
        System.out.print(&amp;quot;Hello&amp;quot;);
        baz();
    }

    private void baz() {
        System.out.println(&amp;quot; world!&amp;quot;);
    }
}
// static method 도 사용 가능
public interface Foo {

    static void buzz() {
        System.out.print(&amp;quot;Hello&amp;quot;);
        staticBaz();
    }

    private static void staticBaz() {
        System.out.println(&amp;quot; static world!&amp;quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;  Sealed class&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;무분별한 상속을 막기 위한 목적으로 등장한 기능으로 지정한 클래스 외 상속을 허용하지 않으며, 지정한 클래스 외 상속 불가능.&lt;/li&gt;
&lt;li&gt;즉, 클래스 또는 인터페이스는 이제 어떤 클래스가 이를 구현하거나 확장할 수 있는지 정의할 수 있다. 이를 통해 도메인 모델링 및 라이브러리 보안 강화에 유용해진다.&lt;/li&gt;
&lt;li&gt;JDk 17 부터 지원&lt;/li&gt;
&lt;li&gt;Sealed interface&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public sealed interface Service permits Car, Truck {

    int getMaxServiceIntervalInMonths();

    default int getMaxDistanceBetweenServicesInKilometers() {
        return 100000;
    }

}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;Sealed abstract class&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public abstract sealed class Vehicle permits Car, Truck {

    protected final String registrationNumber;

    public Vehicle(String registrationNumber) {
        this.registrationNumber = registrationNumber;
    }

    public String getRegistrationNumber() {
        return registrationNumber;
    }

}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;추가 확장을 방지하고자 한다면 final 을 선언한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public final class Truck extends Vehicle implements Service {

    private final int loadCapacity;

    public Truck(int loadCapacity, String registrationNumber) {
        super(registrationNumber);
        this.loadCapacity = loadCapacity;
    }

    public int getLoadCapacity() {
        return loadCapacity;
    }

    @Override
    public int getMaxServiceIntervalInMonths() {
        return 18;
    }

}&lt;/code&gt;&lt;/pre&gt;&lt;ul&gt;
&lt;li&gt;추가 확장을 허용하려면 &lt;code&gt;non-sealed&lt;/code&gt; 를 정의한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public non-sealed class Car extends Vehicle implements Service {

    private final int numberOfSeats;

    public Car(int numberOfSeats, String registrationNumber) {
        super(registrationNumber);
        this.numberOfSeats = numberOfSeats;
    }

    public int getNumberOfSeats() {
        return numberOfSeats;
    }

    @Override
    public int getMaxServiceIntervalInMonths() {
        return 12;
    }

}&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;제약&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;허용된 모든 하위 클래스는 봉인된 클래스와 동일한 모듈에 속해야 한다.&lt;/li&gt;
&lt;li&gt;허용된 모든 하위 클래스는 봉인된 클래스를 명시적으로 확장해야 한다.&lt;/li&gt;
&lt;li&gt;허용된 모든 하위 클래스는 수정자를 정의해야 한다.(final, sealed 또는 non-sealed)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Record 와의 궁합&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Record class 는 내부적으로 final 이 명시된 class 이기 때문에 좀더 간결한 구조로 정의가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public sealed interface Vehicle permits Car, Truck {

    String getRegistrationNumber();

}

public record Car(int numberOfSeats, String registrationNumber) implements Vehicle {

    @Override
    public String getRegistrationNumber() {
        return registrationNumber;
    }

    public int getNumberOfSeats() {
        return numberOfSeats;
    }

}

public record Truck(int loadCapacity, String registrationNumber) implements Vehicle {

    @Override
    public String getRegistrationNumber() {
        return registrationNumber;
    }

    public int getLoadCapacity() {
        return loadCapacity;
    }

}&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Reflection&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;java.lang.Class&lt;/code&gt; 에서 다음과 같은 2개의 API 를 추가로 지원한다.&lt;/li&gt;
&lt;li&gt;isSealed: 주어진 클래스 또는 인터페이스가 봉인된 경우 true 를 반환.&lt;/li&gt;
&lt;li&gt;getPermittedSubclasses: 허용된 모든 하위 클래스를 나타내는 객체 배열을 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Assertions.assertThat(truck.getClass().isSealed()).isEqualTo(false);
Assertions.assertThat(truck.getClass().getSuperclass().isSealed()).isEqualTo(true);
Assertions.assertThat(truck.getClass().getSuperclass().getPermittedSubclasses())
  .contains(ClassDesc.of(truck.getClass().getCanonicalName()));&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;  Virtual Thread(aka. Project Loom)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Java 21 에 정식 지원 예정 (관련 기사 : &lt;a href=&quot;https://www.infoq.com/news/2023/04/virtual-threads-arrives-jdk21/&quot;&gt;https://www.infoq.com/news/2023/04/virtual-threads-arrives-jdk21/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;JDK 21 부터 Java 는 플랫폼 쓰레드와 가상 쓰레드라는 2종류의 쓰레드를 지원&lt;/li&gt;
&lt;li&gt;플랫폼 쓰레드는 OS 쓰레드에 대한 1대1 매핑이고, 가상 쓰레드는 Java 가 제공하는 경량 쓰레드로 하나의 OS 쓰레드 내에서 가상 쓰레드 1만개를 동시 실행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -&amp;gt; {
        executor.submit(() -&amp;gt; {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}&lt;/code&gt;&lt;/pre&gt;&lt;h4&gt;Reference&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://gunsdevlog.blogspot.com/2020/09/java-project-loom-reactive-streams.html&quot;&gt;http://gunsdevlog.blogspot.com/2020/09/java-project-loom-reactive-streams.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cr.openjdk.org/~rpressler/loom/Loom-Proposal.html&quot;&gt;https://cr.openjdk.org/~rpressler/loom/Loom-Proposal.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;  Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blogs.oracle.com/javakr/post/java8-16&quot;&gt;Java 8부터 16까지의 신규 기능들 정리&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techblog.gccompany.co.kr/%EC%9A%B0%EB%A6%AC%ED%8C%80%EC%9D%B4-jdk-17%EC%9D%84-%EB%8F%84%EC%9E%85%ED%95%9C-%EC%9D%B4%EC%9C%A0-ced2b754cd7&quot;&gt;우리팀이 JDK 17을 도입한 이유&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/31</guid>
      <comments>https://yang1s.tistory.com/31#entry31comment</comments>
      <pubDate>Thu, 4 May 2023 18:19:31 +0900</pubDate>
    </item>
    <item>
      <title>Spring 에서 제공하는 MongoDB Annotation</title>
      <link>https://yang1s.tistory.com/30</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Application 에서 정의한 Model 객체가 어떻게 MongoDB 로 변환이 되어 들어가고, 조회시에는 어떻게 Model 객체로 맵핑이 되는지에 대한 궁금증에서 시작.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-data-mongodb/blob/main/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java&quot;&gt;MappingMongoConverter&lt;/a&gt; 에서 이러한 기능을 담당하는거로 확인했지만, 이번 시간에는 이부분에 대해 깊게 파진 않고 이러한 Mapping 을 도와주는 Annotation 에 대해 알아보려고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;@Document&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Spring Data Mongodb 에서만 사용할 수 있는 Annotation&lt;/li&gt;
&lt;li&gt;어노테이션이 정의된 Class 를 사용할 MongoDB Collection 을 지정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/**
 * 나는 base collection 에 들어갈거다!
 */
@Document(&amp;quot;base&amp;quot;)
public class BaseDocument {
    @Id
    private String documentId;
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;@Id&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ObjectId 에 해당하는 필드를 지정할 경우 선언할 수 있다.&lt;/li&gt;
&lt;li&gt;해당 어노테이션이 없을 경우 필드 네이밍이 &lt;code&gt;id&lt;/code&gt; 라고 선언한 필드를 ObjectId 로 인지한다.&lt;/li&gt;
&lt;li&gt;id 이름으로 선언된 필드와 @Id 어노테이션이 지정된 필드가 있을경우 어노테이션을 선언한 필드를 ObjectId 필드로 인지한다.&lt;/li&gt;
&lt;li&gt;@PersistenceConstructor 로 지정한 생성자에 id 값을 주입 안하더라도 MappingMongoConverter 에서 field injection 으로 값을 넣어준다.(이건 setter method 가 없어도 동일하다.)&lt;/li&gt;
&lt;li&gt;해당 어노테이션은 MongoDB 전용은 아니고 Spring Data 하위 프로젝트에서 모두 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;@Id annotation 을 사용할 경우&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;public class BaseDocument {
    @Id
    private String documentId;
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;@Id annotation 을 사용하지 않을 경우&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;public class BaseDocument {
    private String id;
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;setter method 가 없어도 주입된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;public class BaseDocument {
    @Id
    private String id;

        public String getId() {
           return id;
        }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;@PersistenceConstructor 를 사용할 때 굳이 id 값을 주입받지 않아도 된다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;public class BaseDocument {
    @Id
    private String id;
        private String name;

        @PersistenceConstructor
    BaseDocument(String name) {
        this.name = name;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;@PersistenceConstructor&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MongoDB 에서 가져온 데이터(DBObject)를 Model Class 로 전환할 때 사용할 생성자를 지정한다.&lt;/li&gt;
&lt;li&gt;해당 어노테이션이 없을 경우 기본 생성자를 찾아서 사용한다.&lt;/li&gt;
&lt;li&gt;해당 어노테이션을 지정하는 생성자는 public 이 아니어도 된다. protected, private, package private 로 정의된 생성자 모두 사용 가능하다. 이게 가능한 이유는 역시 &lt;a href=&quot;https://steady-coding.tistory.com/609&quot;&gt;Java Reflection&lt;/a&gt; 때문.&lt;/li&gt;
&lt;li&gt;생성자에 정의된 각 argument 의 key 값이 db 필드의 key 값과 일치하면 주입해준다.&lt;/li&gt;
&lt;li&gt;해당 어노테이션은 MongoDB 전용은 아니고 Spring Data 하위 프로젝트에서 모두 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class BaseDocument {
    private String service;
    private Integer version;
    private String name;

    public BaseDocument(String service, Integer version) {
        this.service = service;
        this.version = version;
    }

    @PersistenceConstructor
        // 어노테이션을 지정했기 때문에 위에 선언한 생성자를 사용하지 않는다는데 내 맥북과 마우스 전부를 건다.
        // private 여도 이 생성자를 쓴다는거에 내 맥북과 마우스 전부를 건다.
    private BaseDocument(String service, Integer version, String name) {
        this.service = service;
        this.version = version;
        this.name = name;
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;@CreatedDate, @LastModifiedDate&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Document 의 생성시간, 변경 시간을 정의한 field 에 선언하면 알아서 날짜값을 만들어준다.&lt;/li&gt;
&lt;li&gt;단, 해당 어노테이션이 의도한대로 동작하기 위해서는 @EnableMongoAuditing, @EnableReactiveMongoAuditing 가 선언되야한다.&lt;/li&gt;
&lt;li&gt;단, save 를 통해 한번 저장된 data 를 또 save 하게 될 경우 의도한대로 동작하지 않는다. 한번 save 된 데이터 일 경우 findAndModify 를 쓰거나 @Version Annotation 을 활용해야한다.&lt;/li&gt;
&lt;li&gt;해당 어노테이션은 MongoDB 전용은 아니고 Spring Data 하위 프로젝트에서 모두 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;@EnableMongoAuditing 혹은 @EnableReactiveMongoAuditing 을 선언하고&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableReactiveMongoAuditing
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;다음과 같이 class 를 선언하고&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;public class BaseDocument {
    @Id
    private String id;
    @CreatedDate
    private Date createTime;
    @LastModifiedDate
    private Date updateTime;

    public BaseDocument() {
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이렇게 코드를 짜서 보면&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;BaseDocument saveDocument = template.save(new BaseDocument());
System.out.println(saveDocument);&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;console 에 이렇게 찍힌다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;[mongod output] [id=63bfca54eb3c3c148edb42a9,createTime=Thu Jan 12 17:52:36 KST 2023,updateTime=Thu Jan 12 17:52:36 KST 2023]&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;@CreatedBy, @LastModifiedBy&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Document 를 생성 및 변경에 대한 사용자 관련 field 에 해당 어노테이션을 선언하면 알아서 해당 값을 주입해준다.&lt;/li&gt;
&lt;li&gt;단, 해당 어노테이션이 의도한대로 동작하기 위해서는 @EnableMongoAuditing, @EnableReactiveMongoAuditing 가 선언되야한다.&lt;/li&gt;
&lt;li&gt;또한 생성할 사용자 정보를 전달해주기 위해 AuditorAware 혹은 ReactiveAuditorAware 를 정의해줘야한다.&lt;/li&gt;
&lt;li&gt;해당 어노테이션은 MongoDB 전용은 아니고 Spring Data 하위 프로젝트에서 모두 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;@EnableMongoAuditing 혹은 @EnableReactiveMongoAuditing 을 선언하고&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;@SpringBootApplication
@EnableReactiveMongoAuditing
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;AuditorAware 혹은 ReactiveAuditorAware 를 정의하고&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;@Component
public class UserAudtiting implements AuditorAware&amp;lt;String&amp;gt;{

    @Override
    public Optional&amp;lt;String&amp;gt; getCurrentAuditor() {
        return Optional.of(&amp;quot;me&amp;quot;);
    }

}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;다음과 같이 class 를 선언하고&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;public class BaseDocument {
    @Id
    private String id;
    @CreatedBy
    private  String createUser;
    @LastModifiedBy
    private Date updateUser;

    public BaseDocument() {
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이렇게 코드를 짜서 보면&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;BaseDocument saveDocument = template.save(new BaseDocument());
System.out.println(saveDocument);&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;console 에 이렇게 찍힌다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;[mongod output] [id=63bfca54eb3c3c148edb42a9,createUser=me,updateUser=me]&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;@Transient&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;DB 에 기록하지 않을 field 가 있을 경우 해당 어노테이션을 선언한다.&lt;/li&gt;
&lt;li&gt;Spring Data Mongodb 는 기본적으로 field 에 정의된 데이터만 DB 에 기록하기 때문에 getter method 로 선언한 내용은 해당 어노테이션을 붙이지 않아도 된다.(어차피 DB 에 저장하지 않을것이기 때문)&lt;/li&gt;
&lt;li&gt;해당 어노테이션은 MongoDB 전용은 아니고 Spring Data 하위 프로젝트에서 모두 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;@Version&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Document 의 version 값에 해당하는 field 가 있을 경우 해당 어노테이션을 선언하면 save 할 때 자동으로 version 값을 올려준다.&lt;/li&gt;
&lt;li&gt;해당 어노테이션을 둘 경우 save query 에 기존 version 값이 조건으로 추가되어 동시에 save 할 경우 벌어질 동시성 이슈를 막을 수 있다.(다만, 이건 로직적으로 잘 파악하고 사용해야할듯 하다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;다음과 같이 class 를 선언하고&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;public class BaseDocument {
    @Id
    private String id;
    @Version
    private  long version;

    public BaseDocument() {
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이렇게 코드를 짜서 보면&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;BaseDocument saveDocument = template.save(new BaseDocument());
System.out.println(saveDocument);
BaseDocument secondSaveDocument = template.save(saveDocument);
System.out.println(secondSaveDocument);&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;console 에 이렇게 찍힌다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;[mongod output] [id=63bfca54eb3c3c148edb42a9,version=1]
[mongod output] [id=63bfca54eb3c3c148edb42a9,version=2]&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;@Field&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Spring Data Mongodb 에서만 사용할 수 있는 Annotation&lt;/li&gt;
&lt;li&gt;DB 에 정의된 field 이름과 model 에 정의된 field 이름이 다를 경우 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;MongoDB 에서 Model Class 로 Mapping 할 때 Setter method 를 사용하고자 할때도 사용할 수 있다. 단, 이럴 경우 @AccessType(AccessType.Type.PROPERTY) 를 같이 선언해줘야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;DB 에는 createTime, Model Class 에는 createDate 라고 정의했을 경우&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;public class BaseDocument {
    @Id
    private String id;
    @ Field(&amp;quot;createTime&amp;quot;)
    private  Date createDate;

    public BaseDocument() {
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;setter 함수로 데이터 injection 을 할 경우&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;public class BaseDocument {
    @Id
    private String id;
    private NameCard nameCard = new NameCard();

    public BaseDocument() {
    }

        @AccessType(AccessType.Type.PROPERTY)
        @Field(&amp;quot;name&amp;quot;)
        public setPhone(String name) {
            nameCard.setName(name);
        }

        @AccessType(AccessType.Type.PROPERTY)
        @Field(&amp;quot;phoneNumber&amp;quot;)
        public setPhoneNumber(String phoneNumber) {
            nameCard.setPhoneNumber(phoneNumber);
        }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;@TypeAlias&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MongoDB 의 Collection 에는 다양한 유형의 Model Class 를 Document 로 포함할 수 있는데, 이렇게 저장된 객체를 조회할 때는 입력당시 사용한 Model Class 에 올바르게 Mapping 하는 것이 중요하다. 따라서 Document 를 저장할 때 함께 타입 정보를 저장하는 매커니즘을 사용하며, 이러한 역할에 도움을 주는 것이 바로 &lt;code&gt;_class&lt;/code&gt; 필드다.&lt;/li&gt;
&lt;li&gt;하지만 이 기능의 치명적인 단점은, &lt;code&gt;_class&lt;/code&gt; 로 기록된 타입값이 추후 변경되면 동일한 모델이더라도 올바르게 맵핑할 수 없다는 단점이 존재한다.(예를 들어 package 명이 변경되거나 Model Class 이름을 변경했을 경우)&lt;/li&gt;
&lt;li&gt;그래서 이런 불상사를 막기 위해 &lt;code&gt;_class&lt;/code&gt; 에 기록할 특정 네이밍을 정하고자 한다면, @TypeAlias 를 사용하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❓ @TypeAlias 가 동작하는 케이스는?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;MongoTemplate 혹은 ReactiveMongoTemplate 에서 쿼리를 요청할 때 파라미터로 전달하는 Entity Class 가 Abstract class 혹은 Interface 일 경우 동작한다.&lt;/li&gt;
&lt;li&gt;만약 구현체 Class 를 Entity Class 로 넘겨줄 경우 &lt;code&gt;_class&lt;/code&gt; 값을 통한 Model Class 맵핑은 무시되고 Entity Class 로 전달한 구현체 Class 에 맞춰 데이터가 맵핑된다. &lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❓ _class 가 Package 경로로 주입된 데이터의 모델에 @TypeAlias 를 넣으면 어떻게 될까?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;이미 package 경로로 &lt;code&gt;_class&lt;/code&gt; 가 입력된 데이터에 대한 Model Class 에 @TypeAlias 로 다른 네이밍을 정의했을 경우에도 조회시 정상동작한다.&lt;/li&gt;
&lt;li&gt;즉, Spring Data 에서는 &lt;code&gt;_class&lt;/code&gt; 값이 Model Class 의 Package 경로와 일치하거나 @TypeAlias 의 정의된 네이밍과 일치하면 정상적으로 맵핑시키는 구조로 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❓ 중간에 @TypeAlias 를 빼면 어떻게 될까?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_class&lt;/code&gt; 가 이미 @TypeAlias 에 지정된 값으로 입력되었다면 이후 @TypeAlias 를 제거하거나 네이밍을 변경할 경우 정상적으로 맵핑을 못시킨다.&lt;/li&gt;
&lt;li&gt;단, 이는 특정 field 가 Object 형태이고 해당 Object 내부에 &lt;code&gt;_class&lt;/code&gt; 가 정의되었을경우 정상적으로 맵핑을 못시키지만, root 경로에 있는 &lt;code&gt;_class&lt;/code&gt; 는 실제 Model Class 와 달라도 정상동작한다. 그 이유는 MongoTemplate 에서 findById 등으로 조회시 class 값을 넘겨주는데 이걸로 맵핑을 하기 때문에 root 경로에 있는 &lt;code&gt;_class&lt;/code&gt; 가 달라도 정상적으로 맵핑이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;❓ 특정 필드의 데이터를 Map 혹은 Object 로 조회하면 어떻게 될까?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;만약 @TypeAlias 로 선언된 class 가 있거나, &lt;code&gt;_class&lt;/code&gt; 와 동일한 경로에 Model Class 가 존재할 경우 해당 class 로 맵핑시켜준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;⚠️ @TypeAlias 사용시 주의사항&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Spring 에서 Model Mapping 에 사용하는 MappingMongoConverter 에 @TypeAlias 어노테이션이 정의된 네임에 맵핑할 class 정보가 사전에 미리 셋팅 되어있어야 한다.&lt;/li&gt;
&lt;li&gt;정상적인 Spring Application 이라면 @EnableMongoRepositories 혹은 @EnableReactiveMongoRepositories 가 선언되어있을 경우 이를 scan 하고 자동으로 생성된 MappingMongoConverter 에 주입해주기 때문에 이슈가 없다.&lt;/li&gt;
&lt;li&gt;하지만 직접 MongoTemplate 혹은 MappingMongoConverter 를 생성해서 사용하게 될 경우 위의 scan 작업에 대한 결과가 주입되지 못하기 때문에 해당 class 에 대해 save query 를 수행해야지만 @TypeAlias 네임과 class 정보가 맵핑된다. 그전까지는 정상적으로 맵핑이 되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;이렇게 TypeAlias 를 정의하고&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;/**
 * 나는 base collection 에 들어갈거다!
 */
@Document(&amp;quot;base&amp;quot;)
@TypeAlias(&amp;quot;base&amp;quot;)
public class BaseDocument {
    @Id
    private String id;
}&lt;/code&gt;&lt;/pre&gt;&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;save 를 해보면?&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;pre&gt;&lt;code&gt;{
  &amp;quot;_id&amp;quot;:{
           &amp;quot;$oid&amp;quot;:&amp;quot;63c001f341eb836dfd411888&amp;quot;
        },
  &amp;quot;_class&amp;quot;:&amp;quot;base&amp;quot;
}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;@DBRef&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Spring Data Mongodb 에서만 사용할 수 있는 Annotation&lt;/li&gt;
&lt;li&gt;MongoDB 에서 제공하는 &lt;a href=&quot;https://www.mongodb.com/docs/manual/reference/database-references/#dbrefs&quot;&gt;DBRefs&lt;/a&gt; 를 지원하도록 만들어진 어노테이션&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.mongodb.com/docs/manual/reference/database-references/#dbrefs&quot;&gt;DBRefs&lt;/a&gt; 로 정의된 Data 일 경우 자동으로 해당 Collection 에 맞게 데이터를 조회하여 model 에 맵핑해준다.&lt;/li&gt;
&lt;li&gt;어노테이션의 db 속성값을 통해 다른 Database 에 들어간 데이터도 맵핑이 가능합니다.&lt;/li&gt;
&lt;li&gt;lazy 속성값을 통해 field 에 접근하기 전까지 data 조회를 지연시킬 수 있습니다.(기본값은 false 라서 처음 mapping 과정에서 모든 객체를 로드합니다.)&lt;/li&gt;
&lt;li&gt;해당 어노테이션은 DBObject 에서 Model 객체로 Mapping 해주는 기능에만 사용하기 때문에 아쉽게도 save 시에는 자동으로 생성해주진 않는다.&lt;/li&gt;
&lt;li&gt;자세한 예제는 &lt;a href=&quot;https://www.baeldung.com/spring-mongodb-dbref-annotation&quot;&gt;baeldung&lt;/a&gt; 참고&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;@EnableMongoAuditing, @EnableReactiveMongoAuditing&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Spring Data Mongodb 에서만 사용할 수 있는 Annotation&lt;/li&gt;
&lt;li&gt;Spring Data 에서 제공하는 Auditing 기능을 MongoDB 에 맞게 제공할 수 있도록 지원하는 Annotation&lt;/li&gt;
&lt;li&gt;@CreatedBy, @LastModifiedBy, @CreatedDate, @LastModifiedDate 어노테이션을 사용할 경우 해당 annotation 을 특정 class(Configuration 혹은 Application Class 등)에 정의해줘야한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring Framework</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/30</guid>
      <comments>https://yang1s.tistory.com/30#entry30comment</comments>
      <pubDate>Wed, 5 Apr 2023 12:17:19 +0900</pubDate>
    </item>
    <item>
      <title>[Spring Boot] NoClassDefFoundError 와 Spring Boot 의 Executable Jar 관계</title>
      <link>https://yang1s.tistory.com/29</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에 Spring Boot 로 개발된 Web Application 을 운영하던 도중에 아래와 같은 Error 를 맞딱뜨렸습니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;2022-08-30 11:07:35.485  WARN 3038 --- [SpringContextShutdownHook] o.s.c.support.DefaultLifecycleProcessor  : Failed to stop bean 'webServerGracefulShutdown'

 java.lang.BootstrapMethodError: java.lang.NoClassDefFoundError: org/springframework/boot/web/server/GracefulShutdownResult
     at org.springframework.boot.web.servlet.context.WebServerGracefulShutdownLifecycle.stop(WebServerGracefulShutdownLifecycle.java:51)
     at org.springframework.context.support.DefaultLifecycleProcessor.doStop(DefaultLifecycleProcessor.java:238)
     at org.springframework.context.support.DefaultLifecycleProcessor.access$300(DefaultLifecycleProcessor.java:53)
     at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.stop(DefaultLifecycleProcessor.java:377)
     at org.springframework.context.support.DefaultLifecycleProcessor.stopBeans(DefaultLifecycleProcessor.java:210)
     at org.springframework.context.support.DefaultLifecycleProcessor.onClose(DefaultLifecycleProcessor.java:128)
     at org.springframework.context.support.AbstractApplicationContext.doClose(AbstractApplicationContext.java:1022)
     at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.doClose(ServletWebServerApplicationContext.java:170)
     at org.springframework.context.support.AbstractApplicationContext$1.run(AbstractApplicationContext.java:949)
 Caused by: java.lang.NoClassDefFoundError: org/springframework/boot/web/server/GracefulShutdownResult
     ... 9 common frames omitted
 Caused by: java.lang.ClassNotFoundException: org.springframework.boot.web.server.GracefulShutdownResult
     at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
     at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
     at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151)
     at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
     ... 9 common frames omitted

 2022-08-30 11:08:05.486  INFO 3038 --- [SpringContextShutdownHook] o.s.c.support.DefaultLifecycleProcessor  : Failed to shut down 1 bean with phase value 2147483647 within timeout of 30000ms: [webServerGracefulShutdown]
 2022-08-30 11:08:35.514  INFO 3038 --- [SpringContextShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
 2022-08-30 11:08:35.519  INFO 3038 --- [SpringContextShutdownHook] o.s.s.c.ThreadPoolTaskScheduler          : Shutting down ExecutorService 'taskScheduler'
 2022-08-30 11:08:35.520  INFO 3038 --- [SpringContextShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
 2022-08-30 11:08:35.528  INFO 3038 --- [SpringContextShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
 2022-08-30 11:08:35.530  INFO 3038 --- [SpringContextShutdownHook] o.s.s.c.ThreadPoolTaskScheduler          : Shutting down ExecutorService&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application 을 배포하기 위해 종료하던 과정에서 발생하였는데, 해당 Error 로 인해 Application 이 정상적으로 종료되지 않았고 결국 &lt;code&gt;kill -9&lt;/code&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-filename=&quot;몰라..뭐야..그거..무서워..d.jpg&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bby2yc/btrL8xkovCs/MynL3eNCikdzBQECpeH16k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bby2yc/btrL8xkovCs/MynL3eNCikdzBQECpeH16k/img.jpg&quot; data-alt=&quot;처음 보는 에러가 발생할 때.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bby2yc/btrL8xkovCs/MynL3eNCikdzBQECpeH16k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbby2yc%2FbtrL8xkovCs%2FMynL3eNCikdzBQECpeH16k%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;393&quot; height=&quot;282&quot; data-filename=&quot;몰라..뭐야..그거..무서워..d.jpg&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;처음 보는 에러가 발생할 때.jpg&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;갑작스러운 Error 였고 언제 다시 발생할지 모른다는 불안감에 오랜 시간 해당 Error 가 발생한 원인을 분석해보았는데 그 결과 위 에러는 SpringBoot 의 Executable Jar 동작 방식과 연관이 있었습니다.&lt;/p&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 는 Nested Jar 파일 (즉, jar 안에 jar 가 들어가있는 파일)을 로드하는 표준 방법을 제공하지 않습니다.&lt;br /&gt;그래서 Spring Boot 에서는 자체적으로 Nested Jar 파일을 실행하기 위한 Jar 파일 구조를 정의하고 Loader 를 구현하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot Loader 를 사용하기 위한 Jar 파일 구조는 다음과 같이 정의되어있습니다.&lt;/p&gt;
&lt;pre class=&quot;crystal&quot;&gt;&lt;code&gt;example.jar
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-&amp;lt;spring boot loader classes&amp;gt;
 +-BOOT-INF
    +-classes
    |  +-mycompany
    |     +-project
    |        +-YourClasses.class
    +-lib
       +-dependency1.jar
       +-dependency2.jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application Service 에 관련된 코드(개발자가 작성한 코드) 는 &lt;code&gt;BOOT-INF/classes&lt;/code&gt; 경로에 정리되고,&lt;br /&gt;의존성이 추가된 lib 들은 &lt;code&gt;BOOT-INF/lib&lt;/code&gt; 에 있도록 정의하였습니다.&lt;/p&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;code&gt;spring-boot-maven-plugin&lt;/code&gt; 을 통해 만들어진 jar 결과물을 보면 위와 같은 구조를 확인할 수 있고&lt;br /&gt;추가적으로 Spring Boot 에서 구현한 &lt;a href=&quot;https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java&quot;&gt;JarLauncher&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;&lt;a href=&quot;https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/JarLauncher.java&quot;&gt;JarLauncher&lt;/a&gt; 는 Spring Boot 에서 jar 형태로 빌드된 Spring Boot Application 을 실행하기 위해 만들어진 Launcher Class 입니다.&lt;br /&gt;그래서 jar 로 빌드된 결과물의 Main Class 는 해당 Launcher 로 지정이 되야 정상적으로 실행이 가능합니다. 실제 Spring Boot Jar 에서 &lt;code&gt;META-INF/MANIFEST.MF&lt;/code&gt; 내용을 확인해보면 아래와 같이 정의된걸 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.mycompany.project.MyApplication&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 java 에서 수행할 Main-Class 는 Spring Boot 에서 구현한 JarLauncher 로 지정된걸 알 수 있습니다.&lt;/p&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 에서는 jar 에 안에 있는 각 class 와 jar 들은 어떻게 Load 하는 걸까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 는 아래와 같이 각 class 와 jar 의 파일 offset 값을 기록해 둡니다. 그리하여 필요할 때마다 해당 offset 에 있는 class 와 jar 파일을 load 할 수 있도록 구현을 해 두었습니다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;myapp.jar
+-------------------+-------------------------+
| /BOOT-INF/classes | /BOOT-INF/lib/mylib.jar |
|+-----------------+||+-----------+----------+|
||     A.class      |||  B.class  |  C.class ||
|+-----------------+||+-----------+----------+|
+-------------------+-------------------------+
 ^                    ^           ^
 0063                 3452        3980&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 했을 때 장점은 jar 파일의 압축을 풀거나 모든 항목 데이터를 메모리로 읽지 않아도 필요시 바로 바로 찾아서 Load 할 수 있다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;반대로 단점은 jar 파일이 변경되었을 경우 jar 파일 안에 class 나 jar 파일이 존재함에도 불구하고 제가 위에서 경험한 &lt;code&gt;NoClassDefFoundError&lt;/code&gt; 가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;jar 파일이 변경되기 전에 이미 Load 된 class 나 jar 파일이면 상관이 없지만, 아직 Load 가 안된 상태에서 jar 파일이 변경된 경우 그리고 이로 인해 각 class 와 jar 파일의 offset 정보가 달라졌을 경우 jar 안에 class 나 jar 파일이 존재하더라도 class 를 찾을 수 없는 상태가 되기 때문에 &lt;code&gt;NoClassDefFoundError&lt;/code&gt;, &lt;code&gt;ClassNotFoundException&lt;/code&gt; 이 발생하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제가 맞딱뜨렸던 Error 상황은 배포 시나리오가 잘못되어 빌드된 jar 파일을 application 종료 전에 먼저 배포하게 되었고&lt;br /&gt;종료시그널을 받았을 때 처음으로 Load 된 &lt;code&gt;GracefulShutdownResult&lt;/code&gt; class 로 인해 위와 같은 현상이 발생하게 된 것이었습니다.&lt;br /&gt;현재 해당 현상을 해결할 수 있는 방법은 jar 파일을 교체하기 전에 반드시 Application 을 종료하는 방법밖에 없는것으로 알고 있습니다.&lt;br /&gt;(만약에 다른 방법이 있다면 알려주세요!)&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#appendix.executable-jar&quot;&gt;Spring Boot Document - executable jar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring Framework</category>
      <category>ExecutableJar</category>
      <category>NoClassDefFoundError</category>
      <category>springboot</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/29</guid>
      <comments>https://yang1s.tistory.com/29#entry29comment</comments>
      <pubDate>Wed, 14 Sep 2022 17:51:27 +0900</pubDate>
    </item>
    <item>
      <title>  Spring Rest Docs 로 Swagger API 문서 관리 자동화</title>
      <link>https://yang1s.tistory.com/28</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &amp;nbsp;Spring&amp;nbsp;Rest&amp;nbsp;Docs&amp;nbsp;란?&lt;/b&gt;&amp;nbsp;Spring&amp;nbsp;REST&amp;nbsp;Docs&amp;nbsp;는&amp;nbsp;RESTful&amp;nbsp;서비스의&amp;nbsp;문서화를&amp;nbsp;도와주는&amp;nbsp;도구이다.&lt;br /&gt;&amp;nbsp;문서&amp;nbsp;작성&amp;nbsp;도구로&amp;nbsp;Asciidoctor&amp;nbsp;를&amp;nbsp;기본적으로&amp;nbsp;사용하며,&amp;nbsp;이것을&amp;nbsp;통해&amp;nbsp;HTML&amp;nbsp;문서를&amp;nbsp;생성한다.&amp;nbsp;필요한&amp;nbsp;경우&amp;nbsp;Markdown&amp;nbsp;문법을&amp;nbsp;사용하도록&amp;nbsp;변경할&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;&amp;nbsp;RESTful&amp;nbsp;API를&amp;nbsp;문서화&amp;nbsp;할때&amp;nbsp;중요한&amp;nbsp;포인트는&amp;nbsp;API&amp;nbsp;에서&amp;nbsp;필요한&amp;nbsp;HTTP&amp;nbsp;요청과&amp;nbsp;응답의&amp;nbsp;리소스를&amp;nbsp;설명하는&amp;nbsp;것인데&amp;nbsp;Spring&amp;nbsp;REST&amp;nbsp;Docs&amp;nbsp;를&amp;nbsp;사용할&amp;nbsp;경우&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;작성하여&amp;nbsp;이러한&amp;nbsp;리소스와&amp;nbsp;HTTP&amp;nbsp;요청/응답에&amp;nbsp;대한&amp;nbsp;세부&amp;nbsp;내용을&amp;nbsp;문서화하는데&amp;nbsp;편리한&amp;nbsp;기능을&amp;nbsp;제공한다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;Swagger&amp;nbsp;란?&lt;/b&gt;&lt;br /&gt;&amp;nbsp;RESTful&amp;nbsp;API&amp;nbsp;를&amp;nbsp;약속된&amp;nbsp;규칙에&amp;nbsp;따라&amp;nbsp;json&amp;nbsp;이나&amp;nbsp;yaml&amp;nbsp;형식으로&amp;nbsp;문서화한&amp;nbsp;Open&amp;nbsp;API&amp;nbsp;Specification(OAS)&amp;nbsp;라는게&amp;nbsp;있는데&amp;nbsp;이&amp;nbsp;문서를&amp;nbsp;관리하기위한&amp;nbsp;목적으로&amp;nbsp;만들어진&amp;nbsp;프레임워크다.&lt;br /&gt;&amp;nbsp;API&amp;nbsp;문서&amp;nbsp;자동화&amp;nbsp;도구로&amp;nbsp;Spring&amp;nbsp;REST&amp;nbsp;Docs&amp;nbsp;의&amp;nbsp;비교군으로&amp;nbsp;자주&amp;nbsp;등장하는데&amp;nbsp;Spring&amp;nbsp;을&amp;nbsp;사용할&amp;nbsp;경우&amp;nbsp;서비스&amp;nbsp;코드에&amp;nbsp;어노테이션만&amp;nbsp;추가하면&amp;nbsp;멋진&amp;nbsp;API&amp;nbsp;문서&amp;nbsp;하나가&amp;nbsp;뚝딱&amp;nbsp;만들어지는&amp;nbsp;신기한&amp;nbsp;도구다.&lt;br /&gt;&amp;nbsp;게다가&amp;nbsp;실제로&amp;nbsp;API&amp;nbsp;를&amp;nbsp;요청해&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;기능도 있기&amp;nbsp;때문에&amp;nbsp;API&amp;nbsp;를&amp;nbsp;어떻게&amp;nbsp;요청하고&amp;nbsp;어떤&amp;nbsp;응답을&amp;nbsp;내려주는지도&amp;nbsp;테스트가&amp;nbsp;가능하다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;이걸&amp;nbsp;하게&amp;nbsp;된&amp;nbsp;이유?&lt;/b&gt;&lt;br /&gt;&amp;nbsp;-&amp;nbsp;wiki&amp;nbsp;페이지를&amp;nbsp;통해&amp;nbsp;개발한&amp;nbsp;API&amp;nbsp;문서를&amp;nbsp;정리하고&amp;nbsp;타&amp;nbsp;부서에&amp;nbsp;공유하였는데&amp;nbsp;변경사항이&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;br /&gt;-&amp;nbsp;Swagger&amp;nbsp;를&amp;nbsp;통해&amp;nbsp;API&amp;nbsp;문서를&amp;nbsp;제공해보니&amp;nbsp;API&amp;nbsp;에&amp;nbsp;대한&amp;nbsp;테스트도&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있고&amp;nbsp;코드가&amp;nbsp;변경될때&amp;nbsp;같이&amp;nbsp;변경할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;장점이&amp;nbsp;있으나&amp;nbsp;실제&amp;nbsp;코드에&amp;nbsp;API&amp;nbsp;문서를&amp;nbsp;작성하기&amp;nbsp;위한&amp;nbsp;코드를&amp;nbsp;같이&amp;nbsp;작성해야했고&amp;nbsp;검증되지&amp;nbsp;않은(정상작동하지&amp;nbsp;않는)&amp;nbsp;API&amp;nbsp;가&amp;nbsp;노출될&amp;nbsp;수&amp;nbsp;있었다.&lt;br /&gt;-&amp;nbsp;Spring&amp;nbsp;Rest&amp;nbsp;Docs&amp;nbsp;를&amp;nbsp;써보니&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;통해&amp;nbsp;API&amp;nbsp;문서를&amp;nbsp;작성할&amp;nbsp;수&amp;nbsp;있어&amp;nbsp;검증된&amp;nbsp;API&amp;nbsp;만&amp;nbsp;문서를&amp;nbsp;만들수&amp;nbsp;있고&amp;nbsp;실제&amp;nbsp;코드는&amp;nbsp;API&amp;nbsp;문서에&amp;nbsp;관련된&amp;nbsp;코드를&amp;nbsp;작성하지&amp;nbsp;않아서&amp;nbsp;좋았지만&amp;nbsp;Swagger&amp;nbsp;에서&amp;nbsp;제공해주는&amp;nbsp;API&amp;nbsp;호출&amp;nbsp;기능이&amp;nbsp;없어서&amp;nbsp;아쉬웠다.(개인적인&amp;nbsp;생각이지만&amp;nbsp;UI&amp;nbsp;도&amp;nbsp;Swagger&amp;nbsp;가&amp;nbsp;더&amp;nbsp;좋다고&amp;nbsp;느꼈다)&lt;br /&gt;-&amp;nbsp;좀&amp;nbsp;더&amp;nbsp;좋은&amp;nbsp;방법이&amp;nbsp;없을까를&amp;nbsp;찾던&amp;nbsp;중&amp;nbsp;링크로&amp;nbsp;걸어둔&amp;nbsp;내용을&amp;nbsp;보게&amp;nbsp;되었고&amp;nbsp;팀&amp;nbsp;프로젝트&amp;nbsp;상황에&amp;nbsp;맞게&amp;nbsp;커스터마이징&amp;nbsp;하여&amp;nbsp;적용해&amp;nbsp;보았다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;좋았던&amp;nbsp;점&lt;/b&gt;&lt;br /&gt;-&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;통해&amp;nbsp;어느정도&amp;nbsp;검증된&amp;nbsp;API&amp;nbsp;가&amp;nbsp;문서에&amp;nbsp;반영될&amp;nbsp;수&amp;nbsp;있다는&amp;nbsp;점(+&amp;nbsp;이를&amp;nbsp;Swagger&amp;nbsp;로&amp;nbsp;실행해&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있다는&amp;nbsp;점).&lt;br /&gt;-&amp;nbsp;API에&amp;nbsp;변경이&amp;nbsp;발생하면&amp;nbsp;자연스럽게&amp;nbsp;테스트&amp;nbsp;코드도&amp;nbsp;변경이&amp;nbsp;필요하게되고&amp;nbsp;이로&amp;nbsp;인해&amp;nbsp;API&amp;nbsp;문서&amp;nbsp;변경도&amp;nbsp;자연스럽게&amp;nbsp;발생한다는&amp;nbsp;점.(단,&amp;nbsp;API&amp;nbsp;문서까지&amp;nbsp;자연스럽게&amp;nbsp;변경되려면&amp;nbsp;빌드/배포&amp;nbsp;구조까지&amp;nbsp;잘&amp;nbsp;잡혀져야&amp;nbsp;한다)&lt;br /&gt;-&amp;nbsp;Swagger&amp;nbsp;특성상&amp;nbsp;여러&amp;nbsp;API&amp;nbsp;문서들을&amp;nbsp;같이&amp;nbsp;관리할&amp;nbsp;수&amp;nbsp;있어&amp;nbsp;통합&amp;nbsp;API&amp;nbsp;문서를&amp;nbsp;구축할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;이를&amp;nbsp;통해&amp;nbsp;각&amp;nbsp;컴포넌트&amp;nbsp;별로&amp;nbsp;Swagger&amp;nbsp;URL&amp;nbsp;이&amp;nbsp;따로&amp;nbsp;있지&amp;nbsp;않아도&amp;nbsp;되어&amp;nbsp;타&amp;nbsp;부서에&amp;nbsp;공유할&amp;nbsp;때&amp;nbsp;편했다.(wiki&amp;nbsp;로&amp;nbsp;치면&amp;nbsp;API&amp;nbsp;문서&amp;nbsp;부모&amp;nbsp;페이지&amp;nbsp;하나만&amp;nbsp;공유하면&amp;nbsp;되는것과&amp;nbsp;동일)&lt;br /&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;아쉬운&amp;nbsp;점&lt;/b&gt;&lt;br /&gt;-&amp;nbsp;Spring&amp;nbsp;Rest&amp;nbsp;Docs&amp;nbsp;를&amp;nbsp;쓰기&amp;nbsp;때문에&amp;nbsp;어쩔수&amp;nbsp;없는&amp;nbsp;한계인가&amp;nbsp;싶긴&amp;nbsp;하지만&amp;nbsp;테스트&amp;nbsp;코드&amp;nbsp;작성시&amp;nbsp;API&amp;nbsp;문서를&amp;nbsp;작성하기&amp;nbsp;위한&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;추가로&amp;nbsp;작성해야&amp;nbsp;한다.&lt;br /&gt;-&amp;nbsp;여러가지&amp;nbsp;검증을&amp;nbsp;위한&amp;nbsp;테스트&amp;nbsp;코드를&amp;nbsp;작성하다보면&amp;nbsp;API&amp;nbsp;문서에&amp;nbsp;영향을&amp;nbsp;줄&amp;nbsp;수&amp;nbsp;있기&amp;nbsp;때문에&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;br /&gt;-&amp;nbsp;가끔씩&amp;nbsp;타&amp;nbsp;부서에서&amp;nbsp;미리&amp;nbsp;코드를&amp;nbsp;작성할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;API&amp;nbsp;Interface&amp;nbsp;공유가&amp;nbsp;불가능하냐는&amp;nbsp;문의가&amp;nbsp;들어오면&amp;nbsp;답이&amp;nbsp;없다.(이럴땐&amp;nbsp;어쩔수&amp;nbsp;없이&amp;nbsp;우리쪽&amp;nbsp;API&amp;nbsp;설계&amp;nbsp;문서를&amp;nbsp;공유해주곤&amp;nbsp;한다)&lt;br /&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;Reference&lt;/b&gt;&lt;br /&gt;-&amp;nbsp;Swagger:&amp;nbsp;&lt;a href=&quot;https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/&lt;/a&gt;&lt;br /&gt;-&amp;nbsp;Spring&amp;nbsp;Rest&amp;nbsp;Docs:&amp;nbsp;&lt;a href=&quot;https://docs.spring.io/spring-restdocs/docs/current/reference/html5/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-restdocs/docs/current/reference/html5/&lt;/a&gt;&lt;br /&gt;-&amp;nbsp;Rest&amp;nbsp;Docs&amp;nbsp;API&amp;nbsp;specification&amp;nbsp;Integration:&amp;nbsp;&lt;a href=&quot;https://github.com/ePages-de/restdocs-api-spec&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/ePages-de/restdocs-api-spec&lt;/a&gt;&lt;br /&gt;-&amp;nbsp;Restdocs&amp;nbsp;Spec&amp;nbsp;Generation&amp;nbsp;Support:&amp;nbsp;&lt;a href=&quot;https://github.com/BerkleyTechnologyServices/restdocs-spec&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://github.com/BerkleyTechnologyServices/restdocs-spec&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1661495413102&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - BerkleyTechnologyServices/restdocs-spec: A maven plugin for generating Open API and Postman Collection specifications u&quot; data-og-description=&quot;A maven plugin for generating Open API and Postman Collection specifications using Spring Restdocs. - GitHub - BerkleyTechnologyServices/restdocs-spec: A maven plugin for generating Open API and Po...&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/BerkleyTechnologyServices/restdocs-spec&quot; data-og-url=&quot;https://github.com/BerkleyTechnologyServices/restdocs-spec&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/lMhaI/hyPzmzAFg0/4kB1fEuG4vaHYUWqBbWBY1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/BerkleyTechnologyServices/restdocs-spec&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/BerkleyTechnologyServices/restdocs-spec&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/lMhaI/hyPzmzAFg0/4kB1fEuG4vaHYUWqBbWBY1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&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;GitHub - BerkleyTechnologyServices/restdocs-spec: A maven plugin for generating Open API and Postman Collection specifications u&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A maven plugin for generating Open API and Postman Collection specifications using Spring Restdocs. - GitHub - BerkleyTechnologyServices/restdocs-spec: A maven plugin for generating Open API and Po...&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&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;- [NHN Forward 2020] MSA 환경에서 API 문서 관리하기: 생성부터 배포까지: https://www.youtube.com/watch?v=qguXHW0s8RY&lt;/p&gt;</description>
      <category>Spring Framework</category>
      <category>API문서</category>
      <category>restdocs</category>
      <category>spring</category>
      <category>swagger</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/28</guid>
      <comments>https://yang1s.tistory.com/28#entry28comment</comments>
      <pubDate>Fri, 26 Aug 2022 15:31:41 +0900</pubDate>
    </item>
    <item>
      <title>  Kafka 와 Zookeeper 의 헤어질 결심. 그 결말은??</title>
      <link>https://yang1s.tistory.com/27</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp;Kafka&amp;nbsp;와&amp;nbsp;Zookeeper&amp;nbsp;의&amp;nbsp;관계?&lt;br /&gt;떼려야&amp;nbsp;뗄&amp;nbsp;수&amp;nbsp;없는&amp;nbsp;관계다.&lt;br /&gt;Kafka&amp;nbsp;는&amp;nbsp;Zookeeper&amp;nbsp;를&amp;nbsp;통해&amp;nbsp;필요한&amp;nbsp;Metadata&amp;nbsp;를&amp;nbsp;안전하게&amp;nbsp;저장하고&amp;nbsp;Cluster&amp;nbsp;운영을&amp;nbsp;위한&amp;nbsp;리더&amp;nbsp;및&amp;nbsp;파티션&amp;nbsp;오너를&amp;nbsp;선출하고&amp;nbsp;있다.&amp;nbsp;&lt;br /&gt;이러한&amp;nbsp;관계로&amp;nbsp;Kafka&amp;nbsp;를&amp;nbsp;운영하기&amp;nbsp;위해서는&amp;nbsp;반드시&amp;nbsp;Zookeeper&amp;nbsp;가&amp;nbsp;구축되어야&amp;nbsp;하고&amp;nbsp;운영단계에서는&amp;nbsp;Kafka&amp;nbsp;가&amp;nbsp;사용하는&amp;nbsp;Zookeeper&amp;nbsp;를&amp;nbsp;다른&amp;nbsp;솔루션과&amp;nbsp;함께&amp;nbsp;사용하지&amp;nbsp;말라고&amp;nbsp;권장하고&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt; &amp;nbsp;Kafka&amp;nbsp;는&amp;nbsp;왜&amp;nbsp;Zookeeper&amp;nbsp;와&amp;nbsp;헤어지려고&amp;nbsp;하는가?&lt;br /&gt;다음과&amp;nbsp;같은&amp;nbsp;몇가지&amp;nbsp;이유가&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;1️⃣&amp;nbsp;Kafka&amp;nbsp;를&amp;nbsp;쓰려면&amp;nbsp;Zookeeper&amp;nbsp;도&amp;nbsp;같이&amp;nbsp;알아야&amp;nbsp;한다.&lt;br /&gt;-&amp;nbsp;처음&amp;nbsp;Kafka&amp;nbsp;를&amp;nbsp;배우는&amp;nbsp;입장에서는&amp;nbsp;동시에&amp;nbsp;2가지의&amp;nbsp;솔루션을&amp;nbsp;배워야&amp;nbsp;한다는&amp;nbsp;것에&amp;nbsp;진입장벽을&amp;nbsp;느낄&amp;nbsp;수&amp;nbsp;있다.&lt;br /&gt;-&amp;nbsp;서로&amp;nbsp;다른&amp;nbsp;두개의&amp;nbsp;분산&amp;nbsp;시스템을&amp;nbsp;같이&amp;nbsp;운영하고&amp;nbsp;관리&amp;nbsp;및&amp;nbsp;배포를&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;학습이&amp;nbsp;되고&amp;nbsp;준비가&amp;nbsp;되어야&amp;nbsp;하는데&amp;nbsp;이는&amp;nbsp;처음&amp;nbsp;진입하는&amp;nbsp;사람&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;br /&gt;&lt;br /&gt;2️⃣&amp;nbsp;보안&amp;nbsp;설정시&amp;nbsp;Zookeeper&amp;nbsp;도&amp;nbsp;같이&amp;nbsp;설정해줘야&amp;nbsp;한다.&lt;br /&gt;-&amp;nbsp;1번&amp;nbsp;내용과&amp;nbsp;비슷한&amp;nbsp;맥락이지만&amp;nbsp;완벽하게&amp;nbsp;Kafka&amp;nbsp;Cluster&amp;nbsp;에&amp;nbsp;대한&amp;nbsp;보안&amp;nbsp;설정을&amp;nbsp;적용하려면&amp;nbsp;Zookeeper&amp;nbsp;까지&amp;nbsp;같이&amp;nbsp;해줘야&amp;nbsp;한다.&lt;br /&gt;-&amp;nbsp;만약&amp;nbsp;이러한&amp;nbsp;부분을&amp;nbsp;놓쳐서&amp;nbsp;Kafka&amp;nbsp;만&amp;nbsp;진행했을&amp;nbsp;경우&amp;nbsp;보안에&amp;nbsp;이슈가&amp;nbsp;발생할&amp;nbsp;가능성이&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;3️⃣&amp;nbsp;Zookeeper&amp;nbsp;를&amp;nbsp;운영하는&amp;nbsp;서버&amp;nbsp;리소스가&amp;nbsp;아깝다.&lt;br /&gt;-&amp;nbsp;안전하게&amp;nbsp;운영하길&amp;nbsp;원한다면&amp;nbsp;Zookeeper&amp;nbsp;를&amp;nbsp;최소&amp;nbsp;5대&amp;nbsp;이상&amp;nbsp;구축할것을&amp;nbsp;권장하고&amp;nbsp;있다.&amp;nbsp;만약&amp;nbsp;이렇게&amp;nbsp;구축한&amp;nbsp;Zookeeper&amp;nbsp;노드를&amp;nbsp;모두&amp;nbsp;Kafka&amp;nbsp;로&amp;nbsp;바꿀&amp;nbsp;수&amp;nbsp;있다면&amp;nbsp;Kafka&amp;nbsp;Cluster&amp;nbsp;에서&amp;nbsp;운영할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;토픽이나&amp;nbsp;파티션수가&amp;nbsp;증가하게&amp;nbsp;된다.&lt;br /&gt;-&amp;nbsp;간단하게&amp;nbsp;테스트&amp;nbsp;목적으로&amp;nbsp;Single&amp;nbsp;노드를&amp;nbsp;구축&amp;nbsp;하고싶어도&amp;nbsp;Zookeeper&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;br /&gt;&lt;br /&gt;4️⃣&amp;nbsp;Kafka&amp;nbsp;와&amp;nbsp;Zookeeper&amp;nbsp;간의&amp;nbsp;데이터&amp;nbsp;불일치가&amp;nbsp;발생한다.&lt;br /&gt;-&amp;nbsp;이게&amp;nbsp;아마&amp;nbsp;가장&amp;nbsp;큰&amp;nbsp;이유이지&amp;nbsp;않을까&amp;nbsp;싶은데,&amp;nbsp;몇가지&amp;nbsp;Metadata&amp;nbsp;는&amp;nbsp;Zookeeper&amp;nbsp;에&amp;nbsp;기록하지만&amp;nbsp;Kafka&amp;nbsp;에서도&amp;nbsp;메모리에&amp;nbsp;기록하는데&amp;nbsp;이때&amp;nbsp;Kafka&amp;nbsp;와&amp;nbsp;Zookeeper&amp;nbsp;간에&amp;nbsp;데이터&amp;nbsp;불일치가&amp;nbsp;발생하면&amp;nbsp;운영에&amp;nbsp;크리티컬한&amp;nbsp;이슈가&amp;nbsp;발생한다.&lt;br /&gt;-&amp;nbsp;이러한&amp;nbsp;이슈를&amp;nbsp;피하기&amp;nbsp;위해&amp;nbsp;Zookeeper&amp;nbsp;의&amp;nbsp;znode&amp;nbsp;를&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;br /&gt;&lt;br /&gt; &amp;nbsp;Kafka&amp;nbsp;는&amp;nbsp;Zookeeper&amp;nbsp;와&amp;nbsp;어떻게&amp;nbsp;헤어지려고&amp;nbsp;하는가?&lt;br /&gt;https://careerly.co.kr/comments/64092?utm_campaign=self-share&amp;nbsp;에서도&amp;nbsp;언급했던&amp;nbsp;Raft&amp;nbsp;라는&amp;nbsp;알고리즘으로&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;br /&gt;즉,&amp;nbsp;기존에&amp;nbsp;Zookeeper&amp;nbsp;를&amp;nbsp;통해&amp;nbsp;사용하던&amp;nbsp;ZAB&amp;nbsp;알고리즘을&amp;nbsp;Raft&amp;nbsp;알고리즘으로&amp;nbsp;바꾸는거라&amp;nbsp;생각하면&amp;nbsp;된다.&lt;br /&gt;최소&amp;nbsp;3대&amp;nbsp;이상의&amp;nbsp;쿼럼&amp;nbsp;노드가&amp;nbsp;있고&amp;nbsp;이중에&amp;nbsp;컨트롤러와&amp;nbsp;Metadata&amp;nbsp;복제&amp;nbsp;및&amp;nbsp;컨트롤러&amp;nbsp;상태를&amp;nbsp;체크하는&amp;nbsp;팔로워&amp;nbsp;노드를&amp;nbsp;둔다.&amp;nbsp;&lt;br /&gt;쿼럼&amp;nbsp;노드들은&amp;nbsp;Raft&amp;nbsp;알고리즘으로&amp;nbsp;구현한&amp;nbsp;이벤트&amp;nbsp;전파&amp;nbsp;및&amp;nbsp;Metadata&amp;nbsp;복제,&amp;nbsp;리더&amp;nbsp;선출등을&amp;nbsp;하게&amp;nbsp;된다.&amp;nbsp;&lt;br /&gt;Zookeeper&amp;nbsp;와&amp;nbsp;마찬가지로&amp;nbsp;쿼럼&amp;nbsp;노드가&amp;nbsp;많으면&amp;nbsp;많을수록&amp;nbsp;장애에&amp;nbsp;대한&amp;nbsp;복구&amp;nbsp;가능성이&amp;nbsp;커진다.&lt;br /&gt;변경되는&amp;nbsp;항목중에&amp;nbsp;알고리즘&amp;nbsp;외에&amp;nbsp;흥미로웠던&amp;nbsp;부분은&amp;nbsp;기존에는&amp;nbsp;Metadata&amp;nbsp;에&amp;nbsp;대한&amp;nbsp;변경이&amp;nbsp;발생하면&amp;nbsp;컨트롤러&amp;nbsp;노드가&amp;nbsp;나머지&amp;nbsp;노드들에게&amp;nbsp;변경사항을&amp;nbsp;Push&amp;nbsp;하는&amp;nbsp;구조였는데&amp;nbsp;이게&amp;nbsp;Poll&amp;nbsp;형태로&amp;nbsp;바뀐다는&amp;nbsp;점이다.&lt;br /&gt;&lt;br /&gt; &amp;nbsp;결말은?&lt;br /&gt;2019년&amp;nbsp;7월에&amp;nbsp;이&amp;nbsp;제안이&amp;nbsp;나왔고&amp;nbsp;그로부터&amp;nbsp;2년뒤인&amp;nbsp;2021년&amp;nbsp;4월에&amp;nbsp;2.8&amp;nbsp;버전으로&amp;nbsp;얼리&amp;nbsp;엑세스&amp;nbsp;빌드가&amp;nbsp;릴리즈&amp;nbsp;되었다.&lt;br /&gt;현재&amp;nbsp;3.2&amp;nbsp;버전까지&amp;nbsp;출시가&amp;nbsp;되었지만&amp;nbsp;아직까지&amp;nbsp;운영(Production)단계에서&amp;nbsp;Zookeeper&amp;nbsp;를&amp;nbsp;사용하는것을&amp;nbsp;권장하고&amp;nbsp;있다.&lt;br /&gt;개인적으로&amp;nbsp;Zookeeper&amp;nbsp;제외에&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;br /&gt;그&amp;nbsp;이유로&amp;nbsp;분산&amp;nbsp;시스템의&amp;nbsp;핵심이나&amp;nbsp;다름없는&amp;nbsp;리더&amp;nbsp;선출이나&amp;nbsp;이벤트&amp;nbsp;전파&amp;nbsp;및&amp;nbsp;Metadata&amp;nbsp;관리&amp;nbsp;알고리즘을&amp;nbsp;바꾼다는게&amp;nbsp;생각보다&amp;nbsp;쉬운일이&amp;nbsp;아니다.&amp;nbsp;더군다나&amp;nbsp;Zookeeper&amp;nbsp;에서&amp;nbsp;사용하는&amp;nbsp;ZAB&amp;nbsp;알고리즘이&amp;nbsp;아닌&amp;nbsp;Raft&amp;nbsp;알고리즘으로&amp;nbsp;바꾸는&amp;nbsp;것이니&amp;nbsp;그&amp;nbsp;간극은&amp;nbsp;더&amp;nbsp;클&amp;nbsp;수&amp;nbsp;밖에&amp;nbsp;없다.&lt;br /&gt;또한&amp;nbsp;오픈소스&amp;nbsp;특성상&amp;nbsp;많은&amp;nbsp;사람들이&amp;nbsp;써보고&amp;nbsp;경험한&amp;nbsp;다양한&amp;nbsp;이슈를&amp;nbsp;올려줘야&amp;nbsp;이를&amp;nbsp;패치하고&amp;nbsp;안정화가&amp;nbsp;진행될&amp;nbsp;수&amp;nbsp;있는데&amp;nbsp;Zookeeper&amp;nbsp;제외&amp;nbsp;모드를&amp;nbsp;운영환경에서&amp;nbsp;쓰는&amp;nbsp;사용자가&amp;nbsp;많지&amp;nbsp;않은것도&amp;nbsp;한몫&amp;nbsp;하지&amp;nbsp;않을까&amp;nbsp;싶다.&amp;nbsp;사실&amp;nbsp;이런건&amp;nbsp;Kafka&amp;nbsp;가&amp;nbsp;어느정도&amp;nbsp;안정된&amp;nbsp;상태였을때&amp;nbsp;개선하지&amp;nbsp;말고&amp;nbsp;초반(1.0&amp;nbsp;버전&amp;nbsp;전이었을때)에&amp;nbsp;했어야&amp;nbsp;하지&amp;nbsp;않았나&amp;nbsp;싶다.&lt;br /&gt;이&amp;nbsp;로맨스(?)의&amp;nbsp;결론이&amp;nbsp;어떻게&amp;nbsp;날지&amp;nbsp;귀추가&amp;nbsp;주목된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# Reference&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- https://cwiki.apache.org/confluence/plugins/servlet/mobile?contentId=123898922#content/view/123898922&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>kafka</category>
      <category>raft</category>
      <category>Zookeeper</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/27</guid>
      <comments>https://yang1s.tistory.com/27#entry27comment</comments>
      <pubDate>Fri, 26 Aug 2022 15:28:16 +0900</pubDate>
    </item>
    <item>
      <title> Java 유료화와 OpenJDK 의 오해와 진실</title>
      <link>https://yang1s.tistory.com/26</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; &amp;nbsp;Programming&amp;nbsp;언어가&amp;nbsp;유료화가&amp;nbsp;된다고?&lt;/b&gt;&lt;br /&gt;프로젝트를&amp;nbsp;Java&amp;nbsp;언어로&amp;nbsp;진행하고&amp;nbsp;있거나&amp;nbsp;이미&amp;nbsp;운영중인&amp;nbsp;사람들&amp;nbsp;입장에서는&amp;nbsp;처음&amp;nbsp;Java&amp;nbsp;유료화&amp;nbsp;라는&amp;nbsp;말을&amp;nbsp;들었을&amp;nbsp;때&amp;nbsp;헉!&amp;nbsp;할수밖에&amp;nbsp;없을&amp;nbsp;것이다.&amp;nbsp;그만큼&amp;nbsp;Java&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;br /&gt;&lt;br /&gt;결론부터&amp;nbsp;얘기하자면&amp;nbsp;Java&amp;nbsp;라는&amp;nbsp;프로그래밍&amp;nbsp;언어&amp;nbsp;자체는&amp;nbsp;GPL&amp;nbsp;라이센스로&amp;nbsp;무료이다.&amp;nbsp;&lt;br /&gt;그럼&amp;nbsp;도대체&amp;nbsp;어떤것이&amp;nbsp;유료화가&amp;nbsp;된다는&amp;nbsp;말일까?&lt;br /&gt;&lt;br /&gt;Java&amp;nbsp;프로그램을&amp;nbsp;실행하기&amp;nbsp;위해서는&amp;nbsp;아래의&amp;nbsp;2가지&amp;nbsp;핵심&amp;nbsp;기능이&amp;nbsp;필요하다&lt;br /&gt;-&amp;nbsp;Java&amp;nbsp;로&amp;nbsp;작성한&amp;nbsp;프로그램을&amp;nbsp;컴파일&amp;nbsp;해주는&amp;nbsp;JDK&lt;br /&gt;-&amp;nbsp;컴파일된&amp;nbsp;결과물을&amp;nbsp;실행시켜주는&amp;nbsp;JVM&lt;br /&gt;&lt;br /&gt;JDK&amp;nbsp;에&amp;nbsp;JVM&amp;nbsp;이&amp;nbsp;같이&amp;nbsp;패키징되어&amp;nbsp;배포되는&amp;nbsp;구조라&amp;nbsp;흔히들&amp;nbsp;JDK,&amp;nbsp;JVM&amp;nbsp;을&amp;nbsp;합쳐&amp;nbsp;JDK&amp;nbsp;라&amp;nbsp;부르는데&amp;nbsp;이&amp;nbsp;JDK&amp;nbsp;가&amp;nbsp;유료화&amp;nbsp;되는것이다.&lt;br /&gt;즉,&amp;nbsp;프로그래밍&amp;nbsp;언어로써&amp;nbsp;Java&amp;nbsp;를&amp;nbsp;사용하는건&amp;nbsp;무료이지만&amp;nbsp;이를&amp;nbsp;실행시키기위한&amp;nbsp;컴파일과&amp;nbsp;실행&amp;nbsp;머신이&amp;nbsp;유료라는&amp;nbsp;것이다.&lt;br /&gt;&lt;br /&gt;JDK&amp;nbsp;는&amp;nbsp;크게&amp;nbsp;OracleJDK&amp;nbsp;와&amp;nbsp;OpenJDK&amp;nbsp;로&amp;nbsp;나뉘어지는데&amp;nbsp;이중&amp;nbsp;OracleJDK&amp;nbsp;가&amp;nbsp;유료화&amp;nbsp;정책을&amp;nbsp;펼치고&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;OracleJDK&amp;nbsp;쓰면&amp;nbsp;무조건&amp;nbsp;돈내야해요?&lt;/b&gt;&lt;br /&gt;OracleJDK는&amp;nbsp;BCL(Oracle&amp;nbsp;Binary&amp;nbsp;Code&amp;nbsp;License)&amp;nbsp;이라는&amp;nbsp;라이센스를&amp;nbsp;적용하고&amp;nbsp;있는데&amp;nbsp;BCL&amp;nbsp;라이센스에서는&amp;nbsp;일반적인&amp;nbsp;목적의&amp;nbsp;컴퓨팅(General&amp;nbsp;Purpose&amp;nbsp;Computing)에&amp;nbsp;사용시에만&amp;nbsp;무료라고&amp;nbsp;밝히고&amp;nbsp;있다.&amp;nbsp;&lt;br /&gt;여기서&amp;nbsp;말하는&amp;nbsp;일반적인&amp;nbsp;목적의&amp;nbsp;컴퓨팅이란&amp;nbsp;데스크탑,&amp;nbsp;노트북,&amp;nbsp;스마트폰,&amp;nbsp;태플릿&amp;nbsp;등에서의&amp;nbsp;사용인&amp;nbsp;것인데&amp;nbsp;즉,&amp;nbsp;개인이&amp;nbsp;학습이나&amp;nbsp;테스트&amp;nbsp;목적으로&amp;nbsp;개인의&amp;nbsp;디바이스에&amp;nbsp;사용하는건&amp;nbsp;문제가&amp;nbsp;없다는&amp;nbsp;말이다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;반대로&amp;nbsp;특정&amp;nbsp;기능만을&amp;nbsp;위해&amp;nbsp;작동하는&amp;nbsp;시스템&amp;nbsp;및&amp;nbsp;솔루션에서&amp;nbsp;사용하는것은&amp;nbsp;일반적이지&amp;nbsp;않은&amp;nbsp;목적의&amp;nbsp;컴퓨팅이라&amp;nbsp;규정하고&amp;nbsp;있는데&amp;nbsp;이&amp;nbsp;표현이&amp;nbsp;모호하여&amp;nbsp;대부분의&amp;nbsp;상업&amp;nbsp;프로젝트를&amp;nbsp;진행하는&amp;nbsp;회사의&amp;nbsp;경우&amp;nbsp;라이센스&amp;nbsp;비용을&amp;nbsp;지불한다고&amp;nbsp;생각하는게&amp;nbsp;좋다.&lt;br /&gt;&lt;br /&gt;그렇지만&amp;nbsp;모든&amp;nbsp;OracleJDK&amp;nbsp;가&amp;nbsp;유료인것은&amp;nbsp;아니고&amp;nbsp;2019년&amp;nbsp;01월&amp;nbsp;15일에&amp;nbsp;공개된&amp;nbsp;JDK&amp;nbsp;8u202&amp;nbsp;버전까지는&amp;nbsp;무료로&amp;nbsp;제공해주고&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;OpenJDK&amp;nbsp;는&amp;nbsp;유료화&amp;nbsp;안되나요?&lt;/b&gt;&lt;br /&gt;OpenJDK&amp;nbsp;는&amp;nbsp;JDK(Java&amp;nbsp;SE)&amp;nbsp;의&amp;nbsp;오픈소스&amp;nbsp;구현체로&amp;nbsp;GPL&amp;nbsp;v2&amp;nbsp;with&amp;nbsp;the&amp;nbsp;Classpath&amp;nbsp;Exception&amp;nbsp;라이센스가&amp;nbsp;적용되어&amp;nbsp;무료이다.&lt;br /&gt;많은&amp;nbsp;상업&amp;nbsp;벤더들이&amp;nbsp;구현체&amp;nbsp;개발에&amp;nbsp;참여하여&amp;nbsp;다양한&amp;nbsp;종류의&amp;nbsp;OpenJDK&amp;nbsp;가&amp;nbsp;존재하고&amp;nbsp;무료로&amp;nbsp;배포되고&amp;nbsp;있다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;OpenJDK&amp;nbsp;는&amp;nbsp;OracleJDK&amp;nbsp;보다&amp;nbsp;성능이&amp;nbsp;떨어진다?&lt;/b&gt;&lt;br /&gt;Jave&amp;nbsp;SE&amp;nbsp;6&amp;nbsp;버전의&amp;nbsp;OpenJDK는&amp;nbsp;백포트&amp;nbsp;방식으로&amp;nbsp;구현되어&amp;nbsp;성능&amp;nbsp;저하&amp;nbsp;이슈가&amp;nbsp;존재했었다.&amp;nbsp;이로인해&amp;nbsp;OpenJDK&amp;nbsp;가&amp;nbsp;OracleJDK&amp;nbsp;에&amp;nbsp;비해&amp;nbsp;성능이&amp;nbsp;떨어질것이라는&amp;nbsp;인식이&amp;nbsp;생겼는데&amp;nbsp;Java&amp;nbsp;SE&amp;nbsp;7부터&amp;nbsp;모든&amp;nbsp;JDK는&amp;nbsp;OpenJDK&amp;nbsp;레퍼런스&amp;nbsp;소스&amp;nbsp;코드를&amp;nbsp;기반으로&amp;nbsp;제작되도록&amp;nbsp;변경되어&amp;nbsp;두&amp;nbsp;JDK&amp;nbsp;간에&amp;nbsp;성능차이가&amp;nbsp;거의&amp;nbsp;없어졌다.&lt;br /&gt;또한&amp;nbsp;오라클이&amp;nbsp;아닌&amp;nbsp;서드파티&amp;nbsp;업체가&amp;nbsp;OpenJDK를&amp;nbsp;기반으로&amp;nbsp;공인된&amp;nbsp;JDK를&amp;nbsp;제작하여&amp;nbsp;배포하려면&amp;nbsp;오라클의&amp;nbsp;엄격한&amp;nbsp;TCK&amp;nbsp;인증을&amp;nbsp;통과해야&amp;nbsp;한다.&amp;nbsp;수만가지의&amp;nbsp;테스트를&amp;nbsp;단&amp;nbsp;하나라도&amp;nbsp;통과하지&amp;nbsp;못하면&amp;nbsp;인증되지&amp;nbsp;않는다.&amp;nbsp;이러한&amp;nbsp;프로세스로&amp;nbsp;인해&amp;nbsp;OpenJDK&amp;nbsp;가&amp;nbsp;OracleJDK&amp;nbsp;보다&amp;nbsp;이슈나&amp;nbsp;트러블이&amp;nbsp;발생할&amp;nbsp;일은&amp;nbsp;없어졌다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;그러면&amp;nbsp;무조건&amp;nbsp;OpenJDK&amp;nbsp;쓰면&amp;nbsp;되는거&amp;nbsp;아닌가요?&lt;/b&gt;&lt;br /&gt;일반적인&amp;nbsp;서버&amp;nbsp;어플리케이션이나&amp;nbsp;오픈소스&amp;nbsp;솔루션은&amp;nbsp;OpenJDK&amp;nbsp;로&amp;nbsp;운영이&amp;nbsp;가능하기&amp;nbsp;때문에&amp;nbsp;굳이&amp;nbsp;OracleJDK&amp;nbsp;를&amp;nbsp;쓸&amp;nbsp;필요는&amp;nbsp;없다.&amp;nbsp;&lt;br /&gt;하지만&amp;nbsp;글꼴&amp;nbsp;라이브러리나&amp;nbsp;Java&amp;nbsp;Web&amp;nbsp;Start&amp;nbsp;와&amp;nbsp;같이&amp;nbsp;Oracle이&amp;nbsp;재산권을&amp;nbsp;보유하고&amp;nbsp;있는&amp;nbsp;플러그인&amp;nbsp;기능을&amp;nbsp;써야하는&amp;nbsp;경우&amp;nbsp;OpenJDK&amp;nbsp;에는&amp;nbsp;해당&amp;nbsp;플러그인이&amp;nbsp;제공되지&amp;nbsp;않기&amp;nbsp;때문에&amp;nbsp;쓸&amp;nbsp;수&amp;nbsp;없다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt; &amp;nbsp;마무리&lt;/b&gt;&lt;br /&gt;이&amp;nbsp;논란이&amp;nbsp;발생한건&amp;nbsp;꽤&amp;nbsp;오래전&amp;nbsp;일이지만&amp;nbsp;아직도&amp;nbsp;자세히&amp;nbsp;알지&amp;nbsp;못하는&amp;nbsp;사람들이&amp;nbsp;있는것&amp;nbsp;같아&amp;nbsp;자세히&amp;nbsp;정리된&amp;nbsp;블로그를&amp;nbsp;참고하여&amp;nbsp;정리해보았다.&lt;br /&gt;이&amp;nbsp;논란이&amp;nbsp;한창&amp;nbsp;진행될때는&amp;nbsp;JDK&amp;nbsp;를&amp;nbsp;유료화하는&amp;nbsp;오라클을&amp;nbsp;단순히&amp;nbsp;미워(?)했지만&amp;nbsp;지금&amp;nbsp;생각해보면&amp;nbsp;OracleJDK&amp;nbsp;를&amp;nbsp;통해&amp;nbsp;수익구조를&amp;nbsp;만들었지만&amp;nbsp;동시에&amp;nbsp;OpenJDK&amp;nbsp;에&amp;nbsp;대한&amp;nbsp;운영도&amp;nbsp;주체가&amp;nbsp;되어&amp;nbsp;공존시킴으로써&amp;nbsp;Java&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;br /&gt;이런&amp;nbsp;똑똑한&amp;nbsp;전략이&amp;nbsp;있었기에&amp;nbsp;오늘날에도&amp;nbsp;Java&amp;nbsp;가&amp;nbsp;인기있는&amp;nbsp;언어로&amp;nbsp;손꼽히는게&amp;nbsp;아닐까&amp;nbsp;싶다.&lt;/p&gt;</description>
      <category>Java</category>
      <category>java</category>
      <category>JDK</category>
      <category>JVM</category>
      <category>OpenJDK</category>
      <category>Oracle</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/26</guid>
      <comments>https://yang1s.tistory.com/26#entry26comment</comments>
      <pubDate>Fri, 26 Aug 2022 15:22:20 +0900</pubDate>
    </item>
    <item>
      <title>LinkedIn 의 brooklin 활용</title>
      <link>https://yang1s.tistory.com/25</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근에&amp;nbsp;흥미로웠던&amp;nbsp;LinkedIn&amp;nbsp;에서&amp;nbsp;Brooklin&amp;nbsp;을&amp;nbsp;활용한&amp;nbsp;Mirror&amp;nbsp;Maker&amp;nbsp;를&amp;nbsp;소개하고자&amp;nbsp;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 Kafka Cluster 간 Message 를 Mirroring 하는 방법은 다양하다. 대표적으로 Mirror Maker 라는 오픈소스가 있고(이건 v2 가 release 되었다), Kafka Connector 나 오늘 소개할 Brooklin, 그리고 직접 Consumer 와 Producer 를 구현해서 만들수도 있다.&lt;br /&gt;&amp;nbsp;필자가&amp;nbsp;Kafka&amp;nbsp;Mirroring&amp;nbsp;과&amp;nbsp;관련된&amp;nbsp;모든&amp;nbsp;오픈소스를&amp;nbsp;확인해본건&amp;nbsp;아니지만,&amp;nbsp;적어도&amp;nbsp;위에서&amp;nbsp;언급한&amp;nbsp;방법들에&amp;nbsp;대해서만&amp;nbsp;한정하자면&amp;nbsp;모두&amp;nbsp;공통적으로&amp;nbsp;Source&amp;nbsp;Cluster&amp;nbsp;에서&amp;nbsp;message&amp;nbsp;를&amp;nbsp;Consuming&amp;nbsp;하고,&amp;nbsp;이를&amp;nbsp;Target&amp;nbsp;Cluster&amp;nbsp;로&amp;nbsp;Publish&amp;nbsp;하는&amp;nbsp;방식을&amp;nbsp;기본으로&amp;nbsp;한다.&lt;br /&gt;&amp;nbsp;그래서&amp;nbsp;사실&amp;nbsp;어떤&amp;nbsp;오픈&amp;nbsp;소스를&amp;nbsp;쓰거나&amp;nbsp;혹은&amp;nbsp;직접&amp;nbsp;구현하는게&amp;nbsp;크게&amp;nbsp;특별한&amp;nbsp;차이가&amp;nbsp;존재하지&amp;nbsp;않을거라&amp;nbsp;생각했는데&amp;nbsp;Linkedin&amp;nbsp;의&amp;nbsp;Brooklin&amp;nbsp;활용&amp;nbsp;방식을&amp;nbsp;보고&amp;nbsp;내&amp;nbsp;생각이&amp;nbsp;짧았음을&amp;nbsp;느끼게&amp;nbsp;되었다.&lt;br /&gt;&amp;nbsp;LinkedIn&amp;nbsp;의&amp;nbsp;Brooklin&amp;nbsp;활용&amp;nbsp;방식을&amp;nbsp;통해&amp;nbsp;인상깊게&amp;nbsp;느꼈던&amp;nbsp;부분은&amp;nbsp;다음과&amp;nbsp;같다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;1.&amp;nbsp;Kafka&amp;nbsp;에서&amp;nbsp;제공하는&amp;nbsp;Metric&amp;nbsp;을&amp;nbsp;자동화&amp;nbsp;시스템을&amp;nbsp;위한&amp;nbsp;지표로&amp;nbsp;활용한&amp;nbsp;점.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;Kafka&amp;nbsp;에서&amp;nbsp;제공하는&amp;nbsp;JMX&amp;nbsp;metric&amp;nbsp;중에는&amp;nbsp;Topic&amp;nbsp;의&amp;nbsp;Partition&amp;nbsp;단위로&amp;nbsp;traffic&amp;nbsp;이&amp;nbsp;얼마나&amp;nbsp;되는지&amp;nbsp;볼&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;metric&amp;nbsp;이&amp;nbsp;있는데&amp;nbsp;LinkedIn&amp;nbsp;에서는&amp;nbsp;이&amp;nbsp;metric&amp;nbsp;을&amp;nbsp;활용하여&amp;nbsp;Mirroring&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;br /&gt;&amp;nbsp;필자는&amp;nbsp;이&amp;nbsp;Metric&amp;nbsp;을&amp;nbsp;단순히&amp;nbsp;Kafka&amp;nbsp;가&amp;nbsp;받고&amp;nbsp;있는&amp;nbsp;Traffic&amp;nbsp;이&amp;nbsp;어느정도인지&amp;nbsp;모니터링&amp;nbsp;하는&amp;nbsp;용도로&amp;nbsp;사용할&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;br /&gt;&lt;br /&gt;&lt;b&gt;2.&amp;nbsp;자신들의&amp;nbsp;목표를&amp;nbsp;위해&amp;nbsp;Kafka&amp;nbsp;가&amp;nbsp;제공해주는&amp;nbsp;기능을&amp;nbsp;활용하지&amp;nbsp;않은&amp;nbsp;점.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;Kafka&amp;nbsp;가&amp;nbsp;제공해주는&amp;nbsp;기능중에&amp;nbsp;Consumer&amp;nbsp;의&amp;nbsp;Partition&amp;nbsp;Ownership&amp;nbsp;을&amp;nbsp;각&amp;nbsp;Consumer&amp;nbsp;에&amp;nbsp;적절히&amp;nbsp;분산시켜주는&amp;nbsp;기능이&amp;nbsp;있는데&amp;nbsp;LinkedIn&amp;nbsp;은&amp;nbsp;이를&amp;nbsp;사용하지&amp;nbsp;않았다.&lt;br /&gt;그&amp;nbsp;이유는&amp;nbsp;Kafka&amp;nbsp;가&amp;nbsp;Consumer&amp;nbsp;의&amp;nbsp;Partition&amp;nbsp;Ownership&amp;nbsp;을&amp;nbsp;분배하는&amp;nbsp;방식은&amp;nbsp;단순히&amp;nbsp;Partition&amp;nbsp;갯수를&amp;nbsp;기반으로&amp;nbsp;균등하게&amp;nbsp;분배하는&amp;nbsp;방식이기&amp;nbsp;때문에&amp;nbsp;파티션별&amp;nbsp;트래픽이&amp;nbsp;달라질&amp;nbsp;경우&amp;nbsp;이를&amp;nbsp;컨트롤&amp;nbsp;할&amp;nbsp;수&amp;nbsp;없다.&amp;nbsp;즉,&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;br /&gt;&amp;nbsp;그래서&amp;nbsp;LinkedIn&amp;nbsp;에서는&amp;nbsp;Brooklin&amp;nbsp;에서&amp;nbsp;Mirroring&amp;nbsp;을&amp;nbsp;위한&amp;nbsp;Datastream(흔히들&amp;nbsp;얘기하는&amp;nbsp;Job&amp;nbsp;과&amp;nbsp;비슷한&amp;nbsp;개념으로&amp;nbsp;생각하면&amp;nbsp;된다.)&amp;nbsp;을&amp;nbsp;정의할&amp;nbsp;때&amp;nbsp;Consumer&amp;nbsp;가&amp;nbsp;특정한&amp;nbsp;partition&amp;nbsp;만&amp;nbsp;assign&amp;nbsp;하도록&amp;nbsp;설정한다.&amp;nbsp;즉,&amp;nbsp;Kafka&amp;nbsp;가&amp;nbsp;자동으로&amp;nbsp;Partition&amp;nbsp;을&amp;nbsp;지정해주는게&amp;nbsp;아닌&amp;nbsp;LinkedIn&amp;nbsp;에서&amp;nbsp;직접&amp;nbsp;자신들이&amp;nbsp;원하는&amp;nbsp;Partition&amp;nbsp;에&amp;nbsp;assign&amp;nbsp;하도록&amp;nbsp;로직을&amp;nbsp;구현한&amp;nbsp;것이다.&lt;br /&gt;&amp;nbsp;Kafka&amp;nbsp;의&amp;nbsp;Partition&amp;nbsp;Ownership&amp;nbsp;분배가&amp;nbsp;얼마나&amp;nbsp;강력한&amp;nbsp;기능인지는&amp;nbsp;Kafka&amp;nbsp;를&amp;nbsp;사용해본&amp;nbsp;사람이라면&amp;nbsp;누구나&amp;nbsp;알것이다.&amp;nbsp;그런데&amp;nbsp;LinkedIn&amp;nbsp;은&amp;nbsp;자신들이&amp;nbsp;목표한&amp;nbsp;Mirroring&amp;nbsp;기능을&amp;nbsp;위해&amp;nbsp;이&amp;nbsp;기능을&amp;nbsp;포기하고&amp;nbsp;본인들이&amp;nbsp;직접&amp;nbsp;Control&amp;nbsp;하도록&amp;nbsp;변경한것이다.&lt;br /&gt;&amp;nbsp;당연하게&amp;nbsp;사용해야될&amp;nbsp;기능인줄&amp;nbsp;알았는데&amp;nbsp;이렇게&amp;nbsp;변화를&amp;nbsp;줄&amp;nbsp;수도&amp;nbsp;있다는&amp;nbsp;사실이&amp;nbsp;필자에겐&amp;nbsp;꽤나&amp;nbsp;충격적이고&amp;nbsp;신선했다.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;3.&amp;nbsp;이런&amp;nbsp;구현이&amp;nbsp;가능하도록&amp;nbsp;Brooklin&amp;nbsp;을&amp;nbsp;설계했다.&lt;/b&gt;&lt;br /&gt;&amp;nbsp;Brooklin&amp;nbsp;을&amp;nbsp;만든&amp;nbsp;목적이&amp;nbsp;이&amp;nbsp;글에서&amp;nbsp;소개한&amp;nbsp;자동화된&amp;nbsp;mirroring&amp;nbsp;을&amp;nbsp;구현하기&amp;nbsp;위한건지,&amp;nbsp;아니면&amp;nbsp;만들다보니&amp;nbsp;이렇게&amp;nbsp;활용할&amp;nbsp;수&amp;nbsp;있게&amp;nbsp;된건지는&amp;nbsp;정확하게&amp;nbsp;모르겠지만&amp;nbsp;한가지&amp;nbsp;분명한건&amp;nbsp;LinkedIn&amp;nbsp;에서&amp;nbsp;활용한&amp;nbsp;방식을&amp;nbsp;비슷하게&amp;nbsp;해볼려면&amp;nbsp;필자가&amp;nbsp;알고있는&amp;nbsp;방법중에는&amp;nbsp;Brooklin&amp;nbsp;을&amp;nbsp;쓰는게&amp;nbsp;가장&amp;nbsp;적절해&amp;nbsp;보인다는&amp;nbsp;점이다.&amp;nbsp;현재&amp;nbsp;필자가&amp;nbsp;업무를&amp;nbsp;하고&amp;nbsp;있는&amp;nbsp;팀에서도&amp;nbsp;Mirroring&amp;nbsp;을&amp;nbsp;구현할&amp;nbsp;방안으로&amp;nbsp;Brooklin&amp;nbsp;을&amp;nbsp;활용할지&amp;nbsp;검토중이다.&lt;br /&gt;&amp;nbsp;&lt;br /&gt;&amp;nbsp;이밖에&amp;nbsp;OLAP&amp;nbsp;데이터&amp;nbsp;저장소로&amp;nbsp;Apache&amp;nbsp;Pinot&amp;nbsp;과&amp;nbsp;Samza&amp;nbsp;를&amp;nbsp;통한&amp;nbsp;스트리밍&amp;nbsp;처리도&amp;nbsp;흥미로운&amp;nbsp;주제이지만&amp;nbsp;이&amp;nbsp;내용은&amp;nbsp;다음에&amp;nbsp;기회가&amp;nbsp;되면&amp;nbsp;정리해볼까&amp;nbsp;한다.(Samza&amp;nbsp;대신&amp;nbsp;Kafka&amp;nbsp;Streams&amp;nbsp;를&amp;nbsp;써도&amp;nbsp;되지&amp;nbsp;않았을까&amp;nbsp;하는&amp;nbsp;생각이&amp;nbsp;든다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;# Reference&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://engineering.linkedin.com/blog/2022/load-balanced-brooklin-mirror-maker--replicating-large-scale-kaf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://engineering.linkedin.com/blog/2022/load-balanced-brooklin-mirror-maker--replicating-large-scale-kaf&lt;/a&gt;&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>Brooklin</category>
      <category>kafka</category>
      <category>LinkedIn</category>
      <category>Mirror</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/25</guid>
      <comments>https://yang1s.tistory.com/25#entry25comment</comments>
      <pubDate>Fri, 26 Aug 2022 15:20:28 +0900</pubDate>
    </item>
    <item>
      <title>  Java 의 Project Loom 이 만들어지고 있어요! 조금만 기다려주세요!</title>
      <link>https://yang1s.tistory.com/24</link>
      <description>&lt;h2&gt;Project Loom 이란?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Project Loom 은 Java 의 동시성 처리 개선을 위해 Ron Pressler(Quasar 라는 Java 경량 스레드 라이브러리 개발자)가 제안하여 2017년에 시작된 프로젝트다.&lt;/li&gt;
&lt;li&gt;최근에 완성도 높은 형태의 얼리 엑세스 빌드가 제공되어 올해 정식 릴리즈가 되지 않을까 하는 기대감이 생겼다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Loom 의 동기  &lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;하나의 서버에서 수백만개의 소켓을 다룰 수 있지만, OS 스레드를 직접 사용하는 Java 에서는 동시에 수천개 이상의 요청을 효율적으로 다루기 어렵다. 즉, 현재 Java 의 스레드는 자원 사용면에서 효율적이지 못하다.&lt;/li&gt;
&lt;li&gt;비동기 관련 API 들이 나왔지만 사용하기 어렵고 디버깅이 쉽지 않아 유지보수에 좋지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Loom 의 목표  &lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;동기 코드를 작성하듯이 사용하기 쉽고 비동기 코드처럼 서버 자원을 효율적으로 사용할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;이를 위해 수백만개를 생성할 수 있는 경량 스레드를 정의하여 기존의 동기 코드처럼 작성해도 비용(서버 자원)이 적게 들어가도록 한다. &lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Loom 이 해결한 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;제어 흐름을 잃는 문제. 비동기 프로그래밍을 하게 되면 조건 분기나 반복문 같은 간단한 제어 흐름을 복잡하게 구성해야하는 경우가 생기고 이로 인해 콜백 헬이나 부가적인 코드가 많이 발생하게 되는데 Project Loom 은 코드를 동기식으로 짤 수 있게 만들었기 때문에 이런 문제가 발생하지 않는다.&lt;/li&gt;
&lt;li&gt;컨텍스트를 잃는 문제. 비동기 프로그래밍에서는 스레드를 넘나들며 요청이 처리되기 때문에 거쳐온 컨텍스트가 스택 트레이스에 쌓이지 않게 되고 이로 인해 스택 트레이스가 유용하지 않게 되버린다. 하지만 Project Loom 은 스레드를 넘나드는게 아닌 경량 스레드를 실행하는 구조이기 때문에 이 문제가 없어진다.&lt;/li&gt;
&lt;li&gt;함수의 색 문제. &lt;a href=&quot;https://careerly.co.kr/comments/63069?utm_campaign=self-share&quot;&gt;https://careerly.co.kr/comments/63069?utm_campaign=self-share&lt;/a&gt; 에서도 언급했던 내용인 전염성 혹은 함수의 색문제로 인해 동기/비동기 함수간에 연동이 쉽지 않고 개발단계와 유지보수에 영향을 미치게 된다. Project Loom 은 블록킹 코드로 작성되고 내부적으로 논블록킹으로 동작하기 때문에 동기/비동기 코드간에 벽이 허물어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Loom 이 해결하지 못한 것&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;비록 낮은 수준이지만 동기화 요소를 직접 다루는 컨셉이기 때문에 기존에 나와있는 비동기 처리 솔루션들의 장점인 우아하고 자연스러운 동시성 처리를 하는것에는 한계가 있어 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Loom 이 가져올 변화와 기대되는 점&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Tomcat 과 같은 Servlet 기반 웹서버의 성능이 눈에 띄게 향상될 것이고 이에 대한 사용도 늘어날 것으로 기대.&lt;/li&gt;
&lt;li&gt;Spring Webflux 사용이 줄어들 수 있을 것 같지만 Spring 에서 Project Loom 으로 어떤 멋진걸 만들지도 기대된다.&lt;/li&gt;
&lt;li&gt;코틀린 코루틴 대신 Project Loom 을 활용하는 케이스가 많아질 것 같다.&lt;/li&gt;
&lt;li&gt;Akka 와 같은 기존 Java Thread 의 한계를 극복하기 위해 나왔던 솔루션들이 Project Loom 을 어떻게 수용하고 변화할지 기대된다. &lt;/li&gt;
&lt;li&gt;Java Thread 의 한계로 다른 솔루션(Go 나 NodeJS)으로 건너간 개발자들이 다시 돌아올 수 있을지, Java 가 지금보다 더 크게 활성화 될 수 있을지 기대된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html&quot;&gt;https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Java</category>
      <category>java</category>
      <category>ProjectLoom</category>
      <category>비동기</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/24</guid>
      <comments>https://yang1s.tistory.com/24#entry24comment</comments>
      <pubDate>Fri, 26 Aug 2022 15:18:01 +0900</pubDate>
    </item>
    <item>
      <title>[VictoriaMetrics] Metric 수집(Direct Push)</title>
      <link>https://yang1s.tistory.com/23</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;InfluxDB Line Protocol&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 VictoriaMetrics 를 실행하면 8428 포트로 운영이 되고, 해당 포트에 &lt;a href=&quot;https://docs.influxdata.com/influxdb/v1.7/write_protocols/line_protocol_tutorial/&quot;&gt;InfluxDB Line Protocol&lt;/a&gt; 형식으로 data 요청이 가능하다.&lt;br /&gt;만약 InfluxDB Line Protocol 을 받을 포트를 변경하고 싶다면, &lt;code&gt;-influxListenAddr&lt;/code&gt; 옵션을 통해 변경할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/path/to/victoria-metrics-prod -influxListenAddr=:8428&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VictoriaMetrics 는 InfluxDB Data 를 받으면 이를 Prometheus 데이터 포인트로 변환을 한다.&lt;br /&gt;아래와 같은 InfluxDB Data 를 보낸다고 가정하면&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;foo,tag1=value1,tag2=value2 field1=12,field2=40&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VictoriaMetrics 에는 아래의 형태로 변환이 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;foo_field1{tag1=&quot;value1&quot;, tag2=&quot;value2&quot;} 12
foo_field2{tag1=&quot;value1&quot;, tag2=&quot;value2&quot;} 40&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP Protocol 통신을 통해 InfluxDB Data 를 전송할 수 있다.&lt;br /&gt;요청시 '\n'(개행 문자라고도 함)으로 구분된 임의의 수의 행을 보낼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;curl -d 'measurement,tag1=value1,tag2=value2 field1=123,field2=1.23' -X POST 'http://localhost:8428/write'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 전송된 이후에, /api/v1/export을 통해 확인할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;curl -G 'http://localhost:8428/api/v1/export' -d 'match={__name__=~&quot;measurement_.*&quot;}'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;metric&quot;:{&quot;__name__&quot;:&quot;measurement_field1&quot;,&quot;tag1&quot;:&quot;value1&quot;,&quot;tag2&quot;:&quot;value2&quot;},&quot;values&quot;:[123],&quot;timestamps&quot;:[1560272508147]}
{&quot;metric&quot;:{&quot;__name__&quot;:&quot;measurement_field2&quot;,&quot;tag1&quot;:&quot;value1&quot;,&quot;tag2&quot;:&quot;value2&quot;},&quot;values&quot;:[1.23],&quot;timestamps&quot;:[1560272508147]}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InfluxDB 라인 프로토콜은 기본적으로 나노초 단위의 타임스탬프를 정의하지만, VictoriaMetrics는 밀리초 단위로 저장한다.&lt;br /&gt;extra_label=name=value 쿼리 파라미터를 전달하여 작성된 모든 시계열에 레이블을 추가할 수 있다. 예를 들어 수집된 모든 측정항목에 {foo=&quot;bar&quot;} 레이블을 추가하고자 한다면, /write?extra_label=foo=bar 로 요청한다.&lt;br /&gt;&lt;a href=&quot;https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-send-data-from-influxdb-compatible-agents-such-as-telegraf&quot;&gt;Reference: How to send data from influxdb-compatible agents such as telegraf&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Graphite&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VictoriaMetrics 실행 시 graphite metric을 수집할 포트를 정의한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/path/to/victoria-metrics-prod -graphiteListenAddr=:2003&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메트릭을 graphite 형식에 맞추어 전송한다. 여러 메트릭이 필요한 경우, \n으로 개행한다. timestamp가 없는 경우, 현재 시간이 자동으로 삽입된다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;echo &quot;foo.bar.baz;tag1=value1;tag2=value2 123 `date +%s`&quot; | nc -N localhost 2003&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 전송된 이후에, /api/v1/export을 통해 확인할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;curl -G 'http://localhost:8428/api/v1/export' -d 'match=foo.bar.baz'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;metric&quot;:{&quot;__name__&quot;:&quot;foo.bar.baz&quot;,&quot;tag1&quot;:&quot;value1&quot;,&quot;tag2&quot;:&quot;value2&quot;},&quot;values&quot;:[123],&quot;timestamps&quot;:[1560277406000]}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-send-data-from-graphite-compatible-agents-such-as-statsd&quot;&gt;Reference: How to send data from Graphite-compatible agents such as StatsD&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OpenTSDB 호환 방식(OpenTSDB Agent 호환)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;openTSDB metric을 수집할 HTTP포트를 열어준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/path/to/victoria-metrics-prod -opentsdbListenAddr=:4242&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenTSDB 호환 에이전트에서 지정된 주소로 데이터를 보낸다.&lt;br /&gt;\n(개행 문자라고도 함) 으로 구분된 임의의 수의 행을 한번에 보낼 수 있다.&lt;br /&gt;아래는 nc command 를 사용하여 로컬 VictoriaMetrics에 OpenTSDB 프로토콜로 데이터를 쓰는 예.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;echo &quot;put foo.bar.baz `date +%s` 123 tag1=value1 tag2=value2&quot; | nc -N localhost 4242&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 전송된 이후에, /api/v1/export을 통해 확인할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;curl -G 'http://localhost:8428/api/v1/export' -d 'match=foo.bar.baz'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;metric&quot;:{&quot;__name__&quot;:&quot;foo.bar.baz&quot;,&quot;tag1&quot;:&quot;value1&quot;,&quot;tag2&quot;:&quot;value2&quot;},&quot;values&quot;:[123],&quot;timestamps&quot;:[1560277292000]}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#how-to-send-data-from-opentsdb-compatible-agents&quot;&gt;Reference: How to send data from OpenTSDB-compatible agents&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OpenTSDB 호환 방식(HTTP 통신)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;openTSDB metric을 수집할 HTTP포트를 열어준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;/path/to/victoria-metrics-prod -opentsdbHTTPListenAddr=:4242&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 처럼 json 형대의 메트릭을 /api/put으로 전송한다.&lt;/p&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;curl -H 'Content-Type: application/json' -d '{&quot;metric&quot;:&quot;x.y.z&quot;,&quot;value&quot;:45.34,&quot;tags&quot;:{&quot;t1&quot;:&quot;v1&quot;,&quot;t2&quot;:&quot;v2&quot;}}' http://localhost:4242/api/put&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 메트릭을 보내는 형식은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;scilab&quot;&gt;&lt;code&gt;curl -H 'Content-Type: application/json' -d '[{&quot;metric&quot;:&quot;foo&quot;,&quot;value&quot;:45.34},{&quot;metric&quot;:&quot;bar&quot;,&quot;value&quot;:43}]' http://localhost:4242/api/put&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 전송된 이후에, /api/v1/export을 통해 확인할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;curl -G 'http://localhost:8428/api/v1/export' -d 'match[]=x.y.z' -d 'match[]=foo' -d 'match[]=bar'&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;metric&quot;:{&quot;__name__&quot;:&quot;foo&quot;},&quot;values&quot;:[45.34],&quot;timestamps&quot;:[1566464846000]}
{&quot;metric&quot;:{&quot;__name__&quot;:&quot;bar&quot;},&quot;values&quot;:[43],&quot;timestamps&quot;:[1566464846000]}
{&quot;metric&quot;:{&quot;__name__&quot;:&quot;x.y.z&quot;,&quot;t1&quot;:&quot;v1&quot;,&quot;t2&quot;:&quot;v2&quot;},&quot;values&quot;:[45.34],&quot;timestamps&quot;:[1566464763000]}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.victoriametrics.com/Single-server-VictoriaMetrics.html#sending-opentsdb-data-via-http-apiput-requests&quot;&gt;Reference: Sending OpenTSDB data via HTTP /api/put requests&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;</description>
      <category>Database</category>
      <category>tsdb</category>
      <category>VictoriaMetrics</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/23</guid>
      <comments>https://yang1s.tistory.com/23#entry23comment</comments>
      <pubDate>Wed, 16 Mar 2022 17:02:48 +0900</pubDate>
    </item>
    <item>
      <title>[VictoriaMetrics] Overview</title>
      <link>https://yang1s.tistory.com/22</link>
      <description>&lt;h2&gt;개요&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Go-fasthttp 개발자가 만든 Prometheus 호환 TSDB&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;특징&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;PromQL 을 하위호환하는 MetricsQL 지원&lt;/li&gt;
&lt;li&gt;InfluxDB, Graphite, Prometheus 등 다양한 Metric Input 지원&lt;/li&gt;
&lt;li&gt;간단한 아키텍쳐&lt;/li&gt;
&lt;li&gt;Gorilla Compression 을 통한 효율적인 storage 활용&lt;/li&gt;
&lt;li&gt;Line Use Case 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;설계 철학&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;KISS(Keep It Simple, Stupid)&lt;/li&gt;
&lt;li&gt;단순한 아키텍쳐 지향 - shared nothing&lt;/li&gt;
&lt;li&gt;외부 의존성 최소화&lt;/li&gt;
&lt;li&gt;최소한의 자동화&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;지원하지 않는 것들&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;클러스터 자동 resizing&lt;/li&gt;
&lt;li&gt;자동 노드 discovering&lt;/li&gt;
&lt;li&gt;data rebalancing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pynZx/btrwafZxQRu/giIf6zeUlqVRWyplY0ZDnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pynZx/btrwafZxQRu/giIf6zeUlqVRWyplY0ZDnK/img.png&quot; data-alt=&quot;출처: VictoriaMetrics Github&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pynZx/btrwafZxQRu/giIf6zeUlqVRWyplY0ZDnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpynZx%2FbtrwafZxQRu%2FgiIf6zeUlqVRWyplY0ZDnK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1103&quot; height=&quot;746&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: VictoriaMetrics Github&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2&gt;Gorilla Compression&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Facebook 에서 Time Series Data 를 효과적으로 압축하는 방법론 소개&lt;/li&gt;
&lt;li&gt;이후 대부분의 Modern TSDB 에 적용 &amp;amp; 응용되고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Timestamp Compression&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2292&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HDJN8/btrv9JT9gER/0BDk0kBwmnBLU6EcKfBdZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HDJN8/btrv9JT9gER/0BDk0kBwmnBLU6EcKfBdZ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HDJN8/btrv9JT9gER/0BDk0kBwmnBLU6EcKfBdZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHDJN8%2Fbtrv9JT9gER%2F0BDk0kBwmnBLU6EcKfBdZ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2292&quot; height=&quot;498&quot; data-origin-width=&quot;2292&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;timestamp 는 대부분 일정한 간격으로 수집 된다.&lt;/li&gt;
&lt;li&gt;간격(delta)이 일정하다(delta of delta = 0)면 그냥 1bit flag 하나만으로 다음 timestamp 값을 표현 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Value Compression&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lYzWZ/btrvZIP9Fn9/sSYKD9URrooVu3K7swjygk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lYzWZ/btrvZIP9Fn9/sSYKD9URrooVu3K7swjygk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lYzWZ/btrvZIP9Fn9/sSYKD9URrooVu3K7swjygk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlYzWZ%2FbtrvZIP9Fn9%2FsSYKD9URrooVu3K7swjygk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1770&quot; height=&quot;320&quot; data-origin-width=&quot;1770&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이진수 표현 시 이전 값과 현재 값의 대부분의 bit 는 동일 -&amp;gt; XOR 연산 시 &amp;#39;0&amp;#39;&lt;/li&gt;
&lt;li&gt;XOR 연산으로 &amp;#39;0&amp;#39; 으로 바뀐 bit 들은 압축시켜서 표현&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Compression Summary&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2080&quot; data-origin-height=&quot;554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Kx5GZ/btrwaEx4KMb/ZHwiZRsgzElRlSnDc83gG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Kx5GZ/btrwaEx4KMb/ZHwiZRsgzElRlSnDc83gG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Kx5GZ/btrwaEx4KMb/ZHwiZRsgzElRlSnDc83gG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKx5GZ%2FbtrwaEx4KMb%2FZHwiZRsgzElRlSnDc83gG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2080&quot; height=&quot;554&quot; data-origin-width=&quot;2080&quot; data-origin-height=&quot;554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;1064&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxeKez/btrv4neebTo/x3C7gXAPbtt903h7Rn9grk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxeKez/btrv4neebTo/x3C7gXAPbtt903h7Rn9grk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxeKez/btrv4neebTo/x3C7gXAPbtt903h7Rn9grk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxeKez%2Fbtrv4neebTo%2Fx3C7gXAPbtt903h7Rn9grk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;856&quot; height=&quot;1064&quot; data-origin-width=&quot;856&quot; data-origin-height=&quot;1064&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Timestamp: 96.39%. 1bit 만으로 시간 표현 가능.&lt;/li&gt;
&lt;li&gt;Value: 59.06%. 1bit 만으로 값 표현 가능.&lt;/li&gt;
&lt;li&gt;압축률: 16 bytes를 1.37 bytes 로 줄임 (약 12배 향상)&lt;/li&gt;
&lt;li&gt;Query 속도: 경우에 따라 최대 73 ~ 350배 향상&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database</category>
      <category>tsdb</category>
      <category>VictoriaMetrics</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/22</guid>
      <comments>https://yang1s.tistory.com/22#entry22comment</comments>
      <pubDate>Wed, 16 Mar 2022 15:14:33 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] binary 설치 및 실행</title>
      <link>https://yang1s.tistory.com/21</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-1-zookeeper-구축&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C#1-zookeeper-%EA%B5%AC%EC%B6%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;1. Zookeeper 구축&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-1-zookeeper-설치&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C#1-zookeeper-%EC%84%A4%EC%B9%98&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;1) Zookeeper 설치&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://archive.apache.org/dist/zookeeper/&quot;&gt;https://archive.apache.org/dist/zookeeper/&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;경로에서 원하는 버전의 zookeeper 를 다운받는다.&lt;/li&gt;
&lt;li&gt;다운받은 zookeeper 압축파일을 원하는 경로에 해제한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-2-zookeeper-설정&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C#2-zookeeper-%EC%84%A4%EC%A0%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2) Zookeeper 설정&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://zookeeper.apache.org/doc/r3.3.2/zookeeperAdmin.html#sc_configuration&quot;&gt;zookeeper configuration&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 참고하여 zookeeper 설정 파일을 정의한다.&lt;/li&gt;
&lt;li&gt;cluster 로 구축해야하므로 설정파일에 zookeeper 로 구축할 각 서버정보를 아래와 같이 정의한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;server.{zookeeper-1.serverId}={zookeeper-host1}:{follower-port}:{leader-election-port}
server.{zookeeper-2.serverId}={zookeeper-host2}:{follower-port}:{leader-election-port}
server.{zookeeper-3.serverId}={zookeeper-host3}:{follower-port}:{leader-election-port}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** follower-port 와 leader-election-port 는 kafka 에서 쓰는게 아닌 zookeeper 내부 통신용이므로 보통 kafka 가 접근하는 port 설정인&lt;span&gt;&amp;nbsp;&lt;/span&gt;clientPort&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정과 다르게 해주어야 합니다.&lt;br /&gt;** serverId 는 각 zookeeper 서버에 정의해주어야 하는 ID 값이므로 숫자로 중복되지 않게 정의해줍니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 zookeeper 서버에 위 설정파일에서 정의한&lt;span&gt;&amp;nbsp;&lt;/span&gt;dataDir&lt;span&gt;&amp;nbsp;&lt;/span&gt;경로 하위에 myid 라는 파일명으로 serverId 값을 기록합니다.(예시에서는 $HOME/data/zookeeper 경로를 dataDir 로 정의)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** example(serverId 를 1로 설정한 zookeeper 노드)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ pwd
$HOME/data/zookeeper
$ ll
-rw-rw-r-- 1 user user    1 Sep 25  2019 myid
$ cat myid 
1&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;zookeeper 설정 파일이 있는 경로에&lt;span&gt; 필요한 JVM 환경변수를 정의한 파일이나&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;log4j.properties&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일을 같이 정의하고 zookeeper 설정 파일이 있는 경로를&lt;span&gt;&amp;nbsp;&lt;/span&gt;ZOOCFGDIR&lt;span&gt;&amp;nbsp;&lt;/span&gt;명으로 환경변수를 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** ZOOCFGDIR 환경변수 설정은 설치한 zookeeper 의 conf 디렉토리 하위에 설정파일을 두었다면 하지 않아도 된다.(경로를 변경해야할때 정의한다.)&lt;br /&gt;** JVM 환경변수로 설정해야할 내용은 설치한 zookeeper의&lt;span&gt;&amp;nbsp;&lt;/span&gt;bin&lt;span&gt;&amp;nbsp;&lt;/span&gt;디렉토리 하위에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;zkEnv.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;내용을 참고해서 필요한 사항들을 정의한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-3-zookeeper-실행&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C#3-zookeeper-%EC%8B%A4%ED%96%89&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;3) Zookeeper 실행&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설치한 zookeeper 의&lt;span&gt;&amp;nbsp;&lt;/span&gt;bin&lt;span&gt;&amp;nbsp;&lt;/span&gt;디렉토리 하위에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;zkServer.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 아래와 같이 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[irteamsu@dev-ncs-wmgq-de1-005-ncl bin]$ ./zkServer.sh start {zoo-conf filename}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** {zoo-conf filename} 은 설정파일을 zoo.cfg 로 했을경우 정의하지 않아도 된다. 파일명이 zoo.cfg 가 아닐경우에만 정의한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-4-zookeeper-종료&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C#4-zookeeper-%EC%A2%85%EB%A3%8C&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;4) Zookeeper 종료&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설치한 zookeeper 의&lt;span&gt;&amp;nbsp;&lt;/span&gt;bin&lt;span&gt;&amp;nbsp;&lt;/span&gt;디렉토리 하위에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;zkServer.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 아래와 같이 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;vim&quot;&gt;&lt;code&gt;[irteamsu@dev-ncs-wmgq-de1-005-ncl bin]$ ./zkServer.sh stop {zoo-conf filename}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;** {zoo-conf filename} 은 설정파일을 zoo.cfg 로 했을경우 정의하지 않아도 된다. 파일명이 zoo.cfg 가 아닐경우에만 정의한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-2-apache-kafka-구축&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C#2-apache-kafka-%EA%B5%AC%EC%B6%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2. Apache Kafka 구축&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-1-kafka-설치&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C#1-kafka-%EC%84%A4%EC%B9%98&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;1) Kafka 설치&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://archive.apache.org/dist/kafka/&quot;&gt;https://archive.apache.org/dist/kafka/&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서 원하는 버전의 kafka 를 다운받는다.&lt;/li&gt;
&lt;li&gt;다운받은 kafka 압축파일을 원하는 경로에 해제한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-2-kafka-설정&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C#2-kafka-%EC%84%A4%EC%A0%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;2) Kafka 설정&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kafka.apache.org/documentation/#brokerconfigs&quot;&gt;kafka broker config&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;내용을 참고하여 properites 파일을 작성.&lt;/li&gt;
&lt;li&gt;설정값중&lt;span&gt;&amp;nbsp;&lt;/span&gt;zookeeper.connect&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 위에서 구축한 주키퍼 서버를 '호스트:포트' 형태로&lt;span&gt;&amp;nbsp;&lt;/span&gt;,&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 구분하여 정의한다.&lt;/li&gt;
&lt;li&gt;설정파일 정의시 각 broker 노드들을 구분할 수 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;broker.id&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 숫자형태로 각 서버마다 고유하게 정의한다.&lt;/li&gt;
&lt;li&gt;kafka 실행시 아래의 JVM 환경 변수들을 셋팅하여 적용할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;KAFKA_JMX_OPTS : jmx 관련 설정 정의(단, jmx port 를 해당 환경변수로 설정하고자 한다면, JMX_PORT 환경변수는 설정하지 않는다.)&lt;/li&gt;
&lt;li&gt;JMX_PORT : jmx port 정의(단, KAFKA_JMX_OPTS 에서 -Dcom.sun.management.jmxremote.port 를 정의했을 경우 설정하지 말아야 한다.)&lt;/li&gt;
&lt;li&gt;KAFKA_LOG4J_OPTS : kafka log4j 설정 관련 정의&lt;/li&gt;
&lt;li&gt;LOG_DIR : kafka log 경로 정의&lt;/li&gt;
&lt;li&gt;JAVA_HOME : java home path 정의&lt;/li&gt;
&lt;li&gt;KAFKA_HEAP_OPTS : kafka heap 설정 정의&lt;/li&gt;
&lt;li&gt;KAFKA_JVM_PERFORMANCE_OPTS : kafka jvm 옵션 관련 설정 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-3-kafka-실행&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C#3-kafka-%EC%8B%A4%ED%96%89&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;3) Kafka 실행&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설치한 kafka 의 bin 하위에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;kafka-server-start.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 실행한다.&lt;/li&gt;
&lt;li&gt;백그라운드 프로세스로 실행하기 위해서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;-deamon&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정을 추가한다.&lt;/li&gt;
&lt;li&gt;설정파일 경로를 파라미터로 추가한다.&lt;/li&gt;
&lt;li&gt;만약 설정파일에 정의한 내용을 override 할 설정이 있다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;--override property=value&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태의 옵션을 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ ./kafka-server-start.sh -deamon $CONFIG_DIR/kafka/kafka.properties&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-4-kafka-종료&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%EC%B9%B4%ED%94%84%EC%B9%B4-%EA%B5%AC%EC%B6%95-%EA%B0%80%EC%9D%B4%EB%93%9C#4-kafka-%EC%A2%85%EB%A3%8C&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;4) Kafka 종료&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;설치한 kafka 의 bin 하위에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;kafka-server-stop.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 실행한다.&lt;/li&gt;
&lt;li&gt;종료시 특별히 필요한 파라미터는 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ ./kafka-server-stop.sh&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>Kafka</category>
      <category>broker</category>
      <category>kafka</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/21</guid>
      <comments>https://yang1s.tistory.com/21#entry21comment</comments>
      <pubDate>Tue, 15 Mar 2022 15:12:53 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Consumer Group Offset Reset</title>
      <link>https://yang1s.tistory.com/20</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;필요한 상황&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Consumer 에 이슈가 발생해서 특정 시점으로 offset 을 돌려서 다시 읽고 싶어요.&lt;/li&gt;
&lt;li&gt;지금까지 읽었던 메세지를 다시 읽게 하고 싶을 경우?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-확인-사항-및-대응-방법&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%28%EC%9A%B4%EC%98%81-%EB%A9%94%EB%89%B4%EC%96%BC-Kafka%29-Consumer-Group-Offset-Reset#%ED%99%95%EC%9D%B8-%EC%82%AC%ED%95%AD-%EB%B0%8F-%EB%8C%80%EC%9D%91-%EB%B0%A9%EB%B2%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;사전 확인 사항&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-1-offset-을-어떤-방식으로-reset-하고-싶은지-확인한다&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%28%EC%9A%B4%EC%98%81-%EB%A9%94%EB%89%B4%EC%96%BC-Kafka%29-Consumer-Group-Offset-Reset#1-offset-%EC%9D%84-%EC%96%B4%EB%96%A4-%EB%B0%A9%EC%8B%9D%EC%9C%BC%EB%A1%9C-reset-%ED%95%98%EA%B3%A0-%EC%8B%B6%EC%9D%80%EC%A7%80-%ED%99%95%EC%9D%B8%ED%95%9C%EB%8B%A4&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;- offset 을 어디까지로 reset 할지 확인&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특정 offset 으로 reset 을 하고 싶다면 Consumer 에서 남긴 로그를 통해 각 파티션별 reset 하고자 하는 offset 값을 알아야 한다.&lt;/li&gt;
&lt;li&gt;특정 시점으로 reset 을 하고 싶다면 reset 하고자 하는 시점을 'yyyy-MM-ddTHH:mm:SS.sss' 포맷으로 정의한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-2-서비스에서-메세지-처리가-중복으로-발생해도-이슈가-없는지-확인한다&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%28%EC%9A%B4%EC%98%81-%EB%A9%94%EB%89%B4%EC%96%BC-Kafka%29-Consumer-Group-Offset-Reset#2-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90%EC%84%9C-%EB%A9%94%EC%84%B8%EC%A7%80-%EC%B2%98%EB%A6%AC%EA%B0%80-%EC%A4%91%EB%B3%B5%EC%9C%BC%EB%A1%9C-%EB%B0%9C%EC%83%9D%ED%95%B4%EB%8F%84-%EC%9D%B4%EC%8A%88%EA%B0%80-%EC%97%86%EB%8A%94%EC%A7%80-%ED%99%95%EC%9D%B8%ED%95%9C%EB%8B%A4&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;- 경우에 따라 메세지 처리가 중복으로 발생할 수 있는데 이때 이슈가 없는지 확인한다.&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이슈가 없다면 상관없지만 이슈가 있을 경우 Consumer 로직에서 중복처리를 하지 않도록 수정작업이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-kafka-command-가이드&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%28%EC%9A%B4%EC%98%81-%EB%A9%94%EB%89%B4%EC%96%BC-Kafka%29-Consumer-Group-Offset-Reset#kafka-command-%EA%B0%80%EC%9D%B4%EB%93%9C&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Kafka Command 수행 방법&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-신규-consumer-group-생성-방법&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%28%EC%9A%B4%EC%98%81-%EB%A9%94%EB%89%B4%EC%96%BC-Kafka%29-Consumer-Group-Offset-Reset#%EC%8B%A0%EA%B7%9C-consumer-group-%EC%83%9D%EC%84%B1-%EB%B0%A9%EB%B2%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;- 신규 Consumer Group 생성 방법&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;offset 을 reset 하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;/home1/irteam/apps/kafka/bin/kafka-consumer-groups.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 --execute 요청을 하면 consumer group 이 없을경우 신규로 만들어진다.(즉, consumer group 생성을 위해 특별히 어떤 command 요청을 할 필요는 없고 offset reset command 만 실행하면 된다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-consumer-group-삭제-방법&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%28%EC%9A%B4%EC%98%81-%EB%A9%94%EB%89%B4%EC%96%BC-Kafka%29-Consumer-Group-Offset-Reset#consumer-group-%EC%82%AD%EC%A0%9C-%EB%B0%A9%EB%B2%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;- Consumer Group 삭제 방법&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ consumer group 을 삭제하기 위해서는 해당 consumer group 상태가 inactive 여야 가능하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;consumer group 을 삭제하고자 하는 kafka broker 장비에 접속한다.&lt;/li&gt;
&lt;li&gt;$KAFKA_HOME/bin/kafka-consumer-groups.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 아래와 같이 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ bin/kafka-consumer-groups.sh --bootstrap-server $KAFKA --group $CONSUMER_GROUP --topic $TOPIC --delete --execute&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ Kafka Broker 에 보안설정이 되어있을 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;--command-config 옵션을 통해 보안 관련 설정을 반드시 추가해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-특정-시점의-offset-으로-reset-할-경우&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%28%EC%9A%B4%EC%98%81-%EB%A9%94%EB%89%B4%EC%96%BC-Kafka%29-Consumer-Group-Offset-Reset#%ED%8A%B9%EC%A0%95-%EC%8B%9C%EC%A0%90%EC%9D%98-offset-%EC%9C%BC%EB%A1%9C-reset-%ED%95%A0-%EA%B2%BD%EC%9A%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;특정 시점의 offset 으로 reset 할 경우&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ consumer group 의 offset 을 reset 하기 위해서는 해당 consumer group 상태가 inactive 여야 가능하다. 신규로 정의되는 consumer group 일 경우 이부분은 신경쓰지 않아도 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;offset 을 변경하고자 하는 kafka broker 장비에 접속한다.&lt;/li&gt;
&lt;li&gt;$KAFKA_HOME/bin/kafka-consumer-groups.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 아래와 같이 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ bin/kafka-consumer-groups.sh --bootstrap-server $KAFKA --group $CONSUMER_GROUP --topic $TOPIC --reset-offsets --to-datetime '2020-01-01 01:00:00.001' --execute&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ Kafka Broker 에 보안설정이 되어있을 경우 --command-config 옵션을 통해 보안 관련 설정을 반드시 추가해야 한다.&amp;nbsp;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-특정-offset-값으로-reset-할-경우&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%28%EC%9A%B4%EC%98%81-%EB%A9%94%EB%89%B4%EC%96%BC-Kafka%29-Consumer-Group-Offset-Reset#%ED%8A%B9%EC%A0%95-offset-%EA%B0%92%EC%9C%BC%EB%A1%9C-reset-%ED%95%A0-%EA%B2%BD%EC%9A%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;특정 offset 값으로 reset 할 경우&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ consumer group 의 offset 을 reset 하기 위해서는 해당 consumer group 상태가 inactive 여야 가능하다. 신규로 정의되는 consumer group 일 경우 이부분은 신경쓰지 않아도 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;offset 을 변경하고자 하는 kafka broker 장비에 접속한다.&lt;/li&gt;
&lt;li&gt;아래와 같이 csv 포멧으로 offset 설정 파일을 만든다.(토픽, 파티션Id, offset 값 순으로 나열한다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;example,0,16569
example,1,15715
example,2,13281
example,3,15252
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;$KAFKA_HOME/bin/kafka-consumer-groups.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 아래와 같이 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ bin/kafka-consumer-groups.sh --bootstrap-server $KAFKA --group $CONSUMER_GROUP --reset-offsets --from-file $CONSUMER_GROUP_offsets.csv --execute&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ &lt;span style=&quot;color: #666666;&quot;&gt;Kafka Broker 에 보안설정이 되어있을 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;--command-config 옵션을 통해 보안 관련&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;설정을 반드시 추가해야 한다&lt;/span&gt;.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Kafka</category>
      <category>broker</category>
      <category>kafka</category>
      <category>OFFSET</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/20</guid>
      <comments>https://yang1s.tistory.com/20#entry20comment</comments>
      <pubDate>Tue, 15 Mar 2022 14:56:37 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] Reassign Partition 방법</title>
      <link>https://yang1s.tistory.com/19</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;필요한 상황&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신규 broker 장비 추가&lt;/li&gt;
&lt;li&gt;broker 장비 이전&lt;/li&gt;
&lt;li&gt;특정 토픽을 특정 노드에서만 운영하고 싶을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-운영-방법&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%28%EC%9A%B4%EC%98%81-%EB%A9%94%EB%89%B4%EC%96%BC-Kafka%29-Kafka-Reassign-Partition#%EC%9A%B4%EC%98%81-%EB%B0%A9%EB%B2%95&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;실행 방법&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kafka bin 디렉토리 하위에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;kafka-reassign-partitions.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;스크립트를 사용&lt;/li&gt;
&lt;li&gt;이동시킬 topic 이 무엇인지 json 포맷으로 정의 예) example-to-move.json&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;{&quot;topics&quot;:[{&quot;topic&quot;: &quot;example1&quot;},{&quot;topic&quot;: &quot;example2&quot;},{&quot;topic&quot;: &quot;example3&quot;}],&quot;version&quot;:1}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;--topics-to-move-json-file 옵션으로 위에서 생성한 json 파일을 지정하고, --generate 옵션을 통해 이동시킬 broker 에 대한 파티션 설정값을 자동으로 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ bin/kafka-reassign-partitions.sh --zookeeper &quot;zookeeper-host:port,zookeeper-host:port,...&quot; --broker-list &quot;1,2,3&quot; --topics-to-move-json-file example-to-move.json --generate&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 실행을 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;Current partition replica assignment&lt;span&gt;&amp;nbsp;&lt;/span&gt;과&lt;span&gt;&amp;nbsp;&lt;/span&gt;Proposed partition reassignment configuration&lt;span&gt;&amp;nbsp;&lt;/span&gt;이라는 제목으로 콘솔창에 json 내용이 출력되게 되는데&lt;span&gt;&amp;nbsp;&lt;/span&gt;Proposed partition reassignment configuration&lt;span&gt;&amp;nbsp;&lt;/span&gt;내용을 복사하여 파일로 만들고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Current partition replica assignment&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 혹시 롤백을 해야할 경우를 대비해 따로 복사해둔다.(해당 예시에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;example-partitions-to-move.json&lt;span&gt;&amp;nbsp;&lt;/span&gt;이름으로 생성)&lt;/li&gt;
&lt;li&gt;--reassignment-json-file 옵션으로 위에서 생성한 json 파일을 지정하고, --execute 옵션을 통해 파티션을 이동시키도록 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ bin/kafka-reassign-partitions.sh --zookeeper &quot;zookeeper-host:port,zookeeper-host:port,...&quot; --reassignment-json-file example-partitions-to-move.json --execute&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;--verify 옵션을 통해 현재 이동 상태를 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ bin/kafka-reassign-partitions.sh --zookeeper &quot;zookeeper-host:port,zookeeper-host:port,...&quot; --reassignment-json-file example-partitions-to-move.json --verify

Status of partition reassignment: 
Reassignment of partition example1-52 completed successfully
Reassignment of partition example1-15 completed successfully
Reassignment of partition example2-4 completed successfully
Reassignment of partition example2-12 completed successfully
Reassignment of partition example3-11 completed successfully
Reassignment of partition example1-29 completed successfully
Reassignment of partition example2-40 completed successfully
Reassignment of partition example3-45 completed successfully
Reassignment of partition example3-37 completed successfully
...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-참고&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/%28%EC%9A%B4%EC%98%81-%EB%A9%94%EB%89%B4%EC%96%BC-Kafka%29-Kafka-Reassign-Partition#%EC%B0%B8%EA%B3%A0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;참고&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kafka.apache.org/21/documentation.html#basic_ops_automigrate&quot;&gt;https://kafka.apache.org/21/documentation.html#basic_ops_automigrate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cwiki.apache.org/confluence/display/KAFKA/Replication+tools#Replicationtools-4.ReassignPartitionsTool&quot;&gt;https://cwiki.apache.org/confluence/display/KAFKA/Replication+tools#Replicationtools-4.ReassignPartitionsTool&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Kafka</category>
      <category>broker</category>
      <category>kafka</category>
      <category>Reassign</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/19</guid>
      <comments>https://yang1s.tistory.com/19#entry19comment</comments>
      <pubDate>Tue, 15 Mar 2022 14:41:53 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] OutOfOrderSequenceException</title>
      <link>https://yang1s.tistory.com/18</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;내용 정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka Broker 로그에 간혹가다 아래와 같은 ERROR 로그가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;ERROR [ReplicaManager broker=23] Error processing append operation on partition test-8 (kafka.server.ReplicaManager)
org.apache.kafka.common.errors.OutOfOrderSequenceException: Invalid sequence number for new epoch: 3 (request epoch), 4 (seq. number)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt; ERROR [ReplicaManager broker=22] Error processing append operation on partition test-55 (kafka.server.ReplicaManager)
org.apache.kafka.common.errors.UnknownProducerIdException: Found no record of producerId=557891 on the broker. It is possible that the last message with the producerId=557891 has been removed due to hitting the retention limit.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분석&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;message publish 요청시 호출되는 함수 순서
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kafka.server.KafkaRequestHandler.run&lt;/li&gt;
&lt;li&gt;kafka.server.KafkaApis.handle&lt;/li&gt;
&lt;li&gt;kafka.server.KafkaApis.handleProduceRequest&lt;/li&gt;
&lt;li&gt;kafka.server.ReplicaManager.appendRecords&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;위 에러는&lt;span&gt;&amp;nbsp;&lt;/span&gt;kafka.server.ReplicaManager.appendRecords&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서 ReplicaManager 의 private 함수인&lt;span&gt;&amp;nbsp;&lt;/span&gt;appendToLocalLog&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 수행하면서 발생&lt;/li&gt;
&lt;li&gt;appendRecords 함수 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;  /**
   * Append messages to leader replicas of the partition, and wait for them to be replicated to other replicas;
   * the callback function will be triggered either when timeout or the required acks are satisfied;
   * if the callback function itself is already synchronized on some object then pass this object to avoid deadlock.
   */
  def appendRecords(timeout: Long,
                    requiredAcks: Short,
                    internalTopicsAllowed: Boolean,
                    isFromClient: Boolean,
                    entriesPerPartition: Map[TopicPartition, MemoryRecords],
                    responseCallback: Map[TopicPartition, PartitionResponse] =&amp;gt; Unit,
                    delayedProduceLock: Option[Lock] = None,
                    recordConversionStatsCallback: Map[TopicPartition, RecordConversionStats] =&amp;gt; Unit = _ =&amp;gt; ()) {
    if (isValidRequiredAcks(requiredAcks)) {
      val sTime = time.milliseconds
      val localProduceResults = appendToLocalLog(internalTopicsAllowed = internalTopicsAllowed,
        isFromClient = isFromClient, entriesPerPartition, requiredAcks)
      debug(&quot;Produce to local log in %d ms&quot;.format(time.milliseconds - sTime))

      val produceStatus = localProduceResults.map { case (topicPartition, result) =&amp;gt;
        topicPartition -&amp;gt;
                ProducePartitionStatus(
                  result.info.lastOffset + 1, // required offset
                  new PartitionResponse(result.error, result.info.firstOffset.getOrElse(-1), result.info.logAppendTime, result.info.logStartOffset)) // response status
      }

      recordConversionStatsCallback(localProduceResults.mapValues(_.info.recordConversionStats))

      if (delayedProduceRequestRequired(requiredAcks, entriesPerPartition, localProduceResults)) {
        // create delayed produce operation
        val produceMetadata = ProduceMetadata(requiredAcks, produceStatus)
        val delayedProduce = new DelayedProduce(timeout, produceMetadata, this, responseCallback, delayedProduceLock)

        // create a list of (topic, partition) pairs to use as keys for this delayed produce operation
        val producerRequestKeys = entriesPerPartition.keys.map(new TopicPartitionOperationKey(_)).toSeq

        // try to complete the request immediately, otherwise put it into the purgatory
        // this is because while the delayed produce operation is being created, new
        // requests may arrive and hence make this operation completable.
        delayedProducePurgatory.tryCompleteElseWatch(delayedProduce, producerRequestKeys)

      } else {
        // we can respond immediately
        val produceResponseStatus = produceStatus.mapValues(status =&amp;gt; status.responseStatus)
        responseCallback(produceResponseStatus)
      }
    } else {
      // If required.acks is outside accepted range, something is wrong with the client
      // Just return an error and don't handle the request at all
      val responseStatus = entriesPerPartition.map { case (topicPartition, _) =&amp;gt;
        topicPartition -&amp;gt; new PartitionResponse(Errors.INVALID_REQUIRED_ACKS,
          LogAppendInfo.UnknownLogAppendInfo.firstOffset.getOrElse(-1), RecordBatch.NO_TIMESTAMP, LogAppendInfo.UnknownLogAppendInfo.logStartOffset)
      }
      responseCallback(responseStatus)
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;appendToLocalLog 내용&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;  /**
   * Append the messages to the local replica logs
   */
  private def appendToLocalLog(internalTopicsAllowed: Boolean,
                               isFromClient: Boolean,
                               entriesPerPartition: Map[TopicPartition, MemoryRecords],
                               requiredAcks: Short): Map[TopicPartition, LogAppendResult] = {
    trace(s&quot;Append [$entriesPerPartition] to local log&quot;)
    entriesPerPartition.map { case (topicPartition, records) =&amp;gt;
      brokerTopicStats.topicStats(topicPartition.topic).totalProduceRequestRate.mark()
      brokerTopicStats.allTopicsStats.totalProduceRequestRate.mark()

      // reject appending to internal topics if it is not allowed
      if (Topic.isInternal(topicPartition.topic) &amp;amp;&amp;amp; !internalTopicsAllowed) {
        (topicPartition, LogAppendResult(
          LogAppendInfo.UnknownLogAppendInfo,
          Some(new InvalidTopicException(s&quot;Cannot append to internal topic ${topicPartition.topic}&quot;))))
      } else {
        try {
          val partition = getPartitionOrException(topicPartition, expectLeader = true)
          val info = partition.appendRecordsToLeader(records, isFromClient, requiredAcks)
          val numAppendedMessages = info.numMessages

          // update stats for successfully appended bytes and messages as bytesInRate and messageInRate
          brokerTopicStats.topicStats(topicPartition.topic).bytesInRate.mark(records.sizeInBytes)
          brokerTopicStats.allTopicsStats.bytesInRate.mark(records.sizeInBytes)
          brokerTopicStats.topicStats(topicPartition.topic).messagesInRate.mark(numAppendedMessages)
          brokerTopicStats.allTopicsStats.messagesInRate.mark(numAppendedMessages)

          trace(s&quot;${records.sizeInBytes} written to log $topicPartition beginning at offset &quot; +
            s&quot;${info.firstOffset.getOrElse(-1)} and ending at offset ${info.lastOffset}&quot;)
          (topicPartition, LogAppendResult(info))
        } catch {
          // NOTE: Failed produce requests metric is not incremented for known exceptions
          // it is supposed to indicate un-expected failures of a broker in handling a produce request
          case e@ (_: UnknownTopicOrPartitionException |
                   _: NotLeaderForPartitionException |
                   _: RecordTooLargeException |
                   _: RecordBatchTooLargeException |
                   _: CorruptRecordException |
                   _: KafkaStorageException |
                   _: InvalidTimestampException) =&amp;gt;
            (topicPartition, LogAppendResult(LogAppendInfo.UnknownLogAppendInfo, Some(e)))
          case t: Throwable =&amp;gt;
            val logStartOffset = getPartition(topicPartition) match {
              case Some(partition) =&amp;gt;
                partition.logStartOffset
              case _ =&amp;gt;
                -1
            }
            brokerTopicStats.topicStats(topicPartition.topic).failedProduceRequestRate.mark()
            brokerTopicStats.allTopicsStats.failedProduceRequestRate.mark()
            error(s&quot;Error processing append operation on partition $topicPartition&quot;, t)
            (topicPartition, LogAppendResult(LogAppendInfo.unknownLogAppendInfoWithLogStartOffset(logStartOffset), Some(t)))
        }
      }
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;appendToLocalLog&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서 record append 가 실패하면 실패한 결과를 넘겨주고&lt;span&gt;&amp;nbsp;&lt;/span&gt;appendRecords&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;localProduceResults&lt;span&gt;&amp;nbsp;&lt;/span&gt;라는 변수명으로 결과값을 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;val localProduceResults = appendToLocalLog(internalTopicsAllowed = internalTopicsAllowed,
        origin, entriesPerPartition, requiredAcks)
      debug(&quot;Produce to local log in %d ms&quot;.format(time.milliseconds - sTime))&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;localProduceResults&lt;span&gt;&amp;nbsp;&lt;/span&gt;결과중 실패가 있을 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;delayedProducePurgatory&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 등록하여 재시도 처리한다.&lt;/li&gt;
&lt;li&gt;delayedProducePurgatory&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 등록하는 조건&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt; // If all the following conditions are true, we need to put a delayed produce request and wait for replication to complete
  //
  // 1. required acks = -1
  // 2. there is data to append
  // 3. at least one partition append was successful (fewer errors than partitions)
  private def delayedProduceRequestRequired(requiredAcks: Short,
                                            entriesPerPartition: Map[TopicPartition, MemoryRecords],
                                            localProduceResults: Map[TopicPartition, LogAppendResult]): Boolean = {
    requiredAcks == -1 &amp;amp;&amp;amp;
    entriesPerPartition.nonEmpty &amp;amp;&amp;amp;
    localProduceResults.values.count(_.exception.isDefined) &amp;lt; entriesPerPartition.size
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OutOfOrderSequenceException 로그에서 나온 epoch 는 partition leader epoch 가 아닌 producer 관련 epoch 값이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;org.apache.kafka.common.record.RecordBatch&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 정의된&lt;span&gt;&amp;nbsp;&lt;/span&gt;producerEpoch&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을 사용한다.(partition leader 에 대한 epoch 값은&lt;span&gt;&amp;nbsp;&lt;/span&gt;partitionLeaderEpoch&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 따로 정의되어있다.)&lt;/li&gt;
&lt;li&gt;메세지 publish 시에 producer 에 대한 상태 체크를 진행하게된다.&lt;/li&gt;
&lt;li&gt;kafka.log.producerStateManager&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서 요청시 들어온 producerId 를 가지고 broker 내부에 저장된 producer 상태 정보를 가지고 온다.&lt;/li&gt;
&lt;li&gt;조회한 상태 정보를 바탕으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;kafka.log.ProducerAppendInfo&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 생성해&lt;span&gt;&amp;nbsp;&lt;/span&gt;maybeValidateAppend&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수에서 producerId, producerEpoch, appendFirstSeq 값을 확인하게 되는데 그 때 에러가 발생한다.&lt;/li&gt;
&lt;li&gt;maybeValidateAppend 함수&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private def maybeValidateAppend(producerEpoch: Short, firstSeq: Int) = {
    validationType match {
      case ValidationType.None =&amp;gt;

      case ValidationType.EpochOnly =&amp;gt;
        checkProducerEpoch(producerEpoch)

      case ValidationType.Full =&amp;gt;
        checkProducerEpoch(producerEpoch)
        checkSequence(producerEpoch, firstSeq)
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 에러를 발생하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;checkSequence&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;private def checkSequence(producerEpoch: Short, appendFirstSeq: Int): Unit = {
    if (producerEpoch != updatedEntry.producerEpoch) {
      if (appendFirstSeq != 0) {
        if (updatedEntry.producerEpoch != RecordBatch.NO_PRODUCER_EPOCH) {
          throw new OutOfOrderSequenceException(s&quot;Invalid sequence number for new epoch: $producerEpoch &quot; +
            s&quot;(request epoch), $appendFirstSeq (seq. number)&quot;)
        } else {
          throw new UnknownProducerIdException(s&quot;Found no record of producerId=$producerId on the broker. It is possible &quot; +
            s&quot;that the last message with the producerId=$producerId has been removed due to hitting the retention limit.&quot;)
        }
      }
    } else {
      val currentLastSeq = if (!updatedEntry.isEmpty)
        updatedEntry.lastSeq
      else if (producerEpoch == currentEntry.producerEpoch)
        currentEntry.lastSeq
      else
        RecordBatch.NO_SEQUENCE

      if (currentLastSeq == RecordBatch.NO_SEQUENCE &amp;amp;&amp;amp; appendFirstSeq != 0) {
        // We have a matching epoch, but we do not know the next sequence number. This case can happen if
        // only a transaction marker is left in the log for this producer. We treat this as an unknown
        // producer id error, so that the producer can check the log start offset for truncation and reset
        // the sequence number. Note that this check follows the fencing check, so the marker still fences
        // old producers even if it cannot determine our next expected sequence number.
        throw new UnknownProducerIdException(s&quot;Local producer state matches expected epoch $producerEpoch &quot; +
          s&quot;for producerId=$producerId, but next expected sequence number is not known.&quot;)
      } else if (!inSequence(currentLastSeq, appendFirstSeq)) {
        throw new OutOfOrderSequenceException(s&quot;Out of order sequence number for producerId $producerId: $appendFirstSeq &quot; +
          s&quot;(incoming seq. number), $currentLastSeq (current end sequence number)&quot;)
      }
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 분석 내용 및 코드에서 나오는&lt;span&gt;&amp;nbsp;&lt;/span&gt;producerEpoch&lt;span&gt;&amp;nbsp;&lt;/span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;firstSequence&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 대한 설명은 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변수설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ProducerEpoch&lt;/td&gt;
&lt;td&gt;Introduced in 0.11.0.0 for&amp;nbsp;&lt;a href=&quot;https://cwiki.apache.org/confluence/display/KAFKA/KIP-98+-+Exactly+Once+Delivery+and+Transactional+Messaging#KIP98ExactlyOnceDeliveryandTransactionalMessaging-TheOutOfOrderSequenceException&quot;&gt;KIP-98&lt;/a&gt;, this is the broker assigned producerEpoch received by the 'InitProducerId' request. Clients which want to support idempotent message delivery and transactions must set this field.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FirstSequence&lt;/td&gt;
&lt;td&gt;Introduced in 0.11.0.0 for&amp;nbsp;&lt;a href=&quot;https://cwiki.apache.org/confluence/display/KAFKA/KIP-98+-+Exactly+Once+Delivery+and+Transactional+Messaging#KIP98ExactlyOnceDeliveryandTransactionalMessaging-TheOutOfOrderSequenceException&quot;&gt;KIP-98&lt;/a&gt;, this is the producer assigned sequence number which is used by the broker to deduplicate messages. Clients which want to support idempotent message delivery and transactions must set this field. The sequence number for each Record in the RecordBatch is its OffsetDelta + FirstSequence.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;idempotent write 설정을 해서 위 변수값들이 이용되어있는거로 보인다.&lt;/li&gt;
&lt;li&gt;좀더 자세한 사항은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol&quot;&gt;A Guide To The Kafka Protocol&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고&lt;/li&gt;
&lt;li&gt;kafka broker 2.5.0 에 위 에러에 대한 개선 내용이 있는거로 보여짐.&lt;a href=&quot;https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=89068820&quot;&gt;KIP-360&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Kafka</category>
      <category>broker</category>
      <category>kafka</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/18</guid>
      <comments>https://yang1s.tistory.com/18#entry18comment</comments>
      <pubDate>Mon, 14 Mar 2022 16:11:43 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] kafka SASL / SCRAM 인증, kafka-acls 권한 설정</title>
      <link>https://yang1s.tistory.com/17</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;개요 및 고려 사항&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SCRAM (Salted Challenge Response Authentication Mechanism) 또는 SASL / SCRAM은 PLAIN과 같은 사용자 이름 / 암호 인증을 수행하는 기존 메커니즘의 보안 문제를 해결하는 SASL 메커니즘.&lt;/li&gt;
&lt;li&gt;Apache Kafka는&lt;span&gt;&amp;nbsp;&lt;/span&gt;SCRAM-SHA-256&lt;span&gt;&amp;nbsp;&lt;/span&gt;및&lt;span&gt;&amp;nbsp;&lt;/span&gt;SCRAM-SHA-512을 지원.&lt;/li&gt;
&lt;li&gt;Kafka의 SCRAM 구현은 ZooKeeper에 SCRAM 자격 증명을 저장하기 때문에 ZooKeeper가 개인 네트워크에 있을때 사용하는것을 권장&lt;/li&gt;
&lt;li&gt;runtime 으로 zookeeper 에 사용자에 대한 SCRAM 자격 증명을 생성할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;super user 생성&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;${KAFKA_HOME}/bin/kafka-configs.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 통해 super user 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;$ ./kafka-configs.sh --zookeeper $ZOOKEEPER:2181 --alter --add-config 'SCRAM-SHA-256=[password=super-user-password]' --entity-type users --entity-name super-user-id
Completed Updating config for entity: user-principal 'super-user-id'.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ANONYMOUS user 권한 추가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존에 보안설정을 안한 상태로 유지를 하고있다면 해당 권한 설정을 해주는게 좋다.(아니면&lt;span&gt;&amp;nbsp;&lt;/span&gt;allow.everyone.if.no.acl.found=true&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정을 broker properties 에 해도 되는데 그럴경우 기존 운영했던 port 를 종료할때 broker 를 다시 시작해야한다.)&lt;/li&gt;
&lt;li&gt;authorizer 를 정의하게 되면 기존 운영중인 9092 포트로 들어온 요청도 인증을 타게되어 ANONYMOUS user 로 인증이 되 유입되게 되는데 해당 user 에 대한 권한 설정을 해주지 않으면 broker 와 client 의 operation이 모두 denied 처리 되버린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;* cluster resource 권한 추가(broker 를 위한 설정)&lt;/h4&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;$ ./kafka-configs.sh --authorizer-properties zookeeper.connect=$ZOOKEEPER --add --allow-principal User:ANONYMOUS --operation &quot;ALL&quot; --cluster&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;* topic/group resource 권한 추가(client 를 위한 설정)&lt;/h4&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;$ ./kafka-configs.sh  --authorizer-properties zookeeper.connect=$ZOOKEEPER --add --allow-principal User:ANONYMOUS --operation &quot;ALL&quot; --topic &quot;*&quot; --group=&quot;*&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;broker 설정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 kafka broker 에 jaas config 파일을 생성(kafka_server_jaas.conf)&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;KafkaServer {
   org.apache.kafka.common.security.scram.ScramLoginModule required
   username=&quot;super-user-id&quot;
   password=&quot;super-user-id-password&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;broker 실행 옵션에&lt;span&gt;&amp;nbsp;&lt;/span&gt;-Djava.security.auth.login.config={path to jaas conf}/kafka_server_jaas.conf&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정 추가하여 재시작&lt;/li&gt;
&lt;li&gt;kafka&lt;span&gt;&amp;nbsp;&lt;/span&gt;server.properties&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일에 SASL/SCRAM 메커니즘 활성을 위한 설정 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;# List of enabled mechanisms, can be more than one
sasl.enabled.mechanisms=SCRAM-SHA-256 # or SCRAM-SHA-512
# Specify one of of the SASL mechanisms
sasl.mechanism.inter.broker.protocol=SCRAM-SHA-256 # or SCRAM-SHA-512&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브로커 간 통신에 SASL을 사용하여 통신할거라면&lt;span&gt;&amp;nbsp;&lt;/span&gt;server.properties&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 아래 설정을 추가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SASL_SSL: SSL 암호화를 사용하는 경우&lt;/li&gt;
&lt;li&gt;SASL_PLAINTEXT: SSL 암호화를 사용하지 않는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;security.inter.broker.protocol=SASL_PLAINTEXT # or SASL_SSL &lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 및 브로커 간 SASL연결을 어떻게 할것인지&lt;span&gt;&amp;nbsp;&lt;/span&gt;server.properties&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;listeners=SASL_PLAINTEXT://brokerhost1:9092&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보안설정 없이 동작하는 client 를 위해 기존 PLAINTEXT 도 유지를 하고자 한다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;server.properties&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 아래와 같이 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# 9093 포트로 진입시에만 SASL 사용
listeners=PLAINTEXT://brokerhost1:9092,SASL_PLAINTEXT://brokerhost1:9093
advertised.listeners=PLAINTEXT://brokerhost1:9092,SASL_PLAINTEXT://brokerhost1:9093&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;acl 권한 체크 활성화를 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;server.properties&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 아래와 같이 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;# ssl 설정을 할 경우 아래 설정 추가
# ssl.client.auth=requested 
authorizer.class.name=kafka.security.auth.SimpleAclAuthorizer
# client 에 보안 설정이 되어있지 않을 경우 User:ANONYMOUS 로 요청이 들어오게 되는데, 에러가 발생되지 않도록 잠시 super 유저로 설정
super.users=User:kafka-admin;User:ANONYMOUS 
# 초기 보안 설정 구축시 보안 설정이 되어있지 않은 client 에서도 정상동작을 원한다면 아래 설정을 추가하는것도 방법
allow.everyone.if.no.acl.found=true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;client&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 client 의 properties 파일에 아래 설정을 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;security.protocol=SASL_PLAINTEXT # or SASL_SSL
sasl.mechanism=SCRAM-SHA-256 # or SCRAM-SHA-512
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username=&quot;userid&quot; password=&quot;user password&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한 추가&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;${KAFKA_HOME}/bin/kafka-configs.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 통해 계정 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;$ ./kafka-configs.sh --zookeeper $ZOOKEEPER:2181 --alter --add-config 'SCRAM-SHA-256=[password=demo!23]' --entity-type users --entity-name demo
Completed Updating config for entity: user-principal 'demo'.&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;${KAFKA_HOME}/bin/kafka-acls.sh&lt;span&gt;&amp;nbsp;&lt;/span&gt;스크립트를 통해 권한 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예) demo&lt;span&gt;&amp;nbsp;&lt;/span&gt;계정이&lt;span&gt;&amp;nbsp;&lt;/span&gt;test&lt;span&gt;&amp;nbsp;&lt;/span&gt;토픽으로 접근시 모든 group 에 대해 읽기 권한 추가&lt;/h4&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;$ ./kafka-acls.sh --authorizer-properties zookeeper.connect=$ZOOKEEPER:2181 --add --allow-principal User:demo --operation read --topic test --group='*'
Adding ACLs for resource `Topic:LITERAL:test`: 
        User:demo has Allow permission for operations: Read from hosts: * 

Adding ACLs for resource `Group:LITERAL:*`: 
        User:demo has Allow permission for operations: Read from hosts: * 

Current ACLs for resource `Topic:LITERAL:test`: 
        User:demo has Allow permission for operations: Read from hosts: * 

Current ACLs for resource `Group:LITERAL:*`: 
        User:demo has Allow permission for operations: Read from hosts: *&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reference&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.confluent.io/current/kafka/authentication_sasl/authentication_sasl_scram.html&quot;&gt;https://docs.confluent.io/current/kafka/authentication_sasl/authentication_sasl_scram.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://kafka.apache.org/21/documentation.html#security_sasl_scram&quot;&gt;http://kafka.apache.org/21/documentation.html#security_sasl_scram&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.confluent.io/current/kafka/authorization.html&quot;&gt;https://docs.confluent.io/current/kafka/authorization.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://kafka.apache.org/21/documentation.html#security_authz&quot;&gt;http://kafka.apache.org/21/documentation.html#security_authz&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Kafka</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/17</guid>
      <comments>https://yang1s.tistory.com/17#entry17comment</comments>
      <pubDate>Mon, 14 Mar 2022 15:56:26 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] LeaderEpochCache Warn Log</title>
      <link>https://yang1s.tistory.com/16</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;개요&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Broker 에 아래와 같이 Warn Level 로그가 남겨짐&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[2020-07-30 12:40:03,237] WARN [LeaderEpochCache message_action-46] New epoch entry EpochEntry(epoch=27, startOffset=0) caused truncation of conflicting entries ListBuffer(EpochEntry(epoch=26, startOffset=0)). Cache now contains 1 entries. (kafka.server.epoch.LeaderEpochFileCache)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 로그가 왜 남겨지고, 해소할 방법은 없는지 탐색.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인 파악&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 로그는 partition leader 에 대한 epoch 값이 변경될 경우 변경된 epoch 값을 cache 및 기존 cache 된 내용을 truncate 하는 과정에서 발생하는 로그다.(LeaderEpoch 에 대한 내용은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://cwiki.apache.org/confluence/display/KAFKA/KIP-101+-+Alter+Replication+Protocol+to+use+Leader+Epoch+rather+than+High+Watermark+for+Truncation&quot;&gt;KIP-101&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고)&lt;/li&gt;
&lt;li&gt;로그를 남기고 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/4f1d4ffe9c97e03051bd77fb09bd957a1d496bc1/core/src/main/scala/kafka/server/epoch/LeaderEpochFileCache.scala?fbclid=IwAR31Z3p45xViGgv5NfSIRoRI5-dbBw02Bgd-NkZQLxC5NS1REV2xluhXP3M#L70&quot;&gt;v2.1.1 코드&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 아래와 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;/**
   * Remove any entries which violate monotonicity following the insertion of an assigned epoch.
   */
  private def truncateAndAppend(entryToAppend: EpochEntry): Unit = {
    validateAndMaybeWarn(entryToAppend)

    val (retainedEpochs, removedEpochs) = epochs.partition { entry =&amp;gt;
      entry.epoch &amp;lt; entryToAppend.epoch &amp;amp;&amp;amp; entry.startOffset &amp;lt; entryToAppend.startOffset
    }

    epochs = retainedEpochs :+ entryToAppend

    if (removedEpochs.isEmpty) {
      debug(s&quot;Appended new epoch entry $entryToAppend. Cache now contains ${epochs.size} entries.&quot;)
    } else {
      warn(s&quot;New epoch entry $entryToAppend caused truncation of conflicting entries $removedEpochs. &quot; +
        s&quot;Cache now contains ${epochs.size} entries.&quot;)
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신규로 append 되는 epoch 의 startOffset 이 기존과 같거나 작으면 해당 warn log 를 남기도록 되어있다.&lt;/li&gt;
&lt;li&gt;startOffset 이 작은 경우 문제가 있는 케이스이지만 같은건 정상적인 상황에서도 가능한 케이스라 이렇게 남긴것이 의문.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/kafka/blob/18a913733fb71c0178e80cc4805251d1600e7fac/core/src/main/scala/kafka/server/epoch/LeaderEpochFileCache.scala#L70&quot;&gt;v2.3.0 코드&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 보니 아래와 같이 변경되어있다.(해당 코드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/apache/kafka/pull/6773&quot;&gt;PR&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서 수정 요청되었다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;/**
   * Remove any entries which violate monotonicity following the insertion of an assigned epoch.
   */
  private def truncateAndAppend(entryToAppend: EpochEntry): Unit = {
    validateAndMaybeWarn(entryToAppend)

    val (retainedEpochs, removedEpochs) = epochs.partition { entry =&amp;gt;
      entry.epoch &amp;lt; entryToAppend.epoch &amp;amp;&amp;amp; entry.startOffset &amp;lt; entryToAppend.startOffset
    }

    epochs = retainedEpochs :+ entryToAppend

    if (removedEpochs.isEmpty) {
      debug(s&quot;Appended new epoch entry $entryToAppend. Cache now contains ${epochs.size} entries.&quot;)
    } else if (removedEpochs.size &amp;gt; 1 || removedEpochs.head.startOffset != entryToAppend.startOffset) {
      // Only log a warning if there were non-trivial removals. If the start offset of the new entry
      // matches the start offfset of the removed epoch, then no data has been written and the truncation
      // is expected.
      warn(s&quot;New epoch entry $entryToAppend caused truncation of conflicting entries $removedEpochs. &quot; +
        s&quot;Cache now contains ${epochs.size} entries.&quot;)
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저렇게 변경한 이유는 위에서 의문을 제기한 startOffset 이 같을 경우가 꼭 문제가 있는 상황은 아니기 때문이다. 메세지가 publish 되지 않은 상황이라면(traffic 이 적은 상황이라면) 충분히 가능한 케이스이기 때문에 2.3.1 에서 해당 상황은 로그를 안남기는거로 제외되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Leader epoch 값이 왜 달랐을까?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;kafka broker 에서는 partition 에 대한 leader epoch 값을&lt;span&gt;&amp;nbsp;&lt;/span&gt;Partition&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체에도 저장(memory)하고, 추후 복구를 대비하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;LeaderEpochFileCache&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용하여 file 로도 저장한다.&lt;/li&gt;
&lt;li&gt;문제는 partition leader 가 변경될 경우 변경된 leader epoch 값을 memory(Partition 객체) 와 file(LeaderEpochFileCache) 에 모두 저장하는 노드는 partition leader 노드만 그렇게 저장하고, follower 노드는 memory(Partition 객체) 에만 저장한다.&lt;/li&gt;
&lt;li&gt;그러다보니 follower 노드에서 fetcher thread 를 통해 message 복제를 시도할 때 memory 에 저장된 leader epoch 값으로 LeaderEpochFileCache 에 append 를 하니 위와 같은 로그가 남게되는것이다.&lt;/li&gt;
&lt;li&gt;즉, 갑자기 leader epoch 값이 올라가서 발생한게 아니라 기존에 leader epoch 값이 변경이 되었는데(브로커 노드 재시작 등으로 인해) follower 노드에서 이에 대한 update 가 message 복제 시점에 발생해서 그런 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관련 코드&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;partition leader 로 정의하는 Partition 의&lt;span&gt;&amp;nbsp;&lt;/span&gt;makeLeader&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;/**
   * Make the local replica the leader by resetting LogEndOffset for remote replicas (there could be old LogEndOffset
   * from the time when this broker was the leader last time) and setting the new leader and ISR.
   * If the leader replica id does not change, return false to indicate the replica manager.
   */
  def makeLeader(controllerId: Int, partitionStateInfo: LeaderAndIsrRequest.PartitionState, correlationId: Int): Boolean = {
    val (leaderHWIncremented, isNewLeader) = inWriteLock(leaderIsrUpdateLock) {
      val newAssignedReplicas = partitionStateInfo.basePartitionState.replicas.asScala.map(_.toInt)
      // record the epoch of the controller that made the leadership decision. This is useful while updating the isr
      // to maintain the decision maker controller's epoch in the zookeeper path
      controllerEpoch = partitionStateInfo.basePartitionState.controllerEpoch
      // add replicas that are new
      val newInSyncReplicas = partitionStateInfo.basePartitionState.isr.asScala.map(r =&amp;gt; getOrCreateReplica(r, partitionStateInfo.isNew)).toSet
      // remove assigned replicas that have been removed by the controller
      (assignedReplicas.map(_.brokerId) -- newAssignedReplicas).foreach(removeReplica)
      inSyncReplicas = newInSyncReplicas
      newAssignedReplicas.foreach(id =&amp;gt; getOrCreateReplica(id, partitionStateInfo.isNew))

      val leaderReplica = localReplicaOrException
      val leaderEpochStartOffset = leaderReplica.logEndOffset.messageOffset
      info(s&quot;$topicPartition starts at Leader Epoch ${partitionStateInfo.basePartitionState.leaderEpoch} from &quot; +
        s&quot;offset $leaderEpochStartOffset. Previous Leader Epoch was: $leaderEpoch&quot;)

      //We cache the leader epoch here, persisting it only if it's local (hence having a log dir)
      // * Partition 객체에서 멤버 변수로 정의된 leaderEpoch 를 새로 정의한다(메모리에 저장한다)
      leaderEpoch = partitionStateInfo.basePartitionState.leaderEpoch
      leaderEpochStartOffsetOpt = Some(leaderEpochStartOffset)
      zkVersion = partitionStateInfo.basePartitionState.zkVersion

      // In the case of successive leader elections in a short time period, a follower may have
      // entries in its log from a later epoch than any entry in the new leader's log. In order
      // to ensure that these followers can truncate to the right offset, we must cache the new
      // leader epoch and the start offset since it should be larger than any epoch that a follower
      // would try to query.
      // * LeaderEpochFileCache 에 변경된 leaderEpoch 값을 저장한다.(file 에 저장한다.)
      leaderReplica.log.foreach { log =&amp;gt;
        log.maybeAssignEpochStartOffset(leaderEpoch, leaderEpochStartOffset)
      }

      val isNewLeader = !leaderReplicaIdOpt.contains(localBrokerId)
      val curLeaderLogEndOffset = leaderReplica.logEndOffset.messageOffset
      val curTimeMs = time.milliseconds
      // initialize lastCaughtUpTime of replicas as well as their lastFetchTimeMs and lastFetchLeaderLogEndOffset.
      (assignedReplicas - leaderReplica).foreach { replica =&amp;gt;
        val lastCaughtUpTimeMs = if (inSyncReplicas.contains(replica)) curTimeMs else 0L
        replica.resetLastCaughtUpTime(curLeaderLogEndOffset, curTimeMs, lastCaughtUpTimeMs)
      }

      if (isNewLeader) {
        // construct the high watermark metadata for the new leader replica
        leaderReplica.convertHWToLocalOffsetMetadata()
        // mark local replica as the leader after converting hw
        leaderReplicaIdOpt = Some(localBrokerId)
        // reset log end offset for remote replicas
        assignedReplicas.filter(_.brokerId != localBrokerId).foreach(_.updateLogReadResult(LogReadResult.UnknownLogReadResult))
      }
      // we may need to increment high watermark since ISR could be down to 1
      (maybeIncrementLeaderHW(leaderReplica), isNewLeader)
    }
    // some delayed operations may be unblocked after HW changed
    if (leaderHWIncremented)
      tryCompleteDelayedRequests()
    isNewLeader
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;partition follower 로 정의하는 Partition 의&lt;span&gt;&amp;nbsp;&lt;/span&gt;makeFollower&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;/**
   *  Make the local replica the follower by setting the new leader and ISR to empty
   *  If the leader replica id does not change and the new epoch is equal or one
   *  greater (that is, no updates have been missed), return false to indicate to the
    * replica manager that state is already correct and the become-follower steps can be skipped
   */
  def makeFollower(controllerId: Int, partitionStateInfo: LeaderAndIsrRequest.PartitionState, correlationId: Int): Boolean = {
    inWriteLock(leaderIsrUpdateLock) {
      val newAssignedReplicas = partitionStateInfo.basePartitionState.replicas.asScala.map(_.toInt)
      val newLeaderBrokerId = partitionStateInfo.basePartitionState.leader
      val oldLeaderEpoch = leaderEpoch
      // record the epoch of the controller that made the leadership decision. This is useful while updating the isr
      // to maintain the decision maker controller's epoch in the zookeeper path
      controllerEpoch = partitionStateInfo.basePartitionState.controllerEpoch
      // add replicas that are new
      newAssignedReplicas.foreach(r =&amp;gt; getOrCreateReplica(r, partitionStateInfo.isNew))
      // remove assigned replicas that have been removed by the controller
      (assignedReplicas.map(_.brokerId) -- newAssignedReplicas).foreach(removeReplica)
      inSyncReplicas = Set.empty[Replica]
      // * Partition 객체의 멤버 변수인 leaderEpoch 에만 변경된 epoch 값을 정의하고 LeaderEpochFileCache 에는 저장하지 않는다.
      leaderEpoch = partitionStateInfo.basePartitionState.leaderEpoch
      leaderEpochStartOffsetOpt = None
      zkVersion = partitionStateInfo.basePartitionState.zkVersion

      if (leaderReplicaIdOpt.contains(newLeaderBrokerId) &amp;amp;&amp;amp; leaderEpoch == oldLeaderEpoch) {
        false
      } else {
        leaderReplicaIdOpt = Some(newLeaderBrokerId)
        true
      }
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그를 통한 검증&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존 leader 노드를 재시작하여 partition leader 가 변경되었었다.&lt;/li&gt;
&lt;li&gt;partition leader 가 변경되어 leaderEpoch 값이 변경된건 leader 와 follower 노드 모두 정상 처리 되었다.&lt;/li&gt;
&lt;li&gt;leader 노드 로그&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;[2020-07-10 12:22:06,652] TRACE [Broker id=23] Cached leader info PartitionState(controllerEpoch=16, leader=23, leaderEpoch=211, isr=[24, 23], zkVersion=295, replicas=[23, 24], offlineReplicas=[]) for partition test-48 in response to UpdateMetadata request sent by controller 24 epoch 16 with correlation id 11 (state.change.logger)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;follower 노드 로그&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;[2020-07-10 12:22:04,687] TRACE [Broker id=24] Cached leader info PartitionState(controllerEpoch=16, leader=23, leaderEpoch=211, isr=[24, 23], zkVersion=295, replicas=[23, 24], offlineReplicas=[]) for partition test-48 in response to UpdateMetadata request sent by controller 24 epoch 16 with correlation id 17 (state.change.logger)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브로커 재시작 이후 test topic 48번 파티션으로 최초로 메세지가 유입되어 follower 노드쪽에 복제가 발생하고 아래와 같은 로그가 발생하였다&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot; data-ke-language=&quot;scala&quot;&gt;&lt;code&gt;[2020-08-04 19:03:40,945] WARN [LeaderEpochCache test-48] New epoch entry EpochEntry(epoch=211, startOffset=40) caused truncation of conflicting entries ListBuffer(EpochEntry(epoch=210, startOffset=40)). Cache now contains 1 entries. (kafka.server.epoch.LeaderEpochFileCache)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의문&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;왜 follower 노드에서는 파티션 리더가 변경될 당시에 file cache 를 하지 않았는지 의문.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Kafka</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/16</guid>
      <comments>https://yang1s.tistory.com/16#entry16comment</comments>
      <pubDate>Mon, 14 Mar 2022 15:43:52 +0900</pubDate>
    </item>
    <item>
      <title>[Kafka] fetch.max.wait.ms 와 fetch.min.bytes 설정 으로 인한 Consumer 성능 이슈 정리</title>
      <link>https://yang1s.tistory.com/15</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;이슈&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;producer 가 메세지를 일정 간격으로 전송(500ms)&lt;/li&gt;
&lt;li&gt;consumer 설정 중&lt;span&gt;&amp;nbsp;&lt;/span&gt;fetch.min.bytes&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 1로 설정(데이터가 있으면 즉시 받도록 최소로 설정) 하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;fetch.max.wait.ms&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 default(500ms)로 설정했을 때, consumer 가 저장된 메세지를 읽어들이는데 지연시간이 발생.(메세지가 broker 에 도착시간은 12시 0분 0초 0ms 인데 해당 메세지를 컨슈머가 읽은 시간은 12시 0분 0초 300ms)&lt;/li&gt;
&lt;li&gt;fetch.max.wait.ms&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 100ms 로 설정하였을 경우 지연시간이 줄어듬&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://kafka.apache.org/21/documentation.html&quot;&gt;kafka documentation&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 정리된 설정값의 설명을 보면&lt;span&gt;&amp;nbsp;&lt;/span&gt;fetch.min.bytes&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 1로 설정할 경우 데이터가 들어왔을 때 즉시 응답을 줄거로 예상하지만 실제 동작은 그게 아니였다.&lt;/li&gt;
&lt;li&gt;consumer 에서 fetch request 를 보냈을 때 broker 에서는 응답해야할 메세지가 있으면&lt;span&gt;&amp;nbsp;&lt;/span&gt;fetch.min.bytes&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 1 이기 때문에 즉시 응답을 준다.&lt;/li&gt;
&lt;li&gt;하지만 broker 에서 응답을 줄 메세지가 없을 경우 대기를 하게 되고, 이 대기 시간은 새로운 메세지가 들어올때 끝나는게 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;fetch.max.wait.ms&lt;span&gt;&amp;nbsp;&lt;/span&gt;만큼 기다리게 된다.&lt;/li&gt;
&lt;li&gt;즉, 응답할 메세지가 없는 상태에서 fetch request 가 0.3 초에 요청되고 메세지가 0.5 초에 도착했더라도&lt;span&gt;&amp;nbsp;&lt;/span&gt;fetch.max.wait.ms&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 500ms 로 설정되었다면 0.5초에 응답을 받는게 아니라 0.8초에 응답을 받게되어 지연이 발생하게 된다.&lt;/li&gt;
&lt;li&gt;해당 내용은 &lt;a href=&quot;https://github.com/apache/kafka/blob/61c8228f314794228e9b2a74b6034c56a5e3836d/core/src/main/scala/kafka/server/ReplicaManager.scala#L868&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;kafka broker code&lt;/a&gt; 를 통해 확인 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1647237817661&quot; class=&quot;scala&quot; data-ke-language=&quot;scala&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    // respond immediately if 1) fetch request does not want to wait
    //                        2) fetch request does not require any data
    //                        3) has enough data to respond
    //                        4) some error happens while reading data
    if (timeout &amp;lt;= 0 || fetchInfos.isEmpty || bytesReadable &amp;gt;= fetchMinBytes || errorReadingData) {
      val fetchPartitionData = logReadResults.map { case (tp, result) =&amp;gt;
        tp -&amp;gt; FetchPartitionData(result.error, result.highWatermark, result.leaderLogStartOffset, result.info.records,
          result.lastStableOffset, result.info.abortedTransactions)
      }
      responseCallback(fetchPartitionData)
    } else {
      // construct the fetch results from the read results
      val fetchPartitionStatus = new mutable.ArrayBuffer[(TopicPartition, FetchPartitionStatus)]
      fetchInfos.foreach { case (topicPartition, partitionData) =&amp;gt;
        logReadResultMap.get(topicPartition).foreach(logReadResult =&amp;gt; {
          val logOffsetMetadata = logReadResult.info.fetchOffsetMetadata
          fetchPartitionStatus += (topicPartition -&amp;gt; FetchPartitionStatus(logOffsetMetadata, partitionData))
        })
      }
      val fetchMetadata = FetchMetadata(fetchMinBytes, fetchMaxBytes, hardMaxBytesLimit, fetchOnlyFromLeader,
        fetchIsolation, isFromFollower, replicaId, fetchPartitionStatus)
      val delayedFetch = new DelayedFetch(timeout, fetchMetadata, this, quota, responseCallback)

      // create a list of (topic, partition) pairs to use as keys for this delayed fetch operation
      val delayedFetchKeys = fetchPartitionStatus.map { case (tp, _) =&amp;gt; new TopicPartitionOperationKey(tp) }

      // try to complete the request immediately, otherwise put it into the purgatory;
      // this is because while the delayed fetch operation is being created, new requests
      // may arrive and hence make this operation completable.
      delayedFetchPurgatory.tryCompleteElseWatch(delayedFetch, delayedFetchKeys)
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;주석에 설명되어있듯이 즉시 응답을 주는 조건은 다음과 같다.&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;timeout 값(fetch.max.wait.ms 설정값)을 0 이하로 했을 경우&lt;/li&gt;
&lt;li&gt;fetch 요청할 토픽과 파티션 정보가 없을 경우(이런 케이스는 어떤 케이스인지 확인 필요)&lt;/li&gt;
&lt;li&gt;fetch.min.bytes 이상의 데이터를 읽었을 경우&lt;/li&gt;
&lt;li&gt;에러가 발생했을 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그래서.. 결론은?&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;fetch.max.wait.ms&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 줄이는게 무조건 좋은건 아니다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;fetch.max.wait.ms&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 줄이게 되면 그만큼 polling 루프가 빨라지게 되고 fetch 요청도 많아져 broker 에 부담이 갈 수 밖에 없다. 또한,&lt;span&gt;&amp;nbsp;&lt;/span&gt;max.partition.fetch.bytes,&lt;span&gt;&amp;nbsp;&lt;/span&gt;fetch.max.bytes&lt;span&gt;&amp;nbsp;&lt;/span&gt;만큼 받지 못하고 짧게 끊어서 여러번 받기 때문에 네트워크 비용도 증가한다.&lt;/li&gt;
&lt;li&gt;일반적인 client 의 consumer 라면, 그리고 message 를 간헐적으로 보내는 producer 가 있을때 빠르게 반응해야하는게 아니라면 해당 설정은 default 로 두어도 무방하다.&lt;/li&gt;
&lt;li&gt;하지만 어떤 간격의 message 가 오더라도 빠르게 반응하고 전달해야하는 Consumer 라면 fetch.max.wait.ms&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 default 값보다 줄이는게 이득으로 보인다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Kafka</category>
      <category>consumer</category>
      <category>kafka</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/15</guid>
      <comments>https://yang1s.tistory.com/15#entry15comment</comments>
      <pubDate>Mon, 14 Mar 2022 15:06:17 +0900</pubDate>
    </item>
    <item>
      <title>[VictoriaMetrics] 설치</title>
      <link>https://yang1s.tistory.com/14</link>
      <description>&lt;h1&gt;스펙&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Server&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CentOS 7.4 64bit&lt;/li&gt;
&lt;li&gt;CPU: 2core&lt;/li&gt;
&lt;li&gt;Mem: 2GB&lt;/li&gt;
&lt;li&gt;Disk: 100GB&lt;/li&gt;
&lt;li&gt;User: irteam(home: /home1/irteam/)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;VictoriaMetrics&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;version: 1.62.0&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Go&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;version: 1.16.5&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;HA 구성&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HA 구성 방법에는 단일 노드와 cluster 구성 방법이 있다.&lt;/li&gt;
&lt;li&gt;단일 노드로 구성할 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.victoriametrics.com/vmagent.html&quot;&gt;vmagent&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 통해 단일 노드로 구축한 여러대의 victoria-metrics 에 write 를 할 수 있다. 만약 vmagent 를 사용하지 않을 경우 client 가 직접 이런 처리를 해야한다.&lt;/li&gt;
&lt;li&gt;cluster 로 구성할 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;vminsert&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 단일 노드시 사용하는 vmagent 역할을 하는거로 보인다.(이때 replica 도 정의할 수 있다.) 자세한 사항은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/VictoriaMetrics/VictoriaMetrics/tree/cluster&quot;&gt;cluster guide&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고&lt;/li&gt;
&lt;li&gt;&lt;b&gt;metric 데이터량이 크지 않을 경우 단일 노드로 구성하는걸 추천&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;설치 가이드&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;공통 사항&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;dir 생성&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;# apps 및 victoria metrics bin 폴더 생성
mkdir -p /home1/irteam/apps/victoria-metrics/
# data 를 저장할 경로 생성 
mkdir -p /home1/irteam/data/victoria-metrics
mkdir -p /home1/irteam/data/vmagent
# log 폴더 생성
mkdir -p /home1/irteam/logs/victoria-metrics/
# 필요한 내용을 설치할 INSTALL 폴더 생성
mkdir -p /home1/irteam/INSTALL&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1안. source compile&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;go 설치&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;cd /home1/irteam/INSTALL
wget 'https://dl.google.com/go/go1.16.5.linux-amd64.tar.gz'
tar -zxvf go1.16.5.linux-amd64.tar.gz
# apps 로 이동
mv go ~/apps&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환경 변수 설정&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;export GO_HOME=${APP_HOME}/apps/go
export PATH=${GO_HOME}/bin:$PATH&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;make 수행시 compile 에러 방지를 위해 GCC upgrade&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;sudo yum install centos-release-scl -y
sudo yum install devtoolset-7 -y
scl enable devtoolset-7 bash&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Victoria Metrics&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;cd /home1/irteam/INSTALL
git clone -b v1.62.0 https://github.com/VictoriaMetrics/VictoriaMetrics.git
cd VictoriaMetrics
# 1.62.0 이 맞는지 확인을 위하 한번더 checkout
git checkout -b v1.62.0
# fatal: A branch named 'v1.62.0' already exists. 라 뜨면 정상적으로 1.62.0 을 clone 받은 것.
make victoria-metrics
# 생성된 binary 를 apps 폴더로 이동
mv bin/victoria-metrics ~/apps/victoria-metrics/&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vmagent&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# Victoria Metrics 설치 이후에 진행.
make vmagent
# 생성된 binary 를 apps 폴더로 이동
mv bin/vmagent ~/apps/victoria-metrics/&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vmalert&lt;/h3&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;# Victoria Metrics 설치 이후에 진행.
make vmalert
# 생성된 binary 를 apps 폴더로 이동
mv bin/vmalert ~/apps/victoria-metrics/&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2안. victoria metrics 에서 제공해준 product download&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Victoria Metrics&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;cd /home1/irteam/INSTALL
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.62.0/victoria-metrics-amd64-v1.62.0.tar.gz
tar -zxvf victoria-metrics-amd64-v1.62.0.tar.gz
mv victoria-metrics-prod ~/apps/victoria-metrics/victoria-metrics&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vmagent&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;cd /home1/irteam/INSTALL
wget https://github.com/VictoriaMetrics/VictoriaMetrics/releases/download/v1.62.0/vmutils-amd64-v1.62.0.tar.gz
tar -zxvf vmutils-amd64-v1.62.0.tar.gz
mv vmagent-prod ~/apps/victoria-metrics/vmagent&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vmalert&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다운로드&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;# vmagent 설치로 vmutils-amd64-v1.62.0.tar.gz 을 download 받고 압축을 해제했을 경우
mv vmalert-prod ~/apps/victoria-metrics/vmalert&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h1&gt;실행&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;victoria metrics&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bin 폴더 접근하여 아래와 같은 arg 로 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;cd /home1/irteam/apps/victoria-metrics/
./victoria-metrics \ 
     -storageDataPath=/home1/irteam/data/victoria-metrics \
     1&amp;gt; /home1/irteam/logs/victoria-metrics/vm.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행시 줄수 있는 옵션은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/VictoriaMetrics/VictoriaMetrics/tree/v1.62.0#list-of-command-line-flags&quot;&gt;link&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고&lt;/li&gt;
&lt;li&gt;systemd 를 사용하여 실행하는 방법은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/VictoriaMetrics/VictoriaMetrics/issues/43&quot;&gt;link&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;vmagent&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bin 폴더 접근하여 아래와 같은 arg 로 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;cd /home1/irteam/apps/victoria-metrics/
./vmagent \
    -remoteWrite.tmpDataPath=/home1/irteam/data/vmagent \
    -remoteWrite.maxDiskUsagePerURL=1GB \
    -remoteWrite.url=http://&amp;lt;victoriametrics-host-1&amp;gt;:8428/api/v1/write \
    -remoteWrite.url=http://&amp;lt;victoriametrics-host-2&amp;gt;:8428/api/v1/write \
    1&amp;gt; /home1/irteam/logs/victoria-metrics/vmagent.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행시 줄수 있는 옵션은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.victoriametrics.com/vmagent.html#advanced-usage&quot;&gt;link&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;vmalert&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/home1/irteam/config/vmalert 폴더에 alert.rules 설정 파일 정의&lt;/li&gt;
&lt;li&gt;bin 폴더 접근하여 아래와 같은 arg 로 실행&lt;/li&gt;
&lt;li&gt;alert rule 은 prometheus 에서 정의한 &lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/#defining-alerting-rules&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;alert rule&lt;/a&gt; 을 따른다&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;./vmalert -rule=/home1/irteam/config/vmalert/alert.rules \            # Path to the file with rules configuration. Supports wildcard
    -datasource.url=http://localhost:8428 \  # PromQL compatible datasource
    -notifier.url=http://&amp;lt;alertmanager addr&amp;gt;:9093 \    # AlertManager URL
    -remoteWrite.url=http://localhost:8428 \ # Remote write compatible storage to persist rules
    -remoteRead.url=http://localhost:8428  \ # MetricsQL compatible datasource to restore alerts state from
    -evaluationInterval=30s \
    1&amp;gt; /home1/irteam/logs/victoria-metrics/vmalert.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>Database</category>
      <category>tsdb</category>
      <category>VictoriaMetrics</category>
      <category>Vmagent</category>
      <category>Vmalert</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/14</guid>
      <comments>https://yang1s.tistory.com/14#entry14comment</comments>
      <pubDate>Mon, 14 Mar 2022 14:13:04 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - Utils</title>
      <link>https://yang1s.tistory.com/13</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;BlockHound&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Webflux 는 예상치 못한 blocking 로직이 있을 경우 성능에 큰 영향을 미치기 때문에 이러한 로직이 있는지 탐지하고 수정할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reactor/BlockHound&quot;&gt;BlockHound&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;는 비지니스 로직이 수행될 때 blocking 로직이 있으면 exception 을 던져 blocking 로직을 파악할 수 있게 도와준다.&lt;/li&gt;
&lt;li&gt;번역 gateway 에서는 아래와 같은 방식으로 blockhound 를 실행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dependency 추가&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt; &amp;lt;dependency&amp;gt;
     &amp;lt;groupId&amp;gt;io.projectreactor.tools&amp;lt;/groupId&amp;gt;
     &amp;lt;artifactId&amp;gt;blockhound&amp;lt;/artifactId&amp;gt;
     &amp;lt;version&amp;gt;1.0.3.RELEASE&amp;lt;/version&amp;gt;
 &amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application class 에서 BlockHound 실행여부를 판단&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@SpringBootApplication
public class Application {
    static final String BLOCK_HOUND_INSTALL = &quot;service.block-hound.install&quot;; // (1)

    public static void main(String[] args) {
        boolean installBlockHound = SpringApplication.run(Application.class, args) // (2)
            .getEnvironment()
            .getProperty(BLOCK_HOUND_INSTALL, boolean.class, false);

        if (installBlockHound) {
            BlockHound.install(); // (3)
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) application 실행시 특정 config 값을 통해 BlockHound 실행 여부를 판단할 수 있게 정의한다.&lt;br /&gt;(2) application 실행시 BlockHound 실행 여부를 판단할 수 있는 config 값을 조회한다.&lt;br /&gt;(3) BlockHound 를 실행하도록 config 가 주입되었으면 install API 호출을 통해 실행&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비지니스 로직 수행중에 blocking 로직이 탐지되면 아래와 같이 에러가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;java.lang.Error: Blocking call! java.lang.Thread.sleep
at reactor.blockhound.BlockHound$Builder.lambda$new$0(BlockHound.java:167)
at reactor.blockhound.BlockHound$Builder.lambda$install$6(BlockHound.java:226)
at reactor.blockhound.BlockHoundRuntime.checkBlocking(BlockHoundRuntime.java:46)
at java.lang.Thread.sleep(Thread.java)
at com.example.demo.DemoApplication.block(DemoApplication.java:30)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-reloadable-configuration&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#reloadable-configuration&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Reloadable Configuration&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reloadable Configuration 을 하는 방법은 몇가지 있지만 Spring 에서 공식적으로 지원하는 방식은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://spring.io/projects/spring-cloud&quot;&gt;Spring Cloud&lt;/a&gt;에서 지원하는 @RefreshScope 사용한 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud 에 대한 dependency 추가&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;&amp;lt;dependencyManagement&amp;gt;
    &amp;lt;dependencies&amp;gt;
        &amp;lt;dependency&amp;gt;
            &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
            &amp;lt;artifactId&amp;gt;spring-cloud-dependencies&amp;lt;/artifactId&amp;gt;
            &amp;lt;version&amp;gt;${spring-cloud.version}&amp;lt;/version&amp;gt;
            &amp;lt;type&amp;gt;pom&amp;lt;/type&amp;gt;
            &amp;lt;scope&amp;gt;import&amp;lt;/scope&amp;gt;
        &amp;lt;/dependency&amp;gt;
    &amp;lt;/dependencies&amp;gt;
&amp;lt;/dependencyManagement&amp;gt;

&amp;lt;dependencies&amp;gt;
    &amp;lt;dependency&amp;gt;
        &amp;lt;groupId&amp;gt;org.springframework.cloud&amp;lt;/groupId&amp;gt;
        &amp;lt;artifactId&amp;gt;spring-cloud-starter&amp;lt;/artifactId&amp;gt;
    &amp;lt;/dependency&amp;gt;
&amp;lt;/dependencies&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Reloadable ConfigurationProperties 정의&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;dart&quot;&gt;&lt;code&gt;	@Bean
	@RefreshScope
	@ConfigurationProperties(prefix = &quot;example.reload.properties&quot;)
	public Map&amp;lt;String, String&amp;gt; exampleProperties() {
		return new HashMap&amp;lt;String, String&amp;gt;();
	}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Configuration 이 필요한 곳에서는 Bean 으로 등록된 Properties 를 주입받아 사용.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;@Autowired
private Map&amp;lt;String, String&amp;gt; exampleProperties;

/**
 * properties value 조회
 */
public String getValue(String key) {
   return exampleProperties.get(key);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Configuration 변경이 필요할 경우 config file 을 수정한 뒤 다음과 같이 actuator API 호출. (단, management.endpoints.web.exposure.include 에 refresh 가 활성화 되있어야 한다.)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;curl -XPOST http://{host}:{port}/actuator/refresh&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>Spring Framework</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>webflux</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/13</guid>
      <comments>https://yang1s.tistory.com/13#entry13comment</comments>
      <pubDate>Mon, 14 Mar 2022 13:14:01 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - Test Code</title>
      <link>https://yang1s.tistory.com/12</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Test Code&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-stepverifier&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#stepverifier&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;StepVerifier&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reactor 를 사용할 경우 주로 다루는 Mono/Flux 를 반환하는 리액티브 API 에 대한 테스트는 기존 Test Code 작성 방법으로는 테스트하기 힘들다.&lt;/li&gt;
&lt;li&gt;아래와 같이 reactor-test 에 대한 dependency 를 추가해야 사용이 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;io.projectreactor&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;reactor-test&amp;lt;/artifactId&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content--일반적인-test-code-작성시-발생할-수-있는-실수-1&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#-%EC%9D%BC%EB%B0%98%EC%A0%81%EC%9D%B8-test-code-%EC%9E%91%EC%84%B1%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EC%8B%A4%EC%88%98-1&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;◉ 일반적인 Test Code 작성시 발생할 수 있는 실수 1&lt;/h4&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
public void mono() {
  Mono&amp;lt;Integer&amp;gt; mono = Mono.just(1) // (1)
        .subscribeOn(Schedulers.single());  // (2)

  Mono.subscribe(item -&amp;gt; assertThat(item).isEqualTo(1)); // (3)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 1을 생성하는 Mono 생성.&lt;br /&gt;(2) single thread 로 비동기 동작을 수행하도록 scheduler 설정.&lt;br /&gt;(3) subscribe 를 통해 생성한 Mono 구독 후 검증.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위의 테스트 코드의 결과는 성공으로 반환되지만 실제로는 정상적인 테스트가 이루어지지 않은 것이다. 그 이유는 Scheduler 를 통해 비동기로 테스트코드가 동작하게 되면서 아직 테스트코드가 전부 실행되지 않은 상태에서 mono Test Code 함수가 정상적으로 종료되었기 때문이다.&lt;/li&gt;
&lt;li&gt;실제로&lt;span&gt;&amp;nbsp;&lt;/span&gt;isEqualTo(1)&lt;span&gt;&amp;nbsp;&lt;/span&gt;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;isEqualTo(100)&lt;span&gt;&amp;nbsp;&lt;/span&gt;으로 변경해도 테스트 코드는 성공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content--일반적인-test-code-작성시-발생할-수-있는-실수-2&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#-%EC%9D%BC%EB%B0%98%EC%A0%81%EC%9D%B8-test-code-%EC%9E%91%EC%84%B1%EC%8B%9C-%EB%B0%9C%EC%83%9D%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EC%8B%A4%EC%88%98-2&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;◉ 일반적인 Test Code 작성시 발생할 수 있는 실수 2&lt;/h4&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
public void mono() {
  Mono&amp;lt;Integer&amp;gt; mono = Mono.just(1) 
        .subscribeOn(Schedulers.single()); 

  CountDownLatch latch = new CountDownLatch(1); // (1)

  Mono.subscribe(item -&amp;gt; {
        assertThat(item).isEqualTo(2); // (2)
        latch.countDown(); // (3)      
  }); 

  latch.await(); // (4)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 1의 실수 케이스를 보완하기 위해 비동기 동작이 끝날때까지 기다리도록 CountDownLatch 선언.&lt;br /&gt;(2) 테스트 코드 검증 로직에서 실패 발생.&lt;br /&gt;(3) 테스트가 성공할 경우 count 가 증가하도록 하려 했으나 (2) 에서 예외가 발생하여 수행되지 못함.&lt;br /&gt;(4) 테스트가 무한정 대기하게 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기로 동작하는 로직을 검증하기 위해 blocking 로직을 테스트코드에 넣었다가 발생할 수 있는 실수다.&lt;/li&gt;
&lt;li&gt;예외 처리를 통해 위와 같은 실수를 방지할수는 있지만 고작 1을 publish 하는 Mono 에 대한 검증치고는 테스트코드가 너무 방대하고 신경써야할게 많다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-stepverifier-를-사용할-경우&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#stepverifier-%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%A0-%EA%B2%BD%EC%9A%B0&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;StepVerifier 를 사용할 경우&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에 실수 케이스에 대해 StepVerifier 를 사용할 경우 아래와 같이 코드를 짤 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
public void mono() {
  Mono&amp;lt;Integer&amp;gt; mono = Mono.just(1) 
        .subscribeOn(Schedulers.single());  

  StepVerifier.create(mono) // (1)
        .expectNext(1)      // (2)
        .verifyComplete();  // (3)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) mono 에 대한 StepVerifier Step 생성&lt;br /&gt;(2) Mono 에서 처음 publish 한 데이터가 1인지 검증&lt;br /&gt;(3) 테스트 검증 완료&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-예외-발생에-대한-검증&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#%EC%98%88%EC%99%B8-%EB%B0%9C%EC%83%9D%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B2%80%EC%A6%9D&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;예외 발생에 대한 검증&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예외 발생에 대한 검증을 위해 제공하는 API 들도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
public void flux() {
  Flux&amp;lt;Integer&amp;gt; flux = Flux.just(1)
        .concatWith(Mono.error(new IllegalArgumentException(&quot;BOOM!&quot;)) // (1)
        .subscribeOn(Schedulers.single());  

  StepVerifier.create(flux) 
        .expectNext(1)     
        .expectErrorMatches(throwable -&amp;gt; throwable instanceof IllegalArgumentException &amp;amp;&amp;amp; throwable.getMessage().equals(&quot;BOOM!&quot;)) // (2)
        .verify(); // (3)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) 1 이후 IllegalArgumentException 이 발생하도록 추가&lt;br /&gt;(2) 1 이후 error 가 발생할거라 기대하고 해당 에러가 IllegalArgumentException 인지, message 가 기대한것과 같은지 확인&lt;br /&gt;(3) 테스트 검증 완료&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-webtestclient&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#webtestclient&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;WebTestClient&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Request Mapping 및 Handler, Filter 등의 동작에 대한 검증을 할 수 있도록 WebTestClient 를 제공하고 있다.&lt;/li&gt;
&lt;li&gt;아래와 같이 spring-boot-starter-test 에 대한 dependency 를 추가해야 사용이 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-test&amp;lt;/artifactId&amp;gt;
    &amp;lt;scope&amp;gt;test&amp;lt;/scope&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같이 Controller 로 정의한 API 에 대해 테스트 코드를 작성해볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Controller&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Controller
public class HelloController {
   @GetMapping(&quot;/hello/{name}&quot;)
   public Mono&amp;lt;String&amp;gt; hello(@PathVariable String name) {
       return Mono.just(&quot;hello &quot; + name);
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebTestClient Test Code&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RunWith(SpringRunner.class)
@WebFluxTest
public class HelloControllerTest {
  @Autowired 
  private WebTestClient webTestClient; // (1)

  @Test
  public void hello() {
    webTestClient.get()
        .uri(&quot;/hello/{name}&quot;, &quot;Spring&quot;) // (2)
        .exchange()
        .expectStatus()
        .isOk()  // (3)
        .expectBody(String.class)
        .isEqualTo(&quot;hello Spring&quot;); // (4)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) @WebFluxTest 에서 자동으로 WebTestClient 를 생성해서 주입이 가능하게 해주는데 이때는 Test Code 실행시 만들어지는 ApplicationContext 를 활용한 WebTestClient.bindToApplicationContext(context) 함수로 WebTestClient 가 생성된다.&lt;br /&gt;(2) 생성된 WebTestClient 를 통해 /hello/Spring 이라는 url 로 API 를 요청해 본다.&lt;br /&gt;(3) 응답이 성공으로 왔는지 검증한다.&lt;br /&gt;(4) 응답값이 기대한 결과대로 왔는지 검증한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-webtestclient-는-테스트-케이스에-따라-아래와-같이-여러가지-방법으로-생성이-가능하다&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#webtestclient-%EB%8A%94-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BC%80%EC%9D%B4%EC%8A%A4%EC%97%90-%EB%94%B0%EB%9D%BC-%EC%95%84%EB%9E%98%EC%99%80-%EA%B0%99%EC%9D%B4-%EC%97%AC%EB%9F%AC%EA%B0%80%EC%A7%80-%EB%B0%A9%EB%B2%95%EC%9C%BC%EB%A1%9C-%EC%83%9D%EC%84%B1%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%98%EB%8B%A4&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;WebTestClient 는 테스트 케이스에 따라 아래와 같이 여러가지 방법으로 생성이 가능하다.&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Binding to a Server : application server url 로 생성. 해당 Application Server 로 요청이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;x86asm&quot;&gt;&lt;code&gt;WebTestClient testClient = WebTestClient
  .bindToServer()
  .baseUrl(&quot;http://localhost:8080&quot;)
  .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Binding to a Router : RouterFunction 을 통해 생성. 테스트 요청은 해당 RouterFunction 에 맵핑된 url 만 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;RouterFunction function = RouterFunctions.route(
  RequestPredicates.GET(&quot;/resource&quot;),
  request -&amp;gt; ServerResponse.ok().build()
);

WebTestClient testClient = WebTestClient
  .bindToRouterFunction(function)
  .build()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Binding to an Application Context : ApplicationContext 를 통해 생성. ApplicationContext 를 생성한 Application 에 대해 요청이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Autowired
private ApplicationContext context;

WebTestClient testClient = WebTestClient.bindToApplicationContext(context)
  .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Binding to a Controller : Controller 를 통해 생성. 해당 Controller 에 맵핑된 url 에 대해 테스트 요청이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Autowired
private Controller controller;

WebTestClient testClient = WebTestClient.bindToController(controller).build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>Spring Framework</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>test</category>
      <category>webflux</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/12</guid>
      <comments>https://yang1s.tistory.com/12#entry12comment</comments>
      <pubDate>Mon, 14 Mar 2022 13:12:53 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - Monitoring Metrics</title>
      <link>https://yang1s.tistory.com/11</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Monitoring Metrics&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Boot Application 의 Metric 을 수집하는 방법중의 하나로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#actuator&quot;&gt;Spring Actuator&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;Spring Actuator 의 dependency 를 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;dependency&amp;gt;
    &amp;lt;groupId&amp;gt;org.springframework.boot&amp;lt;/groupId&amp;gt;
    &amp;lt;artifactId&amp;gt;spring-boot-starter-actuator&amp;lt;/artifactId&amp;gt;
&amp;lt;/dependency&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;management.endpoints.web.exposure.include&lt;span&gt;&amp;nbsp;&lt;/span&gt;에&lt;span&gt;&amp;nbsp;&lt;/span&gt;metrics&lt;span&gt;&amp;nbsp;&lt;/span&gt;값을 추가하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;/actuator/metrics&lt;span&gt;&amp;nbsp;&lt;/span&gt;API 가 활성화 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;management:
  endpoints:
    web:
      exposure:
        include: [&quot;metrics&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP&lt;span&gt;&amp;nbsp;&lt;/span&gt;GET&lt;span&gt;&amp;nbsp;&lt;/span&gt;method 요청으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;/actuator/metrics/{metric name}&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 요청하면 수집된 metric 에 대한 조회가 가능하다.&lt;/li&gt;
&lt;li&gt;tag 별로 조회하고자 하면 HTTP&lt;span&gt;&amp;nbsp;&lt;/span&gt;GET&lt;span&gt;&amp;nbsp;&lt;/span&gt;method 요청으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;/actuator/metrics/{metric name}?tag=key:value&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 요청하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-tcpserver&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#tcpserver&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;TcpServer&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Netty TcpServer 에 대한 metric&lt;/li&gt;
&lt;li&gt;NettyServerCustomizer 를 통해 tcpServer.metrics(boolean); 함수를 호출하여 TcpServer 에 대한 metric 을 활성화해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nametype설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.tcp.server.data.received&lt;/td&gt;
&lt;td&gt;DistributionSummary&lt;/td&gt;
&lt;td&gt;TcpServer 에서 수신받은 data byte 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.tcp.server.data.sent&lt;/td&gt;
&lt;td&gt;DistributionSummary&lt;/td&gt;
&lt;td&gt;TcpServer 에서 전송한 data byte 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.tcp.server.errors&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;TcpServer Error 발생 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.tcp.server.tls.handshake.time&lt;/td&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;TLS handshake 에 소요된 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-httpclient&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#httpclient&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;HttpClient&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reactor Netty HttpClient 에 대한 metric&lt;/li&gt;
&lt;li&gt;Reactor Netty HttpClient 생성시 HttpClient.metrics(boolean, Consumer) 함수를 호출하여 HttpClient 에 대한 metric 을 활성화해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nametype설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.http.client.data.received&lt;/td&gt;
&lt;td&gt;DistributionSummary&lt;/td&gt;
&lt;td&gt;TcpServer 에서 수신받은 data byte 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.http.client.data.sent&lt;/td&gt;
&lt;td&gt;DistributionSummary&lt;/td&gt;
&lt;td&gt;TcpServer 에서 전송한 data byte 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.http.client.errors&lt;/td&gt;
&lt;td&gt;Counter&lt;/td&gt;
&lt;td&gt;TcpServer Error 발생 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.http.client.tls.handshake.time&lt;/td&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;TLS handshake 에 소요된 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.http.client.connect.time&lt;/td&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;TcpServer 에서 수신받은 data byte 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.http.client.address.resolver&lt;/td&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;TcpServer 에서 전송한 data byte 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.http.client.data.received.time&lt;/td&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;TcpServer Error 발생 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.http.client.data.sent.time&lt;/td&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;TLS handshake 에 소요된 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.http.client.response.time&lt;/td&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;TLS handshake 에 소요된 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-connectionprovider&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#connectionprovider&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;ConnectionProvider&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Reactor Netty HttpClient 에서 Connection 관리를 담당하는 ConnectionProvider 에 대한 metric&lt;/li&gt;
&lt;li&gt;Reactor Netty ConnectionProvider 를 Builder 를 통해 생성시 ConnectionProvider.Builder.metrics(boolean) 함수를 호출하여 ConnectionProvider 에 대한 metric 을 활성화해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nametype설명&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.connection.provider.total.connections&lt;/td&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;TcpServer 에서 수신받은 data byte 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.connection.provider.active.connections&lt;/td&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;TcpServer 에서 전송한 data byte 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.connection.provid er.idle.connections&lt;/td&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;TcpServer Error 발생 횟수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reactor.netty.connection.provider.pending.connections&lt;/td&gt;
&lt;td&gt;Gauge&lt;/td&gt;
&lt;td&gt;TLS handshake 에 소요된 시간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>Spring Framework</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>webflux</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/11</guid>
      <comments>https://yang1s.tistory.com/11#entry11comment</comments>
      <pubDate>Mon, 14 Mar 2022 13:10:53 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - Exception Handle</title>
      <link>https://yang1s.tistory.com/10</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Exception Handle&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-functional-level&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#functional-level&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Functional Level&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Handler 에서 함수 수행시&lt;span&gt;&amp;nbsp;&lt;/span&gt;onErrorReturn,&lt;span&gt;&amp;nbsp;&lt;/span&gt;onErrorResume&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 정의하여 에러 핸들링 처리를 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onErrorReturn&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public Mono&amp;lt;ServerResponse&amp;gt; handleRequest(ServerRequest request) {
    return sayHello(request)
      .onErrorReturn(&quot;Request Fail! But Return Success Status!&quot;)
      .flatMap(s -&amp;gt; ServerResponse.ok()
      .contentType(MediaType.TEXT_PLAIN)
      .bodyValue(s));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onErrorResume&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public Mono&amp;lt;ServerResponse&amp;gt; handleRequest(ServerRequest request) {
    return sayHello(request)
      .flatMap(s -&amp;gt; ServerResponse.ok()
      .contentType(MediaType.TEXT_PLAIN)
      .bodyValue(s))
      .onErrorResume(e -&amp;gt; sayHelloFallback()
         .flatMap(s -&amp;gt; ServerResponse.ok()
               .contentType(MediaType.TEXT_PLAIN)
               .bodyValue(s)
         )
      );
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-controller-level&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#controller-level&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Controller Level&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MVC 와 동일하게 @ExceptionHandler 어노테이션을 통해 error handling 이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Controller public class SimpleController { 
    // ... 

    @ExceptionHandler 
    public ResponseEntity&amp;lt;String&amp;gt; handle(IOException ex) { 
         // ... 
    } 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ Spring WebFlux 에서 @ExceptionHandler 를 사용할 경우 request body나 @ModelAttribute에 관련된 매개변수와 반환 값은 사용 불가능하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-global-level&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#global-level&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Global Level&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebExceptionHandler&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 정의하여 Bean 으로 등록하면 모든 요청에 대한 Exception Handling 이 가능하다.&lt;/li&gt;
&lt;li&gt;Spring WebFlux 에서는 아래의&lt;span&gt;&amp;nbsp;&lt;/span&gt;WebExceptionHandler&lt;span&gt;&amp;nbsp;&lt;/span&gt;구현체를 제공한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ResponseStatusExceptionHandler : ResponseStatusException 유형에 대한 예외 처리&lt;/li&gt;
&lt;li&gt;WebFluxResponseStatusExceptionHandler : @ResponseStatus 가 선언된 경우에 대한 예외처리. ResponseStatusExceptionHandler의 확장&lt;/li&gt;
&lt;li&gt;DefaultErrorWebExceptionHandler : Exception Handle 에 대한 기본적인 기능 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;직접&lt;span&gt;&amp;nbsp;&lt;/span&gt;WebExceptionHandler&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 구현하고 싶다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;AbstractErrorWebExceptionHandler&lt;span&gt;&amp;nbsp;&lt;/span&gt;나&lt;span&gt;&amp;nbsp;&lt;/span&gt;DefaultErrorWebExceptionHandler&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 상속받아 커스터마이징 하여 Bean 으로 등록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Component
@Order(-2)
public class GlobalErrorWebExceptionHandler extends 
    AbstractErrorWebExceptionHandler {

    // constructors

    @Override
    protected RouterFunction&amp;lt;ServerResponse&amp;gt; getRoutingFunction(
      ErrorAttributes errorAttributes) {

        return RouterFunctions.route(
          RequestPredicates.all(), this::renderErrorResponse);
    }

    private Mono&amp;lt;ServerResponse&amp;gt; renderErrorResponse(
       ServerRequest request) {

       Map&amp;lt;String, Object&amp;gt; errorPropertiesMap = getErrorAttributes(request, 
         ErrorAttributeOptions.defaults());

       return ServerResponse.status(HttpStatus.BAD_REQUEST)
         .contentType(MediaType.APPLICATION_JSON)
         .body(BodyInserters.fromValue(errorPropertiesMap));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ Spring WebFlux 에서는 DefaultErrorWebExceptionHandler 가 @Order(-1) 로 등록이 되기 때문에 DefaultErrorWebExceptionHandler 보다 먼저 error handling 을 하려면 @Order(-2) 를 정의해야한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에러 응답에 대한 정의만 필요하다면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;ErrorAttributes&lt;span&gt;&amp;nbsp;&lt;/span&gt;혹은&lt;span&gt;&amp;nbsp;&lt;/span&gt;DefaultErrorAttributes&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 상속받은 구현체를 Bean 으로 정의하는 방법도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Component
public class GlobalErrorAttributes extends DefaultErrorAttributes {
    
    @Override
    public Map&amp;lt;String, Object&amp;gt; getErrorAttributes(ServerRequest request, 
      ErrorAttributeOptions options) {
        Map&amp;lt;String, Object&amp;gt; map = super.getErrorAttributes(
          request, options);
        map.put(&quot;status&quot;, HttpStatus.BAD_REQUEST);
        map.put(&quot;message&quot;, &quot;username is required&quot;);
        return map;
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring Framework</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>webflux</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/10</guid>
      <comments>https://yang1s.tistory.com/10#entry10comment</comments>
      <pubDate>Mon, 14 Mar 2022 13:09:26 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - Server Configuration</title>
      <link>https://yang1s.tistory.com/9</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Server Configuration&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-enablewebflux&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#enablewebflux&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;@EnableWebFlux&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@EnableWebFlux 어노테이션을 정의할 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;DelegatingWebFluxConfiguration&lt;span&gt;&amp;nbsp;&lt;/span&gt;이 동작하는데 해당 Configuration 이 수행하는 기능은 다음과 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebFlux Application 운영을 위한 default spring configuration 을 정의해준다.&lt;/li&gt;
&lt;li&gt;WebFluxConfigurer&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 정의한 configuration 이 있는지 탐지 및 customizing 된 내용 적용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableWebFlux
public class WebConfig {

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약 @EnableWebFlux 를 사용하지 않고 직접 핸들링 하고 싶다면, DelegatingWebFluxConfiguration 을 직접 정의하는 방법도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;scala&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-webfluxconfigurer&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#webfluxconfigurer&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;WebFluxConfigurer&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebFlux Server side 에 대해 사용자가 정의할 수 있는 옵션을 제공한다.&lt;/li&gt;
&lt;li&gt;WebFluxConfigurer 가 동작하기 위해서는 @EnableWebFlux 가 어딘가 선언되어 있어야 한다.&lt;/li&gt;
&lt;li&gt;WebFluxConfigurer 에서 지원하는 옵션은 다음과 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;addFormatters(FormatterRegistry registry) : Conversion, formatting 관련 옵션&lt;/li&gt;
&lt;li&gt;Validator getValidator() : 전역 Validator 정의&lt;/li&gt;
&lt;li&gt;configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) : 요청 media type 에 대한 resolver 정의. default 는 accept header 를 체크하지만 필요에 따라 parameter base 의 resolver 정의도 가능하다.&lt;/li&gt;
&lt;li&gt;configureHttpMessageCodecs(ServerCodecConfigurer configurer) : request, response read/write 에 관련된 codec 설정 정의. 요청 파라미터 body 사이즈를 256kb 이상 늘리고 싶다면 WebClient 에서 언급되었던 MaxInMemorySize 설정과 동일하게 설정해준다.&lt;/li&gt;
&lt;li&gt;configureViewResolvers(ViewResolverRegistry registry) : view resolver 를 정의. 상세 설정 방법은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-config-view-resolvers&quot;&gt;spring doc&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고.&lt;/li&gt;
&lt;li&gt;addResourceHandlers(ResourceHandlerRegistry registry) : static resource 에 대한 handler 정의. 각 케이스별 상세 설정은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-config-static-resources&quot;&gt;spring doc&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고.&lt;/li&gt;
&lt;li&gt;configurePathMatch(PathMatchConfigurer configurer) : path matching 에 대한 사용자 옵션을 정의. 옵션에 대한 자세한 사항은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/5.3.7/javadoc-api/org/springframework/web/reactive/config/PathMatchConfigurer.html&quot;&gt;PathMatchConfigurer&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;참고&lt;/li&gt;
&lt;li&gt;addCorsMappings(CorsRegistry registry) : Cors 에 대한 전역 설정 정의.&lt;/li&gt;
&lt;li&gt;getWebSocketService() : WebSocket Handling 을 위한 WebSocketService 를 정의.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-cors&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#cors&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;CORS&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebFluxConfigurer 의&lt;span&gt;&amp;nbsp;&lt;/span&gt;addCorsMappings&lt;span&gt;&amp;nbsp;&lt;/span&gt;을 설정하거나&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-cors-webfilter&quot;&gt;CorsFilter&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 bean 으로 정의하면 전역 CORS 지원이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebFluxConfigurer 설정&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping(&quot;/api/**&quot;)
            .allowedOrigins(&quot;https://domain2.com&quot;)
            .allowedMethods(&quot;PUT&quot;, &quot;DELETE&quot;)
            .allowedHeaders(&quot;header1&quot;, &quot;header2&quot;, &quot;header3&quot;)
            .exposedHeaders(&quot;header1&quot;, &quot;header2&quot;)
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CorsFilter 설정&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Bean
CorsWebFilter corsFilter() {

    CorsConfiguration config = new CorsConfiguration();

    // Possibly...
    // config.applyPermitDefaultValues()

    config.setAllowCredentials(true);
    config.addAllowedOrigin(&quot;https://domain1.com&quot;);
    config.addAllowedHeader(&quot;*&quot;);
    config.addAllowedMethod(&quot;*&quot;);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration(&quot;/**&quot;, config);

    return new CorsWebFilter(source);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS 에 대한 custom 한 동작이 필요하다면 CorsConfiguration 를 구현하면 된다. 예를들어 domain 의 allow origin check 를 ant pattern 이나 정규표현식 방식으로 check 하고 싶다면 CorsConfiguration 의 checkOrigin 을 구현하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-netty-tcpserver&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#netty-tcpserver&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Netty TcpServer&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/web/embedded/netty/NettyServerCustomizer.html&quot;&gt;NettyServerCustomizer&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 구현하여 Netty TcpServer 에 대한 옵션 정의가 가능하다.&lt;/li&gt;
&lt;li&gt;TcpServer 에 설정가능한 옵션은 다음과 같다.(옵션 종류는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://netty.io/4.1/api/io/netty/channel/ChannelOption.html&quot;&gt;ChannelOption&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 필드로 정의되어있다.)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SO_BACKLOG&lt;/li&gt;
&lt;li&gt;SO_KEEPALIVE&lt;/li&gt;
&lt;li&gt;SO_REUSEADDR&lt;/li&gt;
&lt;li&gt;SO_LINGER&lt;/li&gt;
&lt;li&gt;SO_RCVBUF&lt;/li&gt;
&lt;li&gt;SO_SNDBUF&lt;/li&gt;
&lt;li&gt;SO_TIMEOUT&lt;/li&gt;
&lt;li&gt;TCP_NODELAY&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TcpServer 에 대한 metric 을 활성화 하고 싶다면, TcpServer.metrics(boolean) 함수를 호출하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;public class EventLoopNettyCustomizer implements NettyServerCustomizer {

    @Override
    public HttpServer apply(HttpServer httpServer) {
        EventLoopGroup parentGroup = new NioEventLoopGroup();
        EventLoopGroup childGroup = new NioEventLoopGroup();
        return httpServer.tcpConfiguration(tcpServer -&amp;gt; { // 필요한 설정 변경 수행 });
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ Reactor Netty 의 selector, worker thread 수도 NettyServerCustomizer 를 통해 설정할 수 있는것 같지만, 실제로 해보면 적용이 안된다.&lt;br /&gt;그 이유는 selector, worker thread 는 http server 를 생성할 때 정의해야 하는데 실제 http server 를 생성하는 NettyReactiveWebServerFactory 에서 http server 를 생성한 뒤에 customizer 를 호출하도록 되어있기 때문에 여기서 설정을 해봤자 변경되지 않는다.&lt;br /&gt;selector, worker thread 를 변경하기 위해선 NettyReactiveWebServerFactory 를 통해 ReactorResourceFactory 를 정의해 loopResources 를 셋팅해주거나&amp;nbsp;&lt;a href=&quot;https://github.com/reactor/reactor-netty/blob/main/reactor-netty-core/src/main/java/reactor/netty/ReactorNetty.java&quot;&gt;ReactorNetty&lt;/a&gt;&amp;nbsp;에 정의된 환경변수를 application 실행시 정의해주어야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NettyServerCustomizer 구현체를 Bean 으로 등록하거나 NettyReactiveWebServerFactory 을 Bean 으로 등록하고 이때 NettyServerCustomizer 를 등록하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Configuration
@EnableWebFlux
public class WebConfig {

    @Bean
    public EventLoopNettyCustomizer eventLoopNettyCustomizer() {
        return new EventLoopNettyCustomizer();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@EnableWebFlux
public class WebConfig {

    @Bean
    public NettyReactiveWebServerFactory nettyReactiveWebServerFactory() {
        NettyReactiveWebServerFactory webServerFactory = new NettyReactiveWebServerFactory();
        webServerFactory.addServerCustomizers(new EventLoopNettyCustomizer());
        return webServerFactory;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SSL, Http2 와 관련된 설정을 하고자 한다면, WebServerFactoryCustomizer 를 직접 정의하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Component
public class NettyWebServerFactorySslCustomizer 
  implements WebServerFactoryCustomizer&amp;lt;NettyReactiveWebServerFactory&amp;gt; {

    @Override
    public void customize(NettyReactiveWebServerFactory serverFactory) {
        Ssl ssl = new Ssl();
        ssl.setEnabled(true);
        ssl.setKeyStore(&quot;classpath:sample.jks&quot;);
        ssl.setKeyAlias(&quot;alias&quot;);
        ssl.setKeyPassword(&quot;password&quot;);
        ssl.setKeyStorePassword(&quot;secret&quot;);
        Http2 http2 = new Http2();
        http2.setEnabled(false);
        serverFactory.addServerCustomizers(new SslServerCustomizer(ssl, http2, null));
        serverFactory.setPort(8443);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>Spring Framework</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>webflux</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/9</guid>
      <comments>https://yang1s.tistory.com/9#entry9comment</comments>
      <pubDate>Mon, 14 Mar 2022 13:06:13 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - WebClient</title>
      <link>https://yang1s.tistory.com/8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;WebClient&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-생성&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#%EC%83%9D%EC%84%B1&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;생성&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-create&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#create&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;create&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebClient 는 간단하게 static 메서드로 생성이 가능하다.&lt;/li&gt;
&lt;li&gt;static method 로 생성할 경우 디폴트로 Reactor Netty HttpClient 를 사용하므로 reactor-netty 라이브러리에 대한 의존성이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;WebClient webClient1 = WebClient.create();
WebClient webClient2 = WebClient.create(&quot;http://naver.com&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-builder&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#builder&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;builder&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;몇가지 옵션을 설정하려면 builder 를 사용하여 생성하는것을 권장한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;uriBuilderFactory: base URL 과 관련하여 UriBuilderFactory 를 커스텀하게 구현했을 경우.&lt;/li&gt;
&lt;li&gt;defaultUriVariables: 모든 요청에 핸들링 가능한 URI variable 설정.&lt;/li&gt;
&lt;li&gt;defaultHeader: 모든 요청에 사용할 header 설정.&lt;/li&gt;
&lt;li&gt;defaultCookie: 모든 요청에 사용할 cookie 설정.&lt;/li&gt;
&lt;li&gt;defaultRequest: 모든 요청의 request 를 커스터마이징 하는 설정.&lt;/li&gt;
&lt;li&gt;filter: 모든 요청의 filter 설정.&lt;/li&gt;
&lt;li&gt;exchangeStrategies: HTTP message reader/writer 의 커스터마이징 설정.&lt;/li&gt;
&lt;li&gt;clientConnector: HTTP client library 설정.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;WebClient client = WebClient.builder()
      .exchangeStrategies(builder -&amp;gt; {
              return builder.codecs(codecConfigurer -&amp;gt; {
                  //...
              });
      })
      .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebClient 는 한번 build 를 수행하면 상태를 변경할 수 없다. 기존 WebClient 에 설정을 변경하고 싶다면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;mutate()&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 통해 복제를 하여 설정을 추가한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;WebClient client1 = WebClient.builder()
      .filter(filterA).filter(filterB).build();

WebClient client2 = client1.mutate()
      .filter(filterC).filter(filterD).build();
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-중요한-configuration&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#%EC%A4%91%EC%9A%94%ED%95%9C-configuration&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;중요한 Configuration&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-connection&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#connection&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Connection&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebClient 에서 사용할 Connection 관련 설정은 ConnectionProvider.Builder 를 통해 설정하여 ConnectionProvider 를 생성한 뒤 Reactor Netty HttpClient 생성시 주입하면 된다.&lt;/li&gt;
&lt;li&gt;ConnectionProvider.Builder 를 통해 설정할 수 있는 Connection 관련 설정은 다음과 같다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;evictInBackground : background thread 가 connection 종료 체크를 얼마나 자주 할건지에 대한 설정.&lt;/li&gt;
&lt;li&gt;maxConnections : max connection 수. WebClient 에서 connection 수 이상의 요청을 하게될 경우 queue 에 들어가 대기하게 된다.&lt;/li&gt;
&lt;li&gt;pendingAcquireMaxCount : connection 이 없을 때 queue 에 요청을 담을 최대 갯수. -1 로 설정할 경우 제한 없음.&lt;/li&gt;
&lt;li&gt;pendingAcquireTimeout : connection 이 없어 queue 에 담아놓을 시간(단위: ms).&lt;/li&gt;
&lt;li&gt;maxIdleTime : connection 의 최대 idle time(단위: ms). -1 일 경우 제한 없음.&lt;/li&gt;
&lt;li&gt;maxLifeTime : connection 의 최대 life time(단위: ms). -1 일 경우 제한 없음.&lt;/li&gt;
&lt;li&gt;getLeasingStrategy : Connection Acquire operation 수행시 connection 대여 전략. fifo, lifo 가 있다.(default: fifo)&lt;/li&gt;
&lt;li&gt;metrics : Connection 관련 metric 활성화 여부. 활성화를 할 경우 /actuator/metrics 를 통해 metric 확인 가능.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;ConnectionProvider.Builder connectionProviderBuilder = ConnectionProvider.builder(name);
// 설정 수행
...
HttpClient httpClient = HttpClient.create(connectionProviderBuilder.build());

WebClient webClient = WebClient.builder()
      .clientConnector(new ReactorClientHttpConnector(httpClient))
      .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-maxinmemorysize&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#maxinmemorysize&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;MaxInMemorySize&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 웹플럭스는 어플리케이션 메모리 이슈를 방지하기 위해 코덱의 메모리 버퍼 사이즈를 제한한다. 디폴트는 256KB로 설정돼 있는데, 버퍼가 부족하면 다음과 같은 에러가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;codec 설정을 통해 MaxInMemorySize 를 수정할 수 있다.&lt;/li&gt;
&lt;li&gt;-1 로 설정할 경우 무제한으로 설정된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;xl&quot;&gt;&lt;code&gt;WebClient webClient = WebClient.builder()
      .exchangeStrategies(builder -&amp;gt;
          builder.codecs(codecs -&amp;gt;
              codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
          )
      )
      .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-timeout&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#timeout&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Timeout&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connect, Read/Write Timeout 은 Reactor Netty HttpClient 생성을 통해 다음과 같이 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;haxe&quot;&gt;&lt;code&gt;import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
      .tcpConfiguration(client -&amp;gt;
              client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
                    .doOnConnected(conn -&amp;gt; conn.addHandlerLast(new ReadTimeoutHandler(10000, TimeUnit.MILLISECONDS))
					       .addHandlerLast(new WriteTimeoutHandler(10000, TimeUnit.MILLISECONDS))));

WebClient webClient = WebClient.builder()
      .clientConnector(new ReactorClientHttpConnector(httpClient))
      .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-secure&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#secure&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Secure&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타겟 서버의 ssl 인증서 체크를 무시하고자 한다면 아래와 같이 설정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;HttpClient httpClient = HttpClient.create()
	.secure(
		sslContextSpec -&amp;gt; {
			try {
				sslContextSpec.sslContext(
					SslContextBuilder.forClient()
						.trustManager(InsecureTrustManagerFactory.INSTANCE)
						.build()
				);
			} catch (SSLException e) {
				throw new ServiceException(&quot;Insecure sslContext create fail.&quot;, e);
			}
		}
	);

WebClient webClient = WebClient.builder()
      .clientConnector(new ReactorClientHttpConnector(httpClient))
      .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-metric&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#metric&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Metric&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebClient 의 요청 url 별로 metric 을 얻고 싶다면 Reactor Netty HttpClient 생성시&lt;span&gt;&amp;nbsp;&lt;/span&gt;metrics&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 통해 metric 활성화 및 url 별 tag 설정을 해준다.&lt;/li&gt;
&lt;li&gt;tag 설정이 되었다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;/actuator/metrics/{http.client.metric.key}?tag=key:value&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로 요청하여 tag 에 맞는 metric 조회가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;HttpClient httpClient = HttpClient.create()
	.metrics(properties.isMetricsEnabled(), uri -&amp;gt; {
		// uri 별 tag name 정의
		return &quot;my-api&quot;;
	})

WebClient webClient = WebClient.builder()
      .clientConnector(new ReactorClientHttpConnector(httpClient))
      .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-request-body&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#request-body&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Request Body&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Mono,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Flux&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 통해 비동기적으로 request body serialize &amp;amp; write 가 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;Mono&amp;lt;Person&amp;gt; personMono = ... ;

Mono&amp;lt;Void&amp;gt; result = client.post()
      .uri(&quot;/persons/{id}&quot;, id)
      .contentType(MediaType.APPLICATION_JSON)
      .body(personMono, Person.class)
      .retrieve()
      .bodyToMono(Void.class);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기적으로 하지 않아도 된다면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;bodyValue&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;Person person = ... ;

Mono&amp;lt;Void&amp;gt; result = client.post()
      .uri(&quot;/persons/{id}&quot;, id)
      .contentType(MediaType.APPLICATION_JSON)
      .bodyValue(person)
      .retrieve()
      .bodyToMono(Void.class);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;form data 의 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MultiValueMap&amp;lt;String, String&amp;gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;형태로&lt;span&gt;&amp;nbsp;&lt;/span&gt;bodyValue&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 호출하거나,&lt;span&gt;&amp;nbsp;&lt;/span&gt;BodyInserters&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// bodyValue 사용
MultiValueMap&amp;lt;String, String&amp;gt; formData = ... ;

Mono&amp;lt;Void&amp;gt; result = client.post()
      .uri(&quot;/path&quot;, id)
      .bodyValue(formData)
      .retrieve()
      .bodyToMono(Void.class);

// BodyInserters 사용
Mono&amp;lt;Void&amp;gt; result = client.post()
      .uri(&quot;/path&quot;, id)
      .body(BodyInserters.fromFormData(&quot;k1&quot;, &quot;v1&quot;).with(&quot;k2&quot;, &quot;v2&quot;))
      .retrieve()
      .bodyToMono(Void.class);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ Form Data 를 보낼 경우 FormHttpMessageWriter 에서 자동으로 Content-Type 을 application/x-www-form-urlencoded 으로 정의하기 때문에 contentType 함수를 사용해서 정의하지 않아도 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-request--response-handle&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#request--response-handle&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Request &amp;amp; Response Handle&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-retrive&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#retrive&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;retrive()&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;retrive() 는 response body 를 받아 디코딩하는 간단한 함수이다.&lt;/li&gt;
&lt;li&gt;4xx, 5xx 응답 코드를 받으면 디폴트는&lt;span&gt;&amp;nbsp;&lt;/span&gt;WebClientResponseException&lt;span&gt;&amp;nbsp;&lt;/span&gt;또는 각 HTTP 상태에 해당하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;WebClientResponseException.BadRequest,&lt;span&gt;&amp;nbsp;&lt;/span&gt;WebClientResponseException.NotFound&lt;span&gt;&amp;nbsp;&lt;/span&gt;등의 하위 exception을 던진다.&lt;/li&gt;
&lt;li&gt;http status 별 핸들링을 다르게 하고 싶다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;onStatus&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드로 커스텀할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Mono&amp;lt;Person&amp;gt; result = client.get()
      .uri(&quot;/persons/{id}&quot;, id).accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .onStatus(HttpStatus::is4xxClientError, response -&amp;gt; ...)
      .onStatus(HttpStatus::is5xxServerError, response -&amp;gt; ...)
      .bodyToMono(Person.class);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ response body 가 있을 경우 onStatus 에서 소비하지 않으면 onStatus 호출 이후 response body 가 리소스 반환을 위해 비워지기 때문에 response body 를 볼 수 없게 된다. onStatus 를 사용하게 될 경우 response body 도 반드시 소비해야한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;a id=&quot;user-content-exchange&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#exchange&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;exchange()&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직접 ClientResponse 를 핸들링 해야할 필요가 있다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;exchange()&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 통해 요청한다.&lt;/li&gt;
&lt;li&gt;exchange()&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수는&lt;span&gt;&amp;nbsp;&lt;/span&gt;retrive()&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수와는 다르게 4XX, 5XX http status 에 대해 자동으로 에러 응답을 주지 않는다. 사용자가 직접 http status 를 확인하고 핸들링 하는 로직을 구현해야한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 model 객체로 변환시&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Mono&amp;lt;Person&amp;gt; result = client.get()
      .uri(&quot;/persons/{id}&quot;, id).accept(MediaType.APPLICATION_JSON)
      .exchange()
      .flatMap(response -&amp;gt; response.bodyToMono(Person.class));&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ResponseEntity 로 변환시&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Mono&amp;lt;ResponseEntity&amp;lt;Person&amp;gt;&amp;gt; result = client.get()
      .uri(&quot;/persons/{id}&quot;, id).accept(MediaType.APPLICATION_JSON)
      .exchange()
      .flatMap(response -&amp;gt; response.toEntity(Person.class));&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️&lt;span&gt;&amp;nbsp;&lt;/span&gt;retrieve()와는 다르게&lt;span&gt;&amp;nbsp;&lt;/span&gt;exchange()는 모든 시나리오에서(성공, 오류, 예기치 못한 데이터 등) 어플리케이션이 직접 response body를 컨슘해야 한다. 그렇지 않으면 메모리 릭이 발생할 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;ClientResponse&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/reactive/function/client/ClientResponse.html&quot;&gt;javadoc&lt;/a&gt;을 참고하면 body를 컨슘할 수 있는 모든 옵션이 나와 있다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Spring Framework</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>webflux</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/8</guid>
      <comments>https://yang1s.tistory.com/8#entry8comment</comments>
      <pubDate>Mon, 14 Mar 2022 12:55:31 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - Filter</title>
      <link>https://yang1s.tistory.com/7</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Filter&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Webflux 에서는 MVC 의 Interceptor 와 비슷한 역할을 하는 HandlerFilterFunction 과 Servlet Filter 와 비슷한 역할을 하는 WebFilter Interface 를 제공하고 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-handlerfilterfunction&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#handlerfilterfunction&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;HandlerFilterFunction&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;핸들러 펑션에 필터를 적용할 땐 RouterFunction Builder 를 사용하면 되는데, 다음과 같은 함수가 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;before&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Handler 함수 수행 전에 동작하는 함수를 등록하며 인자로 Function&amp;lt;ServerRequest, ServerRequest&amp;gt; 를 받는다.&lt;/li&gt;
&lt;li&gt;after&lt;span&gt;&amp;nbsp;&lt;/span&gt;: Handler 함수가 수행 완료되면 동작하는 함수를 등록하며 인자로 BiFunction&amp;lt;ServerRequest, ServerResponse, ServerResponse&amp;gt; 를 받는다.&lt;/li&gt;
&lt;li&gt;filter&lt;span&gt;&amp;nbsp;&lt;/span&gt;: 인자로 HandlerFilterFunction&amp;lt;ServerRequest, ServerResponse&amp;gt; 를 받으며, 필터를 어떻게 수행하고 다음 handler function 을 어떻게 호출할지의 일련의 동작을 모두 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;필터는 빌더의 모든 라우터 펑션에 적용된다. 이 말은 필터를 감싸져 있는 라우터에서 정의하면, 상위 레벨에는 적용되지 않는다는 뜻이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;RouterFunction&amp;lt;ServerResponse&amp;gt; route = RouterFunctions.route()
    .path(&quot;/person&quot;, b1 -&amp;gt; b1
        .nest(RequestPredicates.accept(APPLICATION_JSON), b2 -&amp;gt; b2
            .GET(&quot;/{id}&quot;, handler::getPerson)
            .GET(&quot;&quot;, handler::listPeople)
            .before(request -&amp;gt; ServerRequest.from(request) // (1) 
                .header(&quot;X-RequestHeader&quot;, &quot;Value&quot;)
                .build()))
        .POST(&quot;/person&quot;, handler::createPerson))
    .after((request, response) -&amp;gt; logResponse(response)) // (2) 
    .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1)&lt;span&gt;&amp;nbsp;&lt;/span&gt;X-RequestHeader&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 추가하는 filter 는 두&lt;span&gt;&amp;nbsp;&lt;/span&gt;GET&lt;span&gt;&amp;nbsp;&lt;/span&gt;Router 에만 적용되며 Router 실행 전에 수행된다.&lt;br /&gt;(2) response 를 logging 하는 filter 는 모든 Router 에 적용되며 Router 실행 후 수행된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;filter&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;HandlerFilterFunction을 인자로 받는다. 이 인터페이스는&lt;span&gt;&amp;nbsp;&lt;/span&gt;ServerRequest,&lt;span&gt;&amp;nbsp;&lt;/span&gt;HandlerFunction을 받아&lt;span&gt;&amp;nbsp;&lt;/span&gt;ServerResponse를 리턴하는 함수다. 두번째 인자로 받는 HandlerFunction 파라미터는 체인에 있는 다음 HandlerFunction 이다. 보통 이 HandlerFunction 은 라우팅할 핸들러지만, HandlerFilterFunction 이 여러 개가 등록되어있다면 HandlerFilterFunction 일 수도 있다.&lt;/li&gt;
&lt;li&gt;path를 보고 요청을 허가할지 말지 결정하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;SecurityManager가 있다고 가정하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;filter&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 통해 보안 필터를 라우터에 적용해 보면 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SecurityManager securityManager = ...

RouterFunction&amp;lt;ServerResponse&amp;gt; route = RouterFunctions.route()
    .path(&quot;/person&quot;, b1 -&amp;gt; b1
        .nest(RequestPredicates.accept(APPLICATION_JSON), b2 -&amp;gt; b2
            .GET(&quot;/{id}&quot;, handler::getPerson)
            .GET(&quot;&quot;, handler::listPeople))
        .POST(&quot;/person&quot;, handler::createPerson))
    .filter((request, next) -&amp;gt; {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
    .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 예제를 보면&lt;span&gt;&amp;nbsp;&lt;/span&gt;next.handle(ServerRequest)&lt;span&gt;&amp;nbsp;&lt;/span&gt;호출은 선택이라는 점을 알 수 있다. 여기선 접근을 허가할 때만 실행했다.&lt;br /&gt;⚠️ filter 를 구현하다보면 Request 객체에 attribute 나 header 등의 값을 주입하거나 변경해야할 때가 있는데, 그럴 때 ServerRequest.Builder 나 혹은 Builder 를 사용하는 API(from) 로 ServerRequest 를 새로 생성하여 변경할 경우 다음 HandlerFunction 에서 request body 를 가져올 수 없다. 반드시 주입받은 ServerRequest 를 다음 HandlerFunction 에 넘겨주도록 구현해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-webfilter&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#webfilter&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;WebFilter&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/server/WebFilter.html&quot;&gt;WebFilter&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 구현하여 Bean 으로 등록(혹은&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Component&lt;span&gt;&amp;nbsp;&lt;/span&gt;Annotation 을 등록)하면 WebFilter 를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;WebFilter 는 여러개가 등록 가능하며 순서를 맞추고자 한다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Order&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 사용한다.&lt;/li&gt;
&lt;li&gt;아래는 Spring Webflux 에서 HttpHandler, ExceptionHandler, WebFlilter, DispatcherHandler 간의 동작 과정이다. FilteringWebHandler 부터 DefaultWebFilterChain 까지가 WebFilter 의 동작 부분이라 볼 수 있으며 Bean(혹은 @Component Annotation)으로 등록한 WebFliter 는 DefaultWebFilterChain 안에서 동작하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;webfilter.png&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIzVot/btrvQf1zrGT/twkZEhEYIlPdYrP9GSgWmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIzVot/btrvQf1zrGT/twkZEhEYIlPdYrP9GSgWmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIzVot/btrvQf1zrGT/twkZEhEYIlPdYrP9GSgWmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIzVot%2FbtrvQf1zrGT%2FtwkZEhEYIlPdYrP9GSgWmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;675&quot; height=&quot;280&quot; data-filename=&quot;webfilter.png&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;⚠️ HandlerFilterFunction 은 위 동작에 포함되지 않으며 DispatcherHandler 수행시 동작하게 된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증, ACL, Access Logging 과 같이 요청시 우선적으로 실행해야 하거나 HandlerFilterFunction 에 영향을 받지 않도록 하려면 WebFilter 를 구현하여 사용하는게 좋다.&lt;/li&gt;
&lt;li&gt;Spring Webflux 에서 제공하는 대표적인 WebFilter 로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-cors-webfilter&quot;&gt;CorsFilter&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring Framework</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>webflux</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/7</guid>
      <comments>https://yang1s.tistory.com/7#entry7comment</comments>
      <pubDate>Mon, 14 Mar 2022 12:53:03 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - HandlerFunction</title>
      <link>https://yang1s.tistory.com/6</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;HandlerFunction&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ServerRequest&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 받아&lt;span&gt;&amp;nbsp;&lt;/span&gt;ServerResponse&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 리턴하는, MVC 의&lt;span&gt;&amp;nbsp;&lt;/span&gt;Controller&lt;span&gt;&amp;nbsp;&lt;/span&gt;와 유사한 로직을 구현하는 함수이다.&lt;/li&gt;
&lt;li&gt;ServerRequest와&lt;span&gt;&amp;nbsp;&lt;/span&gt;ServerResponse는 자바 8 방식으로 HTTP 요청과 응답에 접근할 수 있는 불변(immutable) 인터페이스다.&lt;/li&gt;
&lt;li&gt;요청, 응답 body 모두&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.reactive-streams.org/&quot;&gt;리액티브 스트림&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;back pressure로 처리한다. request body는 리액터&lt;span&gt;&amp;nbsp;&lt;/span&gt;Flux나&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mono로 표현한다.&lt;/li&gt;
&lt;li&gt;response body는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Flux와&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mono를 포함한 어떤 리액티브 스트림&lt;span&gt;&amp;nbsp;&lt;/span&gt;Publisher든 상관없다. 자세한 정보는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-reactive-libraries&quot;&gt;Reactive Libraries&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 참고.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-serverrequest&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#serverrequest&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;ServerRequest&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ServerRequest로 HTTP 메소드, URI, 헤더, 쿼리 파라미터에 접근할 수 있으며, body를 추출할 수 있는 메소드를 제공한다.&lt;/li&gt;
&lt;li&gt;query 파라미터 조회.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Optional&amp;lt;String&amp;gt; param = serverRequest.queryParam(&quot;param&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;path variable 파라미터 조회.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;String id = serverRequest.pathVariable(&quot;id&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RequestBody 를&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mono&amp;lt;String&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;으로 추출.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;Mono&amp;lt;String&amp;gt; string = request.bodyToMono(String.class);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RequestBody 를&lt;span&gt;&amp;nbsp;&lt;/span&gt;Flux&amp;lt;Person&amp;gt;으로 추출한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;Person&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체는 JSON이나 XML 같은 직렬화된 데이터로 디코딩한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;coffeescript&quot;&gt;&lt;code&gt;Flux&amp;lt;Person&amp;gt; people = request.bodyToFlux(Person.class);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bodyToMono, bodyToFlux 는 함수형 인터페이스&lt;span&gt;&amp;nbsp;&lt;/span&gt;BodyExtractor를 받는&lt;span&gt;&amp;nbsp;&lt;/span&gt;ServerRequest.body(BodyExtractor)&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드의 축약 버전이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;BodyExtractors&lt;span&gt;&amp;nbsp;&lt;/span&gt;유틸리티 클래스에 있는 인터페이스를 활용하여 위의 예제를 다음과 같이 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Mono&amp;lt;String&amp;gt; string = request.body(BodyExtractors.toMono(String.class));
Flux&amp;lt;Person&amp;gt; people = request.body(BodyExtractors.toFlux(Person.class));&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이밖에도 다음과 같은 목적에 맞게 request body 를 추출할 수 있는 API 가 제공된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;form data 추출&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Mono&amp;lt;MultiValueMap&amp;lt;String, String&amp;gt; map = request.formData();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multipart data 추출&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Mono&amp;lt;MultiValueMap&amp;lt;String, Part&amp;gt; map = request.multipartData();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;multiparts 를 streaming 으로 하나씩 추출할 때&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Flux&amp;lt;Part&amp;gt; parts = request.body(BodyExtractors.toParts());&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-serverresponse&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#serverresponse&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;ServerResponse&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 응답은&lt;span&gt;&amp;nbsp;&lt;/span&gt;ServerResponse로 접근할 수 있으며, 이 인터페이스는 불변이기 때문에(immutable)&lt;span&gt;&amp;nbsp;&lt;/span&gt;build&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드로 생성한다. 빌더로 헤더를 추가하거나, 상태 코드, body를 설정할 수 있다.&lt;/li&gt;
&lt;li&gt;JSON 컨텐츠로 200 (OK) 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Mono&amp;lt;Person&amp;gt; person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;body 없이 Location 헤더로만 201 (CREATED) 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;URI location = ...
ServerResponse.created(location).build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;hint 파라미터를 넘기면 사용하는 코덱에 따라 body 직렬화/역직렬화 방식을 커스텀할 수 있다. 아래 예는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.baeldung.com/jackson-json-view-annotation&quot;&gt;Jackson JSON view&lt;/a&gt;를 지정하는 예제이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-handler-classes&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#handler-classes&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Handler Classes&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/reactive/function/server/HandlerFunction.html&quot;&gt;HandlerFunction&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;은&lt;span&gt;&amp;nbsp;&lt;/span&gt;FunctionalInterface&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 정의되어있기 때문에 lamda 형태로 정의가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;HandlerFunction&amp;lt;ServerResponse&amp;gt; helloWorld = request -&amp;gt; ServerResponse.ok().bodyValue(&quot;Hello World&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다만 HandlerFunction 로직이 복잡하거나 많을 경우 lamda 로 정의하는데 부담이 생기기 때문에 일반 class 에 함수를 정의하고 해당 함수를 handler function 으로 묶을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;router&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction&amp;lt;?&amp;gt; personRouter(PersonRepository repository) {
        PersonHandler handler = new PersonHandler(repository);

        return RouterFunction&amp;lt;ServerResponse&amp;gt; route = route()
           .GET(&quot;/person/{id}&quot;, accept(APPLICATION_JSON), handler::getPerson)
           .GET(&quot;/person&quot;, accept(APPLICATION_JSON), handler::listPeople)
           .POST(&quot;/person&quot;, handler::createPerson)
           .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handler class&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

public class PersonHandler {

    private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }

    public Mono&amp;lt;ServerResponse&amp;gt; listPeople(ServerRequest request) { // (1)
        Flux&amp;lt;Person&amp;gt; people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people, Person.class);
    }

    public Mono&amp;lt;ServerResponse&amp;gt; createPerson(ServerRequest request) { // (2)
        Mono&amp;lt;Person&amp;gt; person = request.bodyToMono(Person.class);
        return ok().build(repository.savePerson(person));
    }

    public Mono&amp;lt;ServerResponse&amp;gt; getPerson(ServerRequest request) { // (3)
        int personId = Integer.valueOf(request.pathVariable(&quot;id&quot;));
        return repository.getPerson(personId)
            .flatMap(person -&amp;gt; ok().contentType(APPLICATION_JSON).bodyValue(person))
            .switchIfEmpty(ServerResponse.notFound().build());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1)&lt;span&gt;&amp;nbsp;&lt;/span&gt;listPeople은 레포지토리에 있는 모든&lt;span&gt;&amp;nbsp;&lt;/span&gt;Person&lt;span&gt;&amp;nbsp;&lt;/span&gt;객체를 JSON으로 반환하는 핸들러 펑션이다.&lt;br /&gt;(2)&lt;span&gt;&amp;nbsp;&lt;/span&gt;createPerson은 request body에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Person을 저장하는 핸들러 펑션이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;PersonRepository.savePerson(Person)은&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mono&amp;lt;Void&amp;gt;를 리턴한다는 점에 주의해라. 비어 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mono는 요청 데이터를 읽어 저장하고 나면 완료됐다는 신호를 보낸다. 따라서 이 신호를 받았을 때(즉,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Person이 저장됐을 때) 응답을 보내기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;build(Publisher&amp;lt;Void&amp;gt;)를 사용한다.&lt;br /&gt;(3)&lt;span&gt;&amp;nbsp;&lt;/span&gt;getPerson은 path variable에 있는&lt;span&gt;&amp;nbsp;&lt;/span&gt;id로 식별한 person 객체 하나를 리턴하는 핸들러 펑션이다. 레포지토리에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Person을 찾으면 JSON 응답을 만든다. 찾지 못했다면&lt;span&gt;&amp;nbsp;&lt;/span&gt;switchIfEmpty(Mono&amp;lt;T&amp;gt;)를 실행해 404 Not Found로 응답한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-validation&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#validation&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Validation&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수형 엔드포인트는 스프링&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation&quot;&gt;validation facilities&lt;/a&gt;를 사용해서 request body를 검증할 수 있다.&lt;/li&gt;
&lt;li&gt;다음 예제는 커스텀 스프링 Validator 구현체로&lt;span&gt;&amp;nbsp;&lt;/span&gt;person을 검증한다&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;public class PersonHandler {

    private final Validator validator = new PersonValidator(); // (1) 

    // ...

    public Mono&amp;lt;ServerResponse&amp;gt; createPerson(ServerRequest request) {
        Mono&amp;lt;Person&amp;gt; person = request.bodyToMono(Person.class).doOnNext(this::validate); // (2) 
        return ok().build(repository.savePerson(person));
    }

    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, &quot;person&quot;);
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString()); // (3) 
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1)&lt;span&gt;&amp;nbsp;&lt;/span&gt;Validator&lt;span&gt;&amp;nbsp;&lt;/span&gt;인스턴스를 생성한다.&lt;br /&gt;(2) 검증 로직을 실행한다.&lt;br /&gt;(3) 400으로 응답하는 exception을 발생시킨다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>Spring Framework</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>webflux</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/6</guid>
      <comments>https://yang1s.tistory.com/6#entry6comment</comments>
      <pubDate>Mon, 14 Mar 2022 12:50:32 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - Request Mapping</title>
      <link>https://yang1s.tistory.com/5</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Request Mapping&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Webflux 에선 아래와 같이 2가지 programming model 이 있고 그에 맞게 Request Mapping 을 해줄 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-annotated-controllers&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#annotated-controllers&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Annotated Controllers&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 MVC와 동일하며&lt;span&gt;&amp;nbsp;&lt;/span&gt;spring-web&lt;span&gt;&amp;nbsp;&lt;/span&gt;모듈에 있는 같은 어노테이션을 사용한다. 스프링 MVC와 웹플럭스 컨트롤러 모두 리액티브(Reactor, RxJava) 리턴 타입을 지원하기 때문에 이 둘을 구분하기 어렵다. 한 가지 눈에 띄는 차이는 웹플럭스에선&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RequestBody로 리액티브 인자를 받을 수 있다.&lt;/li&gt;
&lt;li&gt;스프링 웹플럭스는 어노테이션 기반 프로그래밍 모델을 지원하기 때문에,&lt;span&gt;&amp;nbsp;&lt;/span&gt;@Controller,&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RestController&lt;span&gt;&amp;nbsp;&lt;/span&gt;컴포넌트로 요청을 매핑하고, 입력을 받고, exception을 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;컨트롤러는 메소드를 여러 가지로 활용할 수 있어서 클래스를 상속하거나 인터페이스를 구현하지 않아도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RestController
public class HelloController {

    @GetMapping(&quot;/hello&quot;)
    public Mono&amp;lt;String&amp;gt; handle(@RequestBody Mono&amp;lt;String&amp;gt; request) {
        return request.map(name -&amp;gt; &quot;Hello Webflux. My name is &quot; + name);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-functional-endpoints&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#functional-endpoints&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Functional Endpoints&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 웹플럭스는 경량화된 함수형 프로그래밍 모델을 지원한다. WebFlux.fn이라고도 하는 이 모델은, 함수로 요청을 라우팅하고 핸들링하기 때문에 불변성(Immutablility)을 보장한다.&lt;/li&gt;
&lt;li&gt;WebFlux.fn에선&lt;span&gt;&amp;nbsp;&lt;/span&gt;HandlerFunction이 HTTP 요청을 처리한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;HandlerFunction은&lt;span&gt;&amp;nbsp;&lt;/span&gt;ServerRequest를 받아 비동기&lt;span&gt;&amp;nbsp;&lt;/span&gt;ServerResponse(i.e.&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mono&amp;lt;ServerResponse&amp;gt;)를 리턴하는 함수다. 요청, 응답 객체 모두 불변(immutable)이기 때문에 JDK 8 방식으로 HTTP 요청, 응답에 접근할 수 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;HandlerFunction&lt;span&gt;&amp;nbsp;&lt;/span&gt;역할은 어노테이션 프로그래밍 모델로 치면&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RequestMapping&lt;span&gt;&amp;nbsp;&lt;/span&gt;메소드가 하던 일과 동일하다.&lt;/li&gt;
&lt;li&gt;요청은&lt;span&gt;&amp;nbsp;&lt;/span&gt;RouterFunction이 핸들러 펑션에 라우팅한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;RouterFunction은&lt;span&gt;&amp;nbsp;&lt;/span&gt;ServerRequest를 받아 비동기&lt;span&gt;&amp;nbsp;&lt;/span&gt;HandlerFunction(i.e.&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mono&amp;lt;HandlerFunction&amp;gt;)을 리턴하는 함수다. 매칭되는 라우터 펑션이 있으면 핸들러 펑션을 리턴하고 그 외는 비어있는 Mono를 리턴한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;RouterFunction이 하는 일은&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RequestMapping&lt;span&gt;&amp;nbsp;&lt;/span&gt;어노테이션과 동일하지만, 라우터 펑션은 데이터뿐 아니라 행동까지 제공한다는 점이 다르다.&lt;/li&gt;
&lt;li&gt;라우터를 만들 때는 아래 예제처럼&lt;span&gt;&amp;nbsp;&lt;/span&gt;RouterFunctions.route()가 제공하는 빌더를 사용할 수 있다.&lt;/li&gt;
&lt;li&gt;RouterFunction을 실행하는 방법 중 하나는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-httphandler&quot;&gt;HttpHandler&lt;/a&gt;로 변환해 내장된 서버 어댑터에 등록하는 것인데, 라우터를 bean 으로 등록하면 Spring 에서 이러한 처리를 자동으로 해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction&amp;lt;?&amp;gt; personRouter(PersonHandler handler) {
        return RouterFunction&amp;lt;ServerResponse&amp;gt; route = RouterFunctions.route()
           .GET(&quot;/person/{id}&quot;, RequestPredicates.accept(APPLICATION_JSON), handler::getPerson)
           .GET(&quot;/person&quot;, RequestPredicates.accept(APPLICATION_JSON), handler::listPeople)
           .POST(&quot;/person&quot;, handler::createPerson)
           .build();
    }

    @Bean
    public RouterFunction&amp;lt;?&amp;gt; routerFunctionB() {
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-routerfunction&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#routerfunction&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;RouterFunction&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라우터 펑션은 요청을 그에 맞는&lt;span&gt;&amp;nbsp;&lt;/span&gt;HandlerFunction으로 라우팅한다.&lt;/li&gt;
&lt;li&gt;라우터 펑션을 직접 만들기보단, 보통&lt;span&gt;&amp;nbsp;&lt;/span&gt;RouterFunctions&lt;span&gt;&amp;nbsp;&lt;/span&gt;유틸리티 클래스를 사용한다.&lt;/li&gt;
&lt;li&gt;RouterFunctions.route()가 리턴하는 빌더를 사용하거나,&lt;span&gt;&amp;nbsp;&lt;/span&gt;RouterFunctions.route(RequestPredicate, HandlerFunction)으로 직접 라우터를 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-predicates&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#predicates&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Predicates&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RequestPredicate를 직접 만들어도 되지만, 요청 path, HTTP 메소드, 컨텐츠 타입 등 자주 사용하는 구현체는&lt;span&gt;&amp;nbsp;&lt;/span&gt;RequestPredicates&lt;span&gt;&amp;nbsp;&lt;/span&gt;유틸리티 클래스에 준비돼 있다.&lt;/li&gt;
&lt;li&gt;다음은 유틸리티 클래스로&lt;span&gt;&amp;nbsp;&lt;/span&gt;Accept&lt;span&gt;&amp;nbsp;&lt;/span&gt;헤더 조건을 추가하는 예제다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;RouterFunction&amp;lt;ServerResponse&amp;gt; route = RouterFunctions.route()
    .GET(&quot;/hello-world&quot;, RequestPredicates.accept(MediaType.TEXT_PLAIN),
        request -&amp;gt; ServerResponse.ok().bodyValue(&quot;Hello World&quot;)).build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;and,&lt;span&gt;&amp;nbsp;&lt;/span&gt;or&lt;span&gt;&amp;nbsp;&lt;/span&gt;조건을 걸 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;requestPredicate.and(RequestPredicate other) : requestPredicate 와 other predicate 가 둘다 만족해야한다.&lt;/li&gt;
&lt;li&gt;requestPredicate.or(RequestPredicate other) : requestPredicate 와 other predicate 중 1개만 만족해도 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;RequestPredicates&lt;span&gt;&amp;nbsp;&lt;/span&gt;가 제공하는 함수는 위의&lt;span&gt;&amp;nbsp;&lt;/span&gt;and&lt;span&gt;&amp;nbsp;&lt;/span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;or&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 조합해서 만든 것이 많다. 예를 들어&lt;span&gt;&amp;nbsp;&lt;/span&gt;RequestPredicates.GET(String)&lt;span&gt;&amp;nbsp;&lt;/span&gt;API 는 내부적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;RequestPredicates.method(Method)&lt;span&gt;&amp;nbsp;&lt;/span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;RequestPredicates.path(String)&lt;span&gt;&amp;nbsp;&lt;/span&gt;을&lt;span&gt;&amp;nbsp;&lt;/span&gt;and&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 연결시켰다고 생각하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-routes&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#routes&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Routes&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;라우터 펑션은 정해진 순서대로 실행한다. 첫 번째 조건과 일치하지 않으면 두 번째를 실행하는 식이다. 따라서 구체적인 조건을 앞에 선언해야 한다. 어노테이션 프로그래밍 모델(Spring MVC)에선 자동으로 가장 구체적인 컨트롤러 메소드를 실행하지만, 함수형 모델에선 그렇지 않다 점에 주의해야한다.&lt;/li&gt;
&lt;li&gt;build()를 호출하면 빌더에 정의한 모든 라우터 펑션을&lt;span&gt;&amp;nbsp;&lt;/span&gt;RouterFunction&lt;span&gt;&amp;nbsp;&lt;/span&gt;한 개로 합친다. 다음 방법으로도 여러 라우터 펑션을 조합할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RouterFunctions.route()&lt;span&gt;&amp;nbsp;&lt;/span&gt;빌더의&lt;span&gt;&amp;nbsp;&lt;/span&gt;add(RouterFunction)&lt;/li&gt;
&lt;li&gt;RouterFunction.and(RouterFunction)&lt;/li&gt;
&lt;li&gt;RouterFunction.andRoute(RequestPredicate, HandlerFunction)&amp;thinsp;&amp;mdash;&amp;thinsp;&lt;span&gt;&amp;nbsp;&lt;/span&gt;RouterFunctions.route()를&lt;span&gt;&amp;nbsp;&lt;/span&gt;RouterFunction.and()로 감싸고 있는 축약 버전&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction&amp;lt;?&amp;gt; personRouter(PersonHandler handler) {
        return RouterFunction&amp;lt;ServerResponse&amp;gt; route = RouterFunctions.route()
           .GET(&quot;/person/{id}&quot;, RequestPredicates.accept(APPLICATION_JSON), handler::getPerson) // (1)
           .GET(&quot;/person&quot;, RequestPredicates.accept(APPLICATION_JSON), handler::listPeople) // (2)
           .POST(&quot;/person&quot;, handler::createPerson) // (3)
           .add(otherRoute) // (4)
           .build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;(1)&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Accept&lt;span&gt;&amp;nbsp;&lt;/span&gt;헤더가 JSON인&lt;span&gt;&amp;nbsp;&lt;/span&gt;GET /person/{id}는&lt;span&gt;&amp;nbsp;&lt;/span&gt;PersonHandler.getPerson으로 라우팅한다.&lt;br /&gt;&lt;span&gt;(2)&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Accept&lt;span&gt;&amp;nbsp;&lt;/span&gt;헤더가 JSON인&lt;span&gt;&amp;nbsp;&lt;/span&gt;GET /person은&lt;span&gt;&amp;nbsp;&lt;/span&gt;PersonHandler.listPeople로 라우팅한다.&lt;br /&gt;&lt;span&gt;(3)&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;POST /person은 다른 조건 없이&lt;span&gt;&amp;nbsp;&lt;/span&gt;PersonHandler.createPerson로 라우팅한다.&lt;br /&gt;&lt;span&gt;(4)&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;마지막으로 나머지 요청을 처리할&lt;span&gt;&amp;nbsp;&lt;/span&gt;otherRoute&lt;span&gt;&amp;nbsp;&lt;/span&gt;펑션을 route에 추가한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;user-content-nested-routes&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#nested-routes&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Nested Routes&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;path가 같으면 대부분 같은 조건을 사용하므로, 라우터 펑션을 그룹핑하는 경우가 많다. 위의 Routes 예제는 라우터 펑션 세 개가&lt;span&gt;&amp;nbsp;&lt;/span&gt;/person을 path 조건으로 사용했다.&lt;/li&gt;
&lt;li&gt;어노테이션을 사용했다면 클래스 레벨에&lt;span&gt;&amp;nbsp;&lt;/span&gt;@RequestMapping을 선언해 중복 코드를 줄였을 거다.&lt;/li&gt;
&lt;li&gt;WebFlux.fn 에선 공통 조건을 가지고있는 RouterFunction 을 공유하기 위해 Builder 를 받는 Consumer 인터페이스를 활용하도록 하고있다.&lt;/li&gt;
&lt;li&gt;위의 Routes 에 나온 예제를 아래와 같이 변경 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;RouterFunction&amp;lt;ServerResponse&amp;gt; route = RouterFunctions.route()
    .path(&quot;/person&quot;, builder -&amp;gt; builder // (1)
        .GET(&quot;/{id}&quot;, RequestPredicates.accept(APPLICATION_JSON), handler::getPerson)
        .GET(&quot;&quot;, RequestPredicates.accept(APPLICATION_JSON), handler::listPeople)
        .POST(&quot;/person&quot;, handler::createPerson))
    .add(otherRoute)
    .build();
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(1) path 가 /person 인 조건의 RouterFunction 에 Builder 를 받는 Consumer 를 구현하여 분기처리를 하고있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;builder 에서 제공하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;nest&lt;span&gt;&amp;nbsp;&lt;/span&gt;함수를 활용하면 하위 RouterFunction 에 다른 공통 조건도 추가가 가능하다. 위 예에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;/person/{id}' 와&lt;span&gt;&amp;nbsp;&lt;/span&gt;/person` 의 accept 조건이 같으므로 다음과 같이 변경이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;RouterFunction&amp;lt;ServerResponse&amp;gt; route = RouterFunctions.route()
    .path(&quot;/person&quot;, b1 -&amp;gt; b1
        .nest(RequestPredicates.accept(APPLICATION_JSON), b2 -&amp;gt; b2
            .GET(&quot;/{id}&quot;, handler::getPerson)
            .GET(&quot;&quot;, handler::listPeople))
        .POST(&quot;/person&quot;, handler::createPerson))
    .add(otherRoute)
    .build();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <category>Spring Framework</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>webflux</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/5</guid>
      <comments>https://yang1s.tistory.com/5#entry5comment</comments>
      <pubDate>Mon, 14 Mar 2022 12:48:09 +0900</pubDate>
    </item>
    <item>
      <title>Spring Webflux - Overview</title>
      <link>https://yang1s.tistory.com/4</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring Webflux 란?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring 5.0 버전부터 추가된 리액티브 스택 웹 프레임워크다.&lt;/li&gt;
&lt;li&gt;완벽하게 non-blocking 으로 동작하며,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://www.reactive-streams.org/&quot;&gt;Reactive Streams&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;back pressure를 지원하고, Netty, Undertow, 서블릿 3.1+ 컨테이너 서버에서 실행된다.&lt;/li&gt;
&lt;li&gt;기존 Spring MVC 와 같이 Spring Framework 에 공존하고 MVC 를 사용하며 Webflux 개발을 하는것도 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-spring-webflux-는-왜-만들어졌나&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#spring-webflux-%EB%8A%94-%EC%99%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%A1%8C%EB%82%98&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Spring Webflux 는 왜 만들어졌나?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적은 쓰레드로 동시 처리를 제어하고 적은 하드웨어 리소스로 확장하기 위한 논블로킹 웹 스택의 필요성이 대두됨.&lt;/li&gt;
&lt;li&gt;이전에도 서블릿 3.1 은 논블로킹 I/O를 위한 API를 제공했지만, 서블릿으로 논블로킹을 구현하려면 다른 동기 처리나(Filter,&lt;span&gt;&amp;nbsp;&lt;/span&gt;Servlet) 블로킹 방식(getParameter,&lt;span&gt;&amp;nbsp;&lt;/span&gt;getPart) 을 쓰는 API를 피해야 하지만 그렇게 하기가 어렵다. 이런 점 때문에 어떤 논블로킹과도 잘 동작하는 새 공통 API를 만들게 되었다. 이미 비동기 논블로킹 환경에서 자리를 잡은 서버(e.g. Netty) 때문에라도 새 API가 필요했다.&lt;/li&gt;
&lt;li&gt;또 다른 이유로는 함수형 프로그래밍이 있다. 자바 5의 어노테이션 등장으로 선택의 폭이 넓어진 것처럼(어노테이션을 선언한 REST 컨트롤러나 유닛 테스트 등), 자바 8에서 추가된 람다 표현식덕분에 자바에서도 함수형 API를 작성할 수 있게 됐다. 이 기능은 논블로킹 어플리케이션을 만들 때도 요긴하게 쓰이며, 이제는 continuation-style API(CompletableFuture&lt;span&gt;&amp;nbsp;&lt;/span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;http://reactivex.io/&quot;&gt;ReactiveX&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;로 대중화된)로 비동기 로직을 선언적으로 작성할 수 있다. 프로그래밍 모델 관점에서 보면, 웹플럭스에서 어노테이션을 선언한 컨트롤러와 더불어 함수형 웹 엔드포인트를 사용할 수 있는 건 자바 8 덕분이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-리액티브-는-무슨-뜻인가&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#%EB%A6%AC%EC%95%A1%ED%8B%B0%EB%B8%8C-%EB%8A%94-%EB%AC%B4%EC%8A%A8-%EB%9C%BB%EC%9D%B8%EA%B0%80&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;&amp;ldquo;리액티브&amp;rdquo; 는 무슨 뜻인가?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;ldquo;리액티브&amp;rdquo;라는 용어는 변화에 반응하는 것을 중심에 두고 만든 프로그래밍 모델을 의미한다 (I/O 이벤트에 반응하는 네트워크 컴포넌트, 마우스 이벤트에 반응하는 UI 컨트롤러 등). 논블로킹은 작업을 기다리기보단 완료되거나 데이터를 사용할 수 있게 되면 반응하므로, 이 말대로면 논블로킹도 리액티브다.&lt;/li&gt;
&lt;li&gt;스프링은 &amp;ldquo;리액티브&amp;rdquo;와 관련한 중요한 메커니즘이 하나 더 있는데, 논블로킹 back pressure다. 동기식 명령형(imperative) 코드에서 블로킹 호출은 호출자를 강제로 기다리게 하는 일종의 back pressure다. 논블로킹 코드에선, 프로듀셔 속도가 컨슈머 속도를 압도하지 않도록 이벤트 속도를 제어한다.&lt;/li&gt;
&lt;li&gt;리액티브 스트림은 back pressure를 통한 비동기 컴포넌트 간의 상호작용을 정의한&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/reactive-streams/reactive-streams-jvm/blob/master/README.md#specification&quot;&gt;간단한 스펙&lt;/a&gt;이다(자바 9에서도 채택했다). 예를 들어 데이터 레포지토리(&lt;a href=&quot;https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html&quot;&gt;Publisher&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;역할)가 데이터를 만들고, HTTP 서버(&lt;a href=&quot;https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html&quot;&gt;Subscriber&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;역할)로 이 데이터로 요청을 처리할 수 있다. 리액티브 스트림을 쓰는 주목적은 subscriber가 publisher의 데이터 생산 속도를 제어하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-reactive-api&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#reactive-api&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Reactive API&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리액티브 스트림은 컴포넌트 상호 작용에서 중요한 역할을 한다. 하지만 이건 라이브러리와 기반 구조에 사용되는 컴포넌트엔 유용해도, 어플리케이션 API에서 다루기엔 너무 저수준이다. 어플리케이션은 비동기 로직을 만들기 위한 풍부한 고수준 함수형 API가 필요하다 (자바 8 스트림 API와 비슷하지만 컬렉션만을 위한 게 아니다). 이게 바로 리액티브 라이브러리가 하는 일이다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/reactor/reactor&quot;&gt;Reactor&lt;/a&gt;는 스프링 웹플럭스가 선택한 리액티브 라이브러리다. 리액터는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html&quot;&gt;Mono&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;와&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html&quot;&gt;Flux&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;API 타입을 제공한다.&lt;/li&gt;
&lt;li&gt;ReactiveX&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;http://reactivex.io/documentation/operators.html&quot;&gt;vocabulary of operators&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에 정리된 풍부한 연산자를 사용해 데이터 시퀀스를 0~1개는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mono, 0~N개는&lt;span&gt;&amp;nbsp;&lt;/span&gt;Flux로 표현할 수 있다. 리액터는 리액티브 스트림 라이브러리이기 때문에 모든 연산자는 논블로킹 back pressure를 지원한다. 리액터는 특히 서버 사이드 자바에 초점을 두고 스프링과 긴밀히 협력해서 개발됐다.&lt;/li&gt;
&lt;li&gt;웹플럭스는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/reactor/reactor&quot;&gt;Reactor&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;를 핵심 라이브러리로 사용하지만, 다른 리액티브 라이브러리를 써도 리액티브 스트림으로 상호작용할 수 있다. 웹플럭스 API의 일반적인 룰은, 순수한&lt;span&gt;&amp;nbsp;&lt;/span&gt;Publisher를 입력으로 받아 내부적으로 리액터 타입으로 맞추고, 이걸 사용해서&lt;span&gt;&amp;nbsp;&lt;/span&gt;Flux나&lt;span&gt;&amp;nbsp;&lt;/span&gt;Mono를 반환한다. 따라서 어떤&lt;span&gt;&amp;nbsp;&lt;/span&gt;Publisher든 입력으로 전달하고 연산할 수 있지만, 다른 리액티브 라이브러리를 사용하려면 출력 형식을 맞춰줘야 한다. 웹플럭스는 가능만 하다면 (e.g. 어노테이션을 선언한 컨트롤러) 투명한 방식으로 RxJava나 다른 리액티브 라이브러리에 맞게 바꿔준다. 자세한 내용은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://godekdls.github.io/Reactive%20Spring/reactivelibraries/&quot;&gt;Reactive Libraries&lt;/a&gt;를 참고.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;❗ 리액티브 API와는 별개로 웹플럭스는 코틀린의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://docs.spring.io/spring/docs/current/spring-framework-reference/languages.html#coroutines&quot;&gt;코루틴&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;API와도 사용할 수 있는데, 이를 사용하면 좀 더 명령적(imperative)인 프로그래밍이 가능하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;a id=&quot;user-content-concurrency-model&quot; href=&quot;https://oss.navercorp.com/GITF/issues/wiki/Chapter-1.-Spring-Webflux-%ED%95%B5%EC%8B%AC-%EA%B0%80%EC%9D%B4%EB%93%9C#concurrency-model&quot; aria-hidden=&quot;true&quot;&gt;&lt;/a&gt;Concurrency Model&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;currencymodel.png&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;357&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c43OsQ/btrvUADCYzG/BKMHJWiIuQ5Poa75xphaQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c43OsQ/btrvUADCYzG/BKMHJWiIuQ5Poa75xphaQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c43OsQ/btrvUADCYzG/BKMHJWiIuQ5Poa75xphaQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc43OsQ%2FbtrvUADCYzG%2FBKMHJWiIuQ5Poa75xphaQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;971&quot; height=&quot;357&quot; data-filename=&quot;currencymodel.png&quot; data-origin-width=&quot;971&quot; data-origin-height=&quot;357&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스프링 MVC와 스프링 웹플럭스 둘 다 annotated controller를 사용할 수 있다는 점은 동일해도, 동시성 모델과 블로킹/쓰레드 기본 전략이 다르다.&lt;/li&gt;
&lt;li&gt;스프링 MVC는 (그리고 일반적인 서블릿 어플리케이션이라면) 어플리케이션이 처리 중인 쓰레드가 잠시 중단될 수 있다(예를 들어 외부 서비스를 호출하면). 그렇기 때문에 서블릿 컨테이너는 이 블로킹을 대비에 큰 쓰레드 풀로 요청을 처리한다.&lt;/li&gt;
&lt;li&gt;스프링 웹플럭스는 (그리고 일반적인 논블로킹 서버라면) 실행 중인 쓰레드가 중단되지 않는다는 전제가 있다. 따라서 논블로킹 서버는 작은 쓰레드 풀(이벤트 루프 워커)을 고정해놓고 요청을 처리한다.&lt;/li&gt;
&lt;li&gt;Thread 는 Event 가 들어온 순서대로 로직들을 처리하게 되며 요청 들어온 Event 가 각 Thread 에 골고루 분산되어 효율적으로 비지니스 로직이 실행되게 된다. 필요한 task 작업이 동시에 수행되는건 아니지만 Worker Thread 에 여유가 있다면 이에 준하는 성능으로 비지니스 로직이 동작하게 된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring Framework</category>
      <category>Reactive</category>
      <category>spring</category>
      <category>webflux</category>
      <author>흥부가귀막혀</author>
      <guid isPermaLink="true">https://yang1s.tistory.com/4</guid>
      <comments>https://yang1s.tistory.com/4#entry4comment</comments>
      <pubDate>Mon, 14 Mar 2022 12:36:08 +0900</pubDate>
    </item>
  </channel>
</rss>