<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>ycra.dev | Blog</title><description>Platform &amp; AI Engineer — Kubernetes · 데이터 플랫폼 · LLM 시스템을 만들며 정리하는 기록</description><link>https://ycra-dev.github.io/</link><language>ko</language><item><title>StarRocks Operator는 왜 클러스터가 많아지면 멈추는가</title><link>https://ycra-dev.github.io/blog/starrocks-operator-scaling/</link><guid isPermaLink="true">https://ycra-dev.github.io/blog/starrocks-operator-scaling/</guid><description>CR이 몇 개 안 될 땐 잘 돌던 StarRocks Operator가, 클러스터가 수십 개로 늘자 reconcile이 밀리고 멈추기 시작했다. 원인은 하드코딩된 동시성·resync·rate limit 값이었다. PR #769에서 이 값들을 Helm으로 끄집어낸 과정을 정리한다.

</description><pubDate>Mon, 29 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h2 id=&quot;문제-cr이-늘어나자-operator가-멈췄다&quot;&gt;문제: CR이 늘어나자 Operator가 멈췄다&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;StarRocks Kubernetes Operator는 &lt;code dir=&quot;auto&quot;&gt;StarRocksCluster&lt;/code&gt;라는 CR(Custom Resource) 하나하나를 watch하면서, 그 안에 정의된 &lt;strong&gt;FE(Frontend)·CN(Compute Node)·BE(Backend)&lt;/strong&gt; 컴포넌트를 실제 클러스터 상태와 맞추는(reconcile) 역할을 한다.&lt;/p&gt;
&lt;p&gt;문제는 이 Operator가 &lt;strong&gt;단일 클러스터, 혹은 소수의 클러스터를 관리하는 것을 전제로 설계&lt;/strong&gt;되었다는 점이다. CR이 한두 개일 때는 아무 문제가 없다. 그런데 하나의 클러스터 위에 &lt;strong&gt;수십 개의 StarRocks 클러스터(CR)&lt;/strong&gt; 가 떠 있는 멀티테넌트 환경으로 가면 이야기가 달라진다.&lt;/p&gt;
&lt;p&gt;CR이 특정 개수를 넘어서면서 Operator가 다음과 같은 증상을 보이기 시작했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;새 CR을 만들어도 한참 동안 reconcile이 시작되지 않는다.&lt;/li&gt;
&lt;li&gt;스펙을 바꿔도 반영이 지연된다.&lt;/li&gt;
&lt;li&gt;결국 Operator가 사실상 &lt;strong&gt;hang&lt;/strong&gt;에 빠진 것처럼 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;원인을 추적해보니, 코드에 &lt;strong&gt;하드코딩된 controller-runtime 설정값&lt;/strong&gt; 세 가지가 맞물려 있었다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;왜-멈추는가-세-가지-병목&quot;&gt;왜 멈추는가: 세 가지 병목&lt;/h2&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;1-reconcile-동시성이-1로-고정돼-있다&quot;&gt;1. reconcile 동시성이 1로 고정돼 있다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;controller-runtime의 컨트롤러는 내부적으로 workqueue에서 CR의 key를 꺼내 reconcile하는 &lt;strong&gt;워커 goroutine&lt;/strong&gt;을 돌린다. 이 워커 수가 바로 &lt;code dir=&quot;auto&quot;&gt;MaxConcurrentReconciles&lt;/code&gt;이고, &lt;strong&gt;기본값은 1&lt;/strong&gt;이다.&lt;/p&gt;
&lt;p&gt;즉, 아무리 CR이 많아도 &lt;strong&gt;한 번에 단 하나의 CR만&lt;/strong&gt; reconcile된다. 나머지는 모두 큐에서 순서를 기다린다.&lt;/p&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;DESCRIPTION&quot; height=&quot;355px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 350 355&quot; width=&quot;350px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;!--cluster Reconcile Worker--&gt;&lt;g data-qualified-name=&quot;Reconcile Worker&quot; data-source-line=&quot;5&quot; id=&quot;ent0002&quot;&gt;&lt;rect fill=&quot;none&quot; height=&quot;87&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;154&quot; x=&quot;34.5&quot; y=&quot;112&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; font-weight=&quot;700&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;138.0996&quot; x=&quot;42.4502&quot; y=&quot;126.9951&quot;&gt;Reconcile Worker&lt;/text&gt;&lt;/g&gt;&lt;!--entity W--&gt;&lt;g data-qualified-name=&quot;Reconcile Worker.W&quot; data-source-line=&quot;6&quot; id=&quot;ent0003&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;111.7246&quot; x=&quot;58.5&quot; y=&quot;147&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;91.7246&quot; x=&quot;68.5&quot; y=&quot;169.9951&quot;&gt;Worker (1개뿐)&lt;/text&gt;&lt;/g&gt;&lt;!--entity Q--&gt;&lt;g data-qualified-name=&quot;Q&quot; data-source-line=&quot;4&quot; id=&quot;ent0001&quot;&gt;&lt;path d=&quot;M60,6 L168.9502,6 C173.9502,6 173.9502,27.2969 173.9502,27.2969 C173.9502,27.2969 173.9502,48.5938 168.9502,48.5938 L60,48.5938 C55,48.5938 55,27.2969 55,27.2969 C55,27.2969 55,6 60,6&quot; fill=&quot;#F1F1F1&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M168.9502,6 C163.9502,6 163.9502,27.2969 163.9502,27.2969 C163.9502,48.5938 168.9502,48.5938 168.9502,48.5938&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;82.2637&quot; x=&quot;60&quot; y=&quot;23.9951&quot;&gt;WorkQueue&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;98.9502&quot; x=&quot;60&quot; y=&quot;40.292&quot;&gt;(CR 50개의 key)&lt;/text&gt;&lt;/g&gt;&lt;!--entity API--&gt;&lt;g data-qualified-name=&quot;API&quot; data-source-line=&quot;8&quot; id=&quot;ent0004&quot;&gt;&lt;polygon fill=&quot;#F1F1F1&quot; points=&quot;16,294.5,26,284.5,213.1514,284.5,213.1514,320.7969,203.1514,330.7969,16,330.7969,16,294.5&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;203.1514&quot; x2=&quot;213.1514&quot; y1=&quot;294.5&quot; y2=&quot;284.5&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;16&quot; x2=&quot;203.1514&quot; y1=&quot;294.5&quot; y2=&quot;294.5&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;203.1514&quot; x2=&quot;203.1514&quot; y1=&quot;294.5&quot; y2=&quot;330.7969&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;157.1514&quot; x=&quot;31&quot; y=&quot;317.4951&quot;&gt;Kubernetes API Server&lt;/text&gt;&lt;/g&gt;&lt;g data-qualified-name=&quot;GMN7&quot; data-source-line=&quot;13&quot; id=&quot;ent0008&quot;&gt;&lt;path d=&quot;M205.5,137.5 L205.5,161 L170.56,165 L205.5,169 L205.5,192.8984 A0,0 0 0 0 205.5,192.8984 L335.8003,192.8984 A0,0 0 0 0 335.8003,192.8984 L335.8003,147.5 L325.8003,137.5 L205.5,137.5 A0,0 0 0 0 205.5,137.5&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M325.8003,137.5 L325.8003,147.5 L335.8003,147.5 L325.8003,137.5&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;109.3003&quot; x=&quot;211.5&quot; y=&quot;154.5669&quot;&gt;CR 1개 reconcile이&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;87.2168&quot; x=&quot;211.5&quot; y=&quot;169.6997&quot;&gt;느리면 나머지 49개는&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;35.3374&quot; x=&quot;211.5&quot; y=&quot;184.8325&quot;&gt;전부 대기&lt;/text&gt;&lt;/g&gt;&lt;!--link Q to W--&gt;&lt;g data-entity-1=&quot;ent0001&quot; data-entity-2=&quot;ent0003&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;10&quot; id=&quot;lnk5&quot;&gt;&lt;path d=&quot;M114.5,49.08 C114.5,75.73 114.5,116.72 114.5,141.68&quot; fill=&quot;none&quot; id=&quot;Q-to-W&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;114.5,146.68,118.5,137.68,114.5,141.68,110.5,137.68,114.5,146.68&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;116.6509&quot; x=&quot;115.5&quot; y=&quot;92.0669&quot;&gt;한 번에 1개만 dequeue&lt;/text&gt;&lt;/g&gt;&lt;!--link W to API--&gt;&lt;g data-entity-1=&quot;ent0003&quot; data-entity-2=&quot;ent0004&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;11&quot; id=&quot;lnk6&quot;&gt;&lt;path d=&quot;M114.5,183.23 C114.5,208.58 114.5,250.96 114.5,279.02&quot; fill=&quot;none&quot; id=&quot;W-to-API&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;114.5,284.02,118.5,275.02,114.5,279.02,110.5,275.02,114.5,284.02&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;134.6147&quot; x=&quot;115.5&quot; y=&quot;235.5669&quot;&gt;FE/CN/BE 상태 조회 + 갱신&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;107.6626&quot; x=&quot;128.9761&quot; y=&quot;250.6997&quot;&gt;(CR 1개당 여러 번 호출)&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src HT11Im9150Vm-toAFtjIGYoeGnw24eC88lMmboxRxcIBEcFhM4W4bXr2FNWeb52oInPus4B4JdqVP_mExUo6tMRclVURzrwrwBAAvLbbsBnSuOmoaHVr4eL7RM6nGeaImIiG7ATS25w6u-g8onaX5pLQep1CxXMp-dZ2OkaSjjON_a0D-oZIUjo0LKMMh30OEMfpPhl1KTTHpy0rmV_d4KSzn3O2Hlvq8y8aDuHn9wW_h9rIZr51gzWzFa2UUfSQiS8h8Laa77x54ZimaSBoQG3vzQ1wNOJSk0E7XcqJSvMecHJsCydqKN8l0zMyNJO7KA_-yhc3DIpyJzKUHRE5IFiRgZUHBnDDOzcVgzaW7dGdWgMvXNC1VXRq2_pbwqr64wlXD7YMhQvycyYFAUHTNxxFrRY1pMqDjnw3i1hTora3ijDOp7r2cGCj_m80?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;p&gt;reconcile 하나가 가벼우면 워커 1개로도 충분하다. 하지만 StarRocks의 reconcile은 가볍지 않다. CR 하나마다 FE/CN/BE 각각의 StatefulSet·Service·ConfigMap 상태를 확인하고, 필요하면 API 서버에 갱신 요청을 보낸다. CR 하나를 처리하는 데 걸리는 시간 × 큐에 쌓인 CR 수만큼 &lt;strong&gt;선형으로 지연이 누적&lt;/strong&gt;된다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;2-2분마다-전체-cr이-다시-큐에-들어온다-resync&quot;&gt;2. 2분마다 전체 CR이 다시 큐에 들어온다 (resync)&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;controller-runtime 매니저에는 &lt;code dir=&quot;auto&quot;&gt;SyncPeriod&lt;/code&gt;라는 설정이 있고, StarRocks Operator는 이를 &lt;strong&gt;2분&lt;/strong&gt;으로 잡고 있었다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://ycra-dev.github.io/blog/informer-pitfalls/&quot;&gt;이전 글&lt;/a&gt;에서 정리했듯, resync는 API 서버를 다시 조회하는 게 아니다. &lt;strong&gt;로컬 캐시에 있는 모든 객체를 핸들러에 다시 흘려보내&lt;/strong&gt; reconcile을 한 번 더 돌리는 안전장치다. 문제는 이게 &lt;strong&gt;전체 CR을 대상으로&lt;/strong&gt; 한다는 것이다.&lt;/p&gt;
&lt;p&gt;CR이 50개라면, 2분마다 50개의 reconcile 요청이 한꺼번에 큐에 들어온다. 그런데 (1)에서 봤듯 워커는 1개뿐이다. 한 번의 resync 사이클(50개 reconcile)을 2분 안에 못 끝내면, &lt;strong&gt;다음 resync가 또 50개를 밀어 넣는다.&lt;/strong&gt; 큐는 줄지 않고 계속 불어나고, Operator는 영원히 밀린 일을 따라잡지 못한다.&lt;/p&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;SEQUENCE&quot; height=&quot;308px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 398 308&quot; width=&quot;398px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;T&quot; data-source-line=&quot;1&quot; id=&quot;part1-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;SyncPeriod&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;201.9297&quot; width=&quot;8&quot; x=&quot;46.9478&quot; y=&quot;52.5938&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;50&quot; x2=&quot;50&quot; y1=&quot;52.5938&quot; y2=&quot;254.5234&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;Q&quot; data-source-line=&quot;2&quot; id=&quot;part2-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;WorkQueue&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;201.9297&quot; width=&quot;8&quot; x=&quot;228.0205&quot; y=&quot;52.5938&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;231.8887&quot; x2=&quot;231.8887&quot; y1=&quot;52.5938&quot; y2=&quot;254.5234&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;W&quot; data-source-line=&quot;3&quot; id=&quot;part3-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;Worker .1..&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;201.9297&quot; width=&quot;8&quot; x=&quot;334.814&quot; y=&quot;52.5938&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;338.1523&quot; x2=&quot;338.1523&quot; y1=&quot;52.5938&quot; y2=&quot;254.5234&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;T&quot; data-source-line=&quot;1&quot; id=&quot;part1-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;46.5938&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;91.8955&quot; x=&quot;5&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;77.8955&quot; x=&quot;12&quot; y=&quot;24.9951&quot;&gt;SyncPeriod&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;57.8867&quot; x=&quot;22.0044&quot; y=&quot;41.292&quot;&gt;(2분 타이머)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;T&quot; data-source-line=&quot;1&quot; id=&quot;part1-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;46.5938&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;91.8955&quot; x=&quot;5&quot; y=&quot;253.5234&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;77.8955&quot; x=&quot;12&quot; y=&quot;273.5186&quot;&gt;SyncPeriod&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;57.8867&quot; x=&quot;22.0044&quot; y=&quot;289.8154&quot;&gt;(2분 타이머)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;Q&quot; data-source-line=&quot;2&quot; id=&quot;part2-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;96.2637&quot; x=&quot;183.8887&quot; y=&quot;21.2969&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;82.2637&quot; x=&quot;190.8887&quot; y=&quot;41.292&quot;&gt;WorkQueue&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;Q&quot; data-source-line=&quot;2&quot; id=&quot;part2-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;96.2637&quot; x=&quot;183.8887&quot; y=&quot;253.5234&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;82.2637&quot; x=&quot;190.8887&quot; y=&quot;273.5186&quot;&gt;WorkQueue&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;W&quot; data-source-line=&quot;3&quot; id=&quot;part3-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;97.3232&quot; x=&quot;290.1523&quot; y=&quot;21.2969&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.3232&quot; x=&quot;297.1523&quot; y=&quot;41.292&quot;&gt;Worker (1개)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;W&quot; data-source-line=&quot;3&quot; id=&quot;part3-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;97.3232&quot; x=&quot;290.1523&quot; y=&quot;253.5234&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.3232&quot; x=&quot;297.1523&quot; y=&quot;273.5186&quot;&gt;Worker (1개)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;5&quot; id=&quot;msg1&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;220.0205,79.7266,230.0205,83.7266,220.0205,87.7266,224.0205,83.7266&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;50.9478&quot; x2=&quot;226.0205&quot; y1=&quot;83.7266&quot; y2=&quot;83.7266&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;145.1392&quot; x=&quot;57.9478&quot; y=&quot;78.6606&quot;&gt;0:00  CR 50개 enqueue&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part3&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;6&quot; id=&quot;msg2&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;243.0205,108.8594,233.0205,112.8594,243.0205,116.8594,239.0205,112.8594&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;237.0205&quot; x2=&quot;337.814&quot; y1=&quot;112.8594&quot; y2=&quot;112.8594&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;69.8115&quot; x=&quot;249.0205&quot; y=&quot;107.7935&quot;&gt;처리 중... (느림)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;7&quot; id=&quot;msg3&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;220.0205,137.9922,230.0205,141.9922,220.0205,145.9922,224.0205,141.9922&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;50.9478&quot; x2=&quot;226.0205&quot; y1=&quot;141.9922&quot; y2=&quot;141.9922&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;157.0728&quot; x=&quot;57.9478&quot; y=&quot;136.9263&quot;&gt;2:00  CR 50개 또 enqueue&lt;/text&gt;&lt;/g&gt;&lt;path d=&quot;M237,154.9922 L237,209.9922 L340,209.9922 L340,164.9922 L330,154.9922 L237,154.9922&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M330,154.9922 L330,164.9922 L340,164.9922 L330,154.9922&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;70.6748&quot; x=&quot;243&quot; y=&quot;172.0591&quot;&gt;이전 사이클이 아직&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;79.4155&quot; x=&quot;243&quot; y=&quot;187.1919&quot;&gt;안 끝났는데 50개가&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;82.0308&quot; x=&quot;243&quot; y=&quot;202.3247&quot;&gt;또 쌓인다 → 큐 폭증&lt;/text&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;13&quot; id=&quot;msg4&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;220.0205,232.5234,230.0205,236.5234,220.0205,240.5234,224.0205,236.5234&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;50.9478&quot; x2=&quot;226.0205&quot; y1=&quot;236.5234&quot; y2=&quot;236.5234&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;157.0728&quot; x=&quot;57.9478&quot; y=&quot;231.4575&quot;&gt;4:00  CR 50개 또 enqueue&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src AqWiAibCpYn8p2jHK0gkp4iEI2tAp4-9oTCmUhsjHU5jSyERkLjUhzYXgQIGMAmGmbM0h3uyloWxi3IrD1Ki6OWfcLgae67uQiCSYEvmBguG1LqxXK05AmK3AmC31GNd80LJ0w2yGcfU8SWShd2O_9jDCrulMwFmPib4FJqz1OtNNHDUBzkZYT1lXAh_zOm9S3FoyajI5OeoqpDA5FBJWAvIK01wuSs25eKtJMk0hBUDMu2amfkfBM-MDu8aftOel6wS-xffpUkkAQzxDq1CVBMX0IW7ClPDp-GtStUyxbwYyAXjaiBRnWaAR_lNlbasboirBqK1P1V2KIOu7GK0?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;div&gt;&lt;h3 id=&quot;3-클라이언트-rate-limitqpsburst에-막힌다&quot;&gt;3. 클라이언트 rate limit(QPS/Burst)에 막힌다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;설령 워커를 늘려 동시 처리를 한다 해도, 그 모든 reconcile은 결국 &lt;strong&gt;Kubernetes API 서버로의 호출&lt;/strong&gt;로 이어진다. controller-runtime의 client-go 클라이언트에는 &lt;strong&gt;클라이언트 측 rate limiter&lt;/strong&gt;가 걸려 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;QPS&lt;/code&gt;: 초당 허용 요청 수 (controller-runtime 기본 &lt;strong&gt;20&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;Burst&lt;/code&gt;: 순간적으로 허용하는 버스트 (기본 &lt;strong&gt;30&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CR이 많아 동시에 쏟아지는 API 호출이 이 한도를 넘으면, client-go는 &lt;strong&gt;요청을 클라이언트 쪽에서 큐잉하며 의도적으로 지연&lt;/strong&gt;시킨다. 로그에는 &lt;code dir=&quot;auto&quot;&gt;client-side throttling&lt;/code&gt; 경고가 찍히고, reconcile은 또 한 번 느려진다. 동시성을 올려도 QPS/Burst가 그대로면 이 벽에 막힌다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;종합-세-값이-서로를-악화시킨다&quot;&gt;종합: 세 값이 서로를 악화시킨다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;세 병목은 독립적이지 않다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동시성 1 → reconcile 처리 속도가 낮다&lt;/li&gt;
&lt;li&gt;2분 resync → 그 느린 처리량에 주기적으로 전체 CR을 다시 얹는다&lt;/li&gt;
&lt;li&gt;QPS/Burst 20/30 → 처리 속도를 올리려 해도 API 호출 단계에서 다시 막힌다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CR이 적을 때는 한 사이클을 2분 안에 여유롭게 끝내므로 셋 다 문제가 안 된다. 하지만 CR 수가 임계점을 넘는 순간, 큐가 발산하면서 Operator 전체가 멈춘 것처럼 보이게 된다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;해결-하드코딩-값을-helm으로-끄집어내다-pr-769&quot;&gt;해결: 하드코딩 값을 Helm으로 끄집어내다 (PR #769)&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;근본 원인이 &lt;strong&gt;환경에 맞지 않는 고정값&lt;/strong&gt;이라면, 답은 그 값을 &lt;strong&gt;운영자가 환경에 맞게 조절할 수 있게&lt;/strong&gt; 만드는 것이다. &lt;a href=&quot;https://github.com/StarRocks/starrocks-kubernetes-operator/pull/769&quot;&gt;PR #769&lt;/a&gt;은 이 세 가지(+α) 설정을 Operator 플래그로 노출하고, 다시 Helm 차트 값으로 끌어올렸다. 이제 &lt;strong&gt;이미지 재빌드 없이&lt;/strong&gt; values만 바꿔 튜닝할 수 있다.&lt;/p&gt;
&lt;p&gt;노출된 설정은 다음과 같다.&lt;/p&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;플래그&lt;/th&gt;&lt;th&gt;의미&lt;/th&gt;&lt;th&gt;기본값&lt;/th&gt;&lt;th&gt;비고&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;--max-concurrent-reconciles&lt;/code&gt;&lt;/td&gt;&lt;td&gt;동시에 도는 reconcile 워커 수&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;올리면 병렬 처리&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;--sync-period&lt;/code&gt;&lt;/td&gt;&lt;td&gt;주기적 resync 간격&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;2m&lt;/code&gt;&lt;/td&gt;&lt;td&gt;늘리면 주기적 부하 감소&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;--kube-api-qps&lt;/code&gt;&lt;/td&gt;&lt;td&gt;클라이언트 초당 요청 한도&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; → controller-runtime 기본 &lt;code dir=&quot;auto&quot;&gt;20&lt;/code&gt;&lt;/td&gt;&lt;td&gt;0이면 인자 자체를 생략&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;--kube-api-burst&lt;/code&gt;&lt;/td&gt;&lt;td&gt;클라이언트 버스트 한도&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt; → controller-runtime 기본 &lt;code dir=&quot;auto&quot;&gt;30&lt;/code&gt;&lt;/td&gt;&lt;td&gt;0이면 인자 자체를 생략&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;# values.yaml — starrocksOperator 하위&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;starrocksOperator&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# CR이 많은 환경에서 병렬 처리량을 늘린다&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;maxConcurrentReconciles&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# 주기적 전체 reconcile 부하를 줄인다&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;syncPeriod&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;10m&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# API 호출 한도를 환경에 맞게 상향&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;kubeApiQps&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;kubeApiBurst&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;100&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;div&gt;&lt;h3 id=&quot;하위-호환성&quot;&gt;하위 호환성&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;설계에서 신경 쓴 부분은 &lt;strong&gt;기존 사용자가 아무것도 안 해도 동작이 똑같아야 한다&lt;/strong&gt;는 것이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모든 기본값은 기존 하드코딩 값과 동일하다 (&lt;code dir=&quot;auto&quot;&gt;max-concurrent-reconciles=1&lt;/code&gt;, &lt;code dir=&quot;auto&quot;&gt;sync-period=2m&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code dir=&quot;auto&quot;&gt;kube-api-qps&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;kube-api-burst&lt;/code&gt;는 기본값 &lt;code dir=&quot;auto&quot;&gt;0&lt;/code&gt;일 때 &lt;strong&gt;플래그 자체를 전달하지 않아&lt;/strong&gt;, controller-runtime의 원래 기본값(20/30)이 그대로 적용된다.&lt;/li&gt;
&lt;li&gt;따라서 Helm 차트를 다시 렌더링하지 않고 Operator 이미지만 올려도 안전하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;부모 차트(&lt;code dir=&quot;auto&quot;&gt;kube-starrocks&lt;/code&gt;)의 values는 &lt;code dir=&quot;auto&quot;&gt;scripts/create-parent-chart-values.sh&lt;/code&gt;로 서브차트 값에서 재생성된다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;실험-세-값은-정말-부하를-좌우하는가&quot;&gt;실험: 세 값은 정말 부하를 좌우하는가&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;설정을 노출했으니, &lt;strong&gt;각 값이 실제로 부하를 얼마나 좌우하는지&lt;/strong&gt; 측정했다. 이 문제는 특히 &lt;strong&gt;API 서버 응답이 간헐적으로 느려지는 환경&lt;/strong&gt;에서 두드러졌으므로, 핵심 질문을 이렇게 잡았다 — &lt;em&gt;“API에 지연이 끼고 CR이 여러 개일 때, 이 세 값이 클러스터 생성 시간과 상시 트래픽을 얼마나 바꾸는가?”&lt;/em&gt;&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;실험-환경&quot;&gt;실험 환경&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;빠른 반복 검증을 위해 &lt;strong&gt;envtest&lt;/strong&gt;(실제 kube-apiserver + etcd를 띄우되 노드·kubelet은 없는 경량 환경) 위에서 Operator를 구동하고, &lt;strong&gt;Operator → API 서버 경로의 병목만&lt;/strong&gt; 측정했다. 실제 환경의 API 지연은 &lt;strong&gt;프록시(toxiproxy)로 RTT를 인위 주입&lt;/strong&gt;해 재현했다.&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;측정 방식&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;envtest&lt;/code&gt; — 실제 kube-apiserver + etcd, 노드/kubelet 없음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;측정 대상&lt;/td&gt;&lt;td&gt;StarRocks Operator(&lt;code dir=&quot;auto&quot;&gt;kube-starrocks&lt;/code&gt;) → kube-apiserver 호출&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;API 지연 재현&lt;/td&gt;&lt;td&gt;toxiproxy로 Operator↔apiserver 양방향 RTT 주입&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;비교 축&lt;/td&gt;&lt;td&gt;패치 전(기본값) ↔ 패치 후(동시성·QPS·sync-period 상향)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;측정 범위 주의 — 아래 수치는 “하한선”이다.&lt;/strong&gt; envtest엔 kubelet이 없어 StatefulSet을 만들어도 Pod가 안 뜬다. 그래서 Operator는 &lt;strong&gt;FE의 StatefulSet·Service를 만든 뒤 “FE Pod Ready 대기”에서 멈추고, CN·BE는 아예 실행되지 않는다.&lt;/strong&gt; 즉 측정값은 CR 1건이 유발하는 전체 API 호출 중 &lt;strong&gt;FE 단계까지&lt;/strong&gt;만 잡힌다. 실제 환경에선 CN·BE까지 떠서 호출·resync가 곱으로 늘어나므로, &lt;strong&gt;여기서 보이는 개선폭은 실제로는 더 크게&lt;/strong&gt; 나타날 바닥값으로 읽어야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt;&lt;h3 id=&quot;출발점--cr-하나가-api를-얼마나-두드리나&quot;&gt;출발점 — CR 하나가 API를 얼마나 두드리나&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;먼저 부하의 단위를 쟀다. CR 1개를 만들고 30초간 API audit 로그를 세어보니, &lt;strong&gt;FE 생성까지만 12회&lt;/strong&gt;의 호출이 찍혔다. service·statefulset마다 &lt;code dir=&quot;auto&quot;&gt;create&lt;/code&gt;·&lt;code dir=&quot;auto&quot;&gt;update&lt;/code&gt;·&lt;code dir=&quot;auto&quot;&gt;patch&lt;/code&gt;가 반복되고, status 서브리소스 패치만 3회다. 여기서 전체 컴포넌트(FE+CN+BE+FE-proxy)로 환산하면 &lt;strong&gt;CR 1건당 약 30회&lt;/strong&gt;. CR이 수십 개면 이 호출이 곱으로 쌓인다 — 모든 부하의 출발점이다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;노브-1--max-concurrent-reconciles-직렬-처리가-api-지연을-만나면-터진다&quot;&gt;노브 1 — &lt;code dir=&quot;auto&quot;&gt;max-concurrent-reconciles&lt;/code&gt;: 직렬 처리가 API 지연을 만나면 터진다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;기본값 &lt;code dir=&quot;auto&quot;&gt;1&lt;/code&gt;은 CR을 &lt;strong&gt;한 건씩 순차 처리&lt;/strong&gt;한다. 평소엔 티가 안 나지만, &lt;strong&gt;API 왕복이 느려지는 순간 직렬 처리는 재앙이 된다.&lt;/strong&gt; CR 50개를 만들되 RTT 100ms(왕복 200ms)를 주입한 결과:&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;조건&lt;/th&gt;&lt;th&gt;완료 시간&lt;/th&gt;&lt;th&gt;CR당 평균&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;기본값 (동시성 1)&lt;/td&gt;&lt;td&gt;41.1s&lt;/td&gt;&lt;td&gt;822 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;패치 (동시성 50 + QPS 상향)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.22s&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;4.4 ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;xychart-beta
  title &quot;동시성 1→50 — RTT 100ms·CR 50개 생성 완료 시간 (초)&quot;
  x-axis [&quot;기본값 (동시성 1)&quot;, &quot;패치 (동시성 50)&quot;]
  y-axis &quot;완료 시간 (초)&quot; 0 --&gt; 45
  bar [41.1, 0.22]
&lt;/pre&gt; 
&lt;p&gt;기본값은 CR을 한 건씩 처리하며 호출마다 왕복 200ms가 누적돼 &lt;strong&gt;41초&lt;/strong&gt;가 걸렸다. 동시성을 50으로 풀자 50개 CR의 호출이 거의 동시에 나가고 HTTP/2 multiplexing이 받쳐주며 &lt;strong&gt;0.22초&lt;/strong&gt; — 약 &lt;strong&gt;187배&lt;/strong&gt; 단축됐다. 직렬 병목 자체보다, &lt;strong&gt;그게 RTT와 곱해질 때&lt;/strong&gt; 폭발한다는 게 핵심이다.&lt;/p&gt;
&lt;p&gt;RTT가 없는 환경(CR 100개)에서도 효과는 있지만 결이 다르다:&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;조건&lt;/th&gt;&lt;th&gt;완료 시간&lt;/th&gt;&lt;th&gt;throttling&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;기본값&lt;/td&gt;&lt;td&gt;8.9s&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;동시성 50만&lt;/td&gt;&lt;td&gt;5.99s&lt;/td&gt;&lt;td&gt;&lt;strong&gt;1회&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;전체 패치&lt;/td&gt;&lt;td&gt;0.60s&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;RTT가 없어도 동시성만 올리면 8.9s→5.99s로 &lt;strong&gt;1.5배&lt;/strong&gt; 빨라진다. 그런데 이번엔 &lt;strong&gt;throttling이 1회 떴다&lt;/strong&gt; — 동시 호출을 늘리니 이번엔 QPS 상한(20)에 걸리기 시작한 것이다. 그래서 다음 노브가 따라와야 한다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;노브-2--kube-api-qpsburst-동시성을-올리면-반드시-같이-올려야-한다&quot;&gt;노브 2 — &lt;code dir=&quot;auto&quot;&gt;kube-api-qps&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;burst&lt;/code&gt;: 동시성을 올리면 반드시 같이 올려야 한다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;동시성을 50으로 키워 호출을 한꺼번에 쏟으면, client-go의 &lt;strong&gt;클라이언트 측 rate limiter&lt;/strong&gt;(&lt;code dir=&quot;auto&quot;&gt;QPS=20&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;Burst=30&lt;/code&gt;)에 걸려 스스로 요청을 지연시키는 &lt;strong&gt;self-throttling&lt;/strong&gt;이 발생한다.&lt;/p&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;조건&lt;/th&gt;&lt;th&gt;throttling 이벤트&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;동시성 50, QPS 기본(20)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;1회&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;동시성 50, QPS 100&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0회&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;QPS/Burst를 100/200으로 올리자 throttling이 사라졌다. 즉 &lt;strong&gt;QPS는 단독 레버가 아니라 동시성 상향의 짝&lt;/strong&gt;이다 — 워커만 늘리고 QPS를 그대로 두면 늘린 만큼 클라이언트 큐에서 다시 막힌다. FE만 뜨는 이 환경에서도 1회 관측됐으니, CN까지 뜨는 실제 환경에선 빈도가 더 높다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;노브-3--sync-period-가만히-있어도-깔리는-상시-부하&quot;&gt;노브 3 — &lt;code dir=&quot;auto&quot;&gt;sync-period&lt;/code&gt;: 가만히 있어도 깔리는 상시 부하&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;앞 두 노브가 “생성하는 순간”의 부하라면, &lt;code dir=&quot;auto&quot;&gt;sync-period&lt;/code&gt;는 &lt;strong&gt;아무것도 안 바뀌어도 주기적으로 깔리는 상시 트래픽&lt;/strong&gt;을 좌우한다. CR 100개를 만든 뒤 5분간 초당 API 호출율을 쟀다:&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;조건&lt;/th&gt;&lt;th&gt;5분간 이벤트&lt;/th&gt;&lt;th&gt;평균 req/s&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;기본값 (sync-period 2m)&lt;/td&gt;&lt;td&gt;345&lt;/td&gt;&lt;td&gt;1.15&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;패치 (sync-period 30m)&lt;/td&gt;&lt;td&gt;95&lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.32&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;xychart-beta
  title &quot;sync-period 2m→30m — CR 100개 상시 트래픽 (평균 req/s)&quot;
  x-axis [&quot;sync 2m (기본)&quot;, &quot;sync 30m (패치)&quot;]
  y-axis &quot;평균 req/s&quot; 0 --&gt; 1.3
  bar [1.15, 0.32]
&lt;/pre&gt; 
&lt;p&gt;resync 주기를 2분→30분으로 늘리자 상시 트래픽이 &lt;strong&gt;약 1/4&lt;/strong&gt;로 줄었다. 이 트래픽의 대부분은 &lt;strong&gt;상태가 안 바뀐 CR을 다시 도는 것&lt;/strong&gt; — 순수한 낭비다. 그리고 이 환경은 FE만 떠서 이벤트가 적은데도 이 정도다. CN이 다수 뜨는 실제 환경에선 resync 대상이 훨씬 많아 &lt;strong&gt;상시 부하가 곱으로 커지고&lt;/strong&gt;, 그만큼 주기를 늘리는 효과도 커진다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;종합-해석&quot;&gt;종합 해석&lt;/h3&gt;&lt;/div&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;노브&lt;/th&gt;&lt;th&gt;무엇을 바꾸나&lt;/th&gt;&lt;th&gt;검증된 효과&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;max-concurrent-reconciles&lt;/code&gt; 1 → 50&lt;/td&gt;&lt;td&gt;직렬 → 병렬 처리&lt;/td&gt;&lt;td&gt;RTT 100ms·CR 50개에서 &lt;strong&gt;41.1s → 0.22s&lt;/strong&gt;. API가 느릴수록 효과가 폭발&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;kube-api-qps&lt;/code&gt;/&lt;code dir=&quot;auto&quot;&gt;burst&lt;/code&gt; 20/30 → 100/200&lt;/td&gt;&lt;td&gt;클라이언트 rate limit 완화&lt;/td&gt;&lt;td&gt;동시성 상향 시 생기는 self-throttling &lt;strong&gt;1회 → 0회&lt;/strong&gt;. 동시성의 필수 짝&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;sync-period&lt;/code&gt; 2m → 30m&lt;/td&gt;&lt;td&gt;주기적 resync 간격&lt;/td&gt;&lt;td&gt;상시 트래픽 &lt;strong&gt;1.15 → 0.32 req/s&lt;/strong&gt; (≈1/4). 변경 없는 CR의 낭비 reconcile 감소&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;세 값 모두 &lt;strong&gt;환경에 따라 정답이 다르다&lt;/strong&gt; — API가 느린지, CR이 몇 개인지, drift 보정이 얼마나 급한지에 따라. 그래서 코드에 박지 않고 Helm으로 노출한 것이 옳았다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;StarRocks Operator는 &lt;strong&gt;소수 CR을 전제로&lt;/strong&gt; 설계되어, controller-runtime의 동시성(1)·resync(2분)·rate limit(20/30)이 하드코딩돼 있었다.&lt;/li&gt;
&lt;li&gt;이 고정값들은 CR이 적을 땐 무해하지만, &lt;strong&gt;다수 클러스터 환경에서 큐가 발산&lt;/strong&gt;하며 Operator를 hang에 빠뜨린다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/StarRocks/starrocks-kubernetes-operator/pull/769&quot;&gt;PR #769&lt;/a&gt;은 이 값들을 플래그·Helm values로 노출해 &lt;strong&gt;이미지 재빌드 없이&lt;/strong&gt; 환경에 맞게 튜닝할 수 있게 했고, 기본값은 그대로 둬 하위 호환성을 지켰다.&lt;/li&gt;
&lt;li&gt;envtest에 RTT를 주입해 패치 전후를 측정한 결과: &lt;strong&gt;동시성 1→50&lt;/strong&gt;은 RTT 100ms·CR 50개 환경에서 생성 시간을 &lt;strong&gt;41.1s → 0.22s&lt;/strong&gt;로 줄였고, &lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;sync-period&lt;/code&gt; 2m→30m&lt;/strong&gt;은 상시 트래픽을 약 &lt;strong&gt;1/4&lt;/strong&gt;(1.15→0.32 req/s)로 낮췄으며, &lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;kube-api-qps&lt;/code&gt; 상향&lt;/strong&gt;은 동시성을 올릴 때 따라붙는 self-throttling(1회→0회)을 제거했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국 이 작업의 핵심은 새로운 기능이 아니라 &lt;strong&gt;“환경에 따라 달라야 하는 값을 코드에 박아두지 마라”&lt;/strong&gt; 는 원칙이다. Operator처럼 다양한 규모에서 돌아야 하는 컨트롤러일수록, 동시성·주기·rate limit 같은 운영 파라미터는 &lt;strong&gt;운영자의 손에 쥐여주는 것&lt;/strong&gt;이 옳다.&lt;/p&gt;</content:encoded><category>Kubernetes</category><category>StarRocks</category><category>Operator</category><category>controller-runtime</category><category>Helm</category></item><item><title>Kubernetes Informer는 어떻게 동작하는가</title><link>https://ycra-dev.github.io/blog/kubernetes-informer/</link><guid isPermaLink="true">https://ycra-dev.github.io/blog/kubernetes-informer/</guid><description>컨트롤러는 어떻게 매번 API 서버를 조회하지 않고도 리소스 변화를 알아챌까? client-go Informer의 내부 구조를 Reflector·DeltaFIFO·Indexer 흐름으로 정리한다.

</description><pubDate>Sun, 21 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h2 id=&quot;watch를-직접-걸-때-마주치는-것들&quot;&gt;watch를 직접 걸 때 마주치는 것들&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Kubernetes 위에서 동작하는 컨트롤러를 만들 때, 가장 먼저 부딪히는 문제는 &lt;strong&gt;“리소스 변화를 어떻게 감지할 것인가”&lt;/strong&gt; 다. 가장 단순한 답은 API 서버에 직접 &lt;code dir=&quot;auto&quot;&gt;watch&lt;/code&gt;를 거는 것이다. Java의 fabric8 클라이언트를 사용한다면 이렇게 watch를 걸 수 있다.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// API 서버에 직접 watch 연결 — 이벤트 스트림을 그대로 받는다&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;pods&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;inNamespace&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;watch&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Watcher&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;eventReceived&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Action&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;action&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// ADDED / MODIFIED / DELETED 이벤트가 올 때마다 호출&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;reconcile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;Override&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;onClose&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;WatcherException&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;e&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;// 연결이 끊기면 직접 재연결 로직을 구현해야 한다&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;직접 watch를 걸면 event를 수신할 수는 있지만 여러가지 복잡한 문제를 겪게 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;연결이 끊기면 직접 복구해야 한다.&lt;/strong&gt; watch는 장시간 유지되는 HTTP 스트림이라 네트워크·재시작 등으로 끊긴다. 끊기면 마지막 &lt;code dir=&quot;auto&quot;&gt;resourceVersion&lt;/code&gt;부터 다시 watch를 걸어야 하고, 그 버전이 너무 오래돼 &lt;code dir=&quot;auto&quot;&gt;410 Gone&lt;/code&gt;이 오면 &lt;strong&gt;전체를 다시 List&lt;/strong&gt;한 뒤 watch를 재개해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로컬 상태가 없다.&lt;/strong&gt; “지금 이 네임스페이스의 Pod 전체”가 필요하면 또 API 서버에 List를 날려야 한다. 캐시가 없으니 읽을 때마다 API 서버에 부담이 간다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;컴포넌트마다 watch가 늘어난다.&lt;/strong&gt; 컨트롤러 A, B, C가 각자 Pod를 본다면 watch 연결도 3개다. 같은 데이터를 세 번 받는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;DESCRIPTION&quot; height=&quot;209px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 390 209&quot; width=&quot;390px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;!--entity A--&gt;&lt;g data-qualified-name=&quot;A&quot; data-source-line=&quot;4&quot; id=&quot;ent0001&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;103.2002&quot; x=&quot;7&quot; y=&quot;7&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.2002&quot; x=&quot;17&quot; y=&quot;29.9951&quot;&gt;Controller A&lt;/text&gt;&lt;/g&gt;&lt;!--entity B--&gt;&lt;g data-qualified-name=&quot;B&quot; data-source-line=&quot;5&quot; id=&quot;ent0002&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;103.2275&quot; x=&quot;7&quot; y=&quot;78&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.2275&quot; x=&quot;17&quot; y=&quot;100.9951&quot;&gt;Controller B&lt;/text&gt;&lt;/g&gt;&lt;!--entity C--&gt;&lt;g data-qualified-name=&quot;C&quot; data-source-line=&quot;6&quot; id=&quot;ent0003&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;103.3984&quot; x=&quot;7&quot; y=&quot;149&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.3984&quot; x=&quot;17&quot; y=&quot;171.9951&quot;&gt;Controller C&lt;/text&gt;&lt;/g&gt;&lt;!--entity API--&gt;&lt;g data-qualified-name=&quot;API&quot; data-source-line=&quot;7&quot; id=&quot;ent0004&quot;&gt;&lt;polygon fill=&quot;#F1F1F1&quot; points=&quot;253,83,263,73,365.5088,73,365.5088,109.2969,355.5088,119.2969,253,119.2969,253,83&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;355.5088&quot; x2=&quot;365.5088&quot; y1=&quot;83&quot; y2=&quot;73&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;253&quot; x2=&quot;355.5088&quot; y1=&quot;83&quot; y2=&quot;83&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;355.5088&quot; x2=&quot;355.5088&quot; y1=&quot;83&quot; y2=&quot;119.2969&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;268&quot; y=&quot;105.9951&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;g data-qualified-name=&quot;GMN8&quot; data-source-line=&quot;14&quot; id=&quot;ent0009&quot;&gt;&lt;path d=&quot;M259.5,154 L259.5,194.2656 A0,0 0 0 0 259.5,194.2656 L359.6172,194.2656 A0,0 0 0 0 359.6172,194.2656 L359.6172,164 L349.6172,154 L313.5,154 L309.5,119.07 L305.5,154 L259.5,154 A0,0 0 0 0 259.5,154&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M349.6172,154 L349.6172,164 L359.6172,164 L349.6172,154&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;79.1172&quot; x=&quot;265.5&quot; y=&quot;171.0669&quot;&gt;같은 Pod 데이터를&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;55.0723&quot; x=&quot;265.5&quot; y=&quot;186.1997&quot;&gt;세 번 스트리밍&lt;/text&gt;&lt;/g&gt;&lt;!--link A to API--&gt;&lt;g data-entity-1=&quot;ent0001&quot; data-entity-2=&quot;ent0004&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;9&quot; id=&quot;lnk5&quot;&gt;&lt;path d=&quot;M110.35,39.16 C142.64,48.19 185.3,60.19 223,71 C232.66,73.77 238.1301,75.3496 248.0701,78.2496&quot; fill=&quot;none&quot; id=&quot;A-to-API&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;252.87,79.65,245.3505,73.2894,248.0701,78.2496,243.1099,80.9692,252.87,79.65&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;81.3643&quot; x=&quot;141&quot; y=&quot;45.0669&quot;&gt;watch (연결 1)&lt;/text&gt;&lt;/g&gt;&lt;!--link B to API--&gt;&lt;g data-entity-1=&quot;ent0002&quot; data-entity-2=&quot;ent0004&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;10&quot; id=&quot;lnk6&quot;&gt;&lt;path d=&quot;M110.19,96 C151.57,96 204.97,96 247.68,96&quot; fill=&quot;none&quot; id=&quot;B-to-API&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;252.68,96,243.68,92,247.68,96,243.68,100,252.68,96&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;81.3643&quot; x=&quot;141&quot; y=&quot;92.0669&quot;&gt;watch (연결 2)&lt;/text&gt;&lt;/g&gt;&lt;!--link C to API--&gt;&lt;g data-entity-1=&quot;ent0003&quot; data-entity-2=&quot;ent0004&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;11&quot; id=&quot;lnk7&quot;&gt;&lt;path d=&quot;M110.35,152.84 C142.64,143.81 185.3,131.81 223,121 C232.66,118.23 238.1301,116.6504 248.0701,113.7504&quot; fill=&quot;none&quot; id=&quot;C-to-API&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;252.87,112.35,243.1099,111.0308,248.0701,113.7504,245.3505,118.7106,252.87,112.35&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;81.3643&quot; x=&quot;141&quot; y=&quot;117.0669&quot;&gt;watch (연결 3)&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src TSqnJiD050JGVaynSeMB5417WMIxIcSf9rZidyHYlJ_QV16beqY18gL65Ec0Yc8LKQJWHDdD7OYDHETkf3UQCJGL2CCrixcWRXnLqh1LoyV6BhJJBIfk5sp9oaJMXj0Lj9qPKkfVa1HinR4nv90bq4ja0vhtcWzeqMkXBDU492l7c91x8lStMOwLoZ0QtNSHTrXfgUQuYg_-TF2uJbK-Y3UfAWRnDbMNHo4yi0Ytu6d_1PpyMzm_e-GQOUVZ_lkyyU7Zvo9nSqGul21kty_RO_Zy2dwdoDReTdu1?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;p&gt;Informer는 바로 이 반복되는 문제들 — &lt;strong&gt;재연결, 재동기화, 로컬 캐시, 연결 공유&lt;/strong&gt; — 를 한 번에 정리해 주는 패턴이다. fabric8에도 client-go를 본떠 만든 &lt;code dir=&quot;auto&quot;&gt;SharedIndexInformer&lt;/code&gt;가 있다.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SharedInformerFactory&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;factory&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;informers&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;SharedIndexInformer&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;informer&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;factory&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;sharedIndexInformerFor&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;class&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;30_000L&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;; &lt;/span&gt;&lt;span&gt;// resync 주기 30s&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;informer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;addEventHandler&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ResourceEventHandler&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;&gt;() {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;onAdd&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;reconcile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;onUpdate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;oldPod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;newPod&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;reconcile&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;newPod&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;onDelete&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;boolean&lt;/span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;finalStateUnknown&lt;/span&gt;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; { &lt;/span&gt;&lt;span&gt;cleanup&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;; }&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;factory&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;startAllRegisteredInformers&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;
&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 읽기는 API 서버가 아니라 로컬 캐시에서&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;Lister&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;lister&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;new&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;Lister&lt;/span&gt;&lt;span&gt;&amp;#x3C;&gt;(&lt;/span&gt;&lt;span&gt;informer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;getIndexer&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;List&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Pod&lt;/span&gt;&lt;span&gt;&gt; &lt;/span&gt;&lt;span&gt;pods&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;lister&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;list&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;겉보기엔 “이벤트 핸들러 등록”이 전부지만, 그 아래에서는 꽤 정교한 매커니즘이 돌아간다. 이제 그 내부를 파헤쳐 보자. (이하 구조는 client-go 기준이며, fabric8 informer도 동일한 모델을 따른다.)&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;informer는-어떻게-동작하는가&quot;&gt;Informer는 어떻게 동작하는가&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Informer의 핵심은 &lt;strong&gt;“한 번 전체를 받아오고(List), 그 뒤로는 변화만 따라간다(Watch)”&lt;/strong&gt;, 그리고 &lt;strong&gt;그 결과를 로컬 캐시에 항상 최신으로 유지한다&lt;/strong&gt;는 것이다. 내부는 크게 네 개의 컴포넌트로 구성된다.&lt;/p&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;DESCRIPTION&quot; height=&quot;550px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 396 550&quot; width=&quot;396px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;!--entity API--&gt;&lt;g data-qualified-name=&quot;API&quot; data-source-line=&quot;3&quot; id=&quot;ent0001&quot;&gt;&lt;polygon fill=&quot;#F1F1F1&quot; points=&quot;142,16,152,6,254.5088,6,254.5088,42.2969,244.5088,52.2969,142,52.2969,142,16&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;244.5088&quot; x2=&quot;254.5088&quot; y1=&quot;16&quot; y2=&quot;6&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;142&quot; x2=&quot;244.5088&quot; y1=&quot;16&quot; y2=&quot;16&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;244.5088&quot; x2=&quot;244.5088&quot; y1=&quot;16&quot; y2=&quot;52.2969&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;157&quot; y=&quot;38.9951&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;!--entity R--&gt;&lt;g data-qualified-name=&quot;R&quot; data-source-line=&quot;4&quot; id=&quot;ent0002&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;83.2803&quot; x=&quot;157&quot; y=&quot;129&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;63.2803&quot; x=&quot;167&quot; y=&quot;151.9951&quot;&gt;Reflector&lt;/text&gt;&lt;/g&gt;&lt;!--entity F--&gt;&lt;g data-qualified-name=&quot;F&quot; data-source-line=&quot;5&quot; id=&quot;ent0003&quot;&gt;&lt;path d=&quot;M159,242 L237.6055,242 C242.6055,242 242.6055,255.1484 242.6055,255.1484 C242.6055,255.1484 242.6055,268.2969 237.6055,268.2969 L159,268.2969 C154,268.2969 154,255.1484 154,255.1484 C154,255.1484 154,242 159,242&quot; fill=&quot;#F1F1F1&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M237.6055,242 C232.6055,242 232.6055,255.1484 232.6055,255.1484 C232.6055,268.2969 237.6055,268.2969 237.6055,268.2969&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;68.6055&quot; x=&quot;159&quot; y=&quot;259.9951&quot;&gt;DeltaFIFO&lt;/text&gt;&lt;/g&gt;&lt;!--entity I--&gt;&lt;g data-qualified-name=&quot;I&quot; data-source-line=&quot;6&quot; id=&quot;ent0004&quot;&gt;&lt;path d=&quot;M144,355 C144,345 198.6079,345 198.6079,345 C198.6079,345 253.2158,345 253.2158,355 L253.2158,396.5938 C253.2158,406.5938 198.6079,406.5938 198.6079,406.5938 C198.6079,406.5938 144,406.5938 144,396.5938 L144,355&quot; fill=&quot;#F1F1F1&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M144,355 C144,365 198.6079,365 198.6079,365 C198.6079,365 253.2158,365 253.2158,355&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;53.1563&quot; x=&quot;154&quot; y=&quot;381.9951&quot;&gt;Indexer&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;89.2158&quot; x=&quot;154&quot; y=&quot;398.292&quot;&gt;(Local Store)&lt;/text&gt;&lt;/g&gt;&lt;!--entity H--&gt;&lt;g data-qualified-name=&quot;H&quot; data-source-line=&quot;7&quot; id=&quot;ent0005&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;52.5938&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;146.9092&quot; x=&quot;7&quot; y=&quot;484&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;106.9756&quot; x=&quot;17&quot; y=&quot;506.9951&quot;&gt;Event Handlers&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;126.9092&quot; x=&quot;17&quot; y=&quot;523.292&quot;&gt;(sharedProcessor)&lt;/text&gt;&lt;/g&gt;&lt;!--entity L--&gt;&lt;g data-qualified-name=&quot;L&quot; data-source-line=&quot;8&quot; id=&quot;ent0006&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;58.8418&quot; x=&quot;288&quot; y=&quot;492.5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;38.8418&quot; x=&quot;298&quot; y=&quot;515.4951&quot;&gt;Lister&lt;/text&gt;&lt;/g&gt;&lt;!--link API to R--&gt;&lt;g data-entity-1=&quot;ent0001&quot; data-entity-2=&quot;ent0002&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;10&quot; id=&quot;lnk7&quot;&gt;&lt;path d=&quot;M198.5,52.09 C198.5,74.44 198.5,103.52 198.5,123.89&quot; fill=&quot;none&quot; id=&quot;API-to-R&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;198.5,128.89,202.5,119.89,198.5,123.89,194.5,119.89,198.5,128.89&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.186&quot; x=&quot;199.5&quot; y=&quot;95.0669&quot;&gt;List + Watch&lt;/text&gt;&lt;/g&gt;&lt;!--link R to F--&gt;&lt;g data-entity-1=&quot;ent0002&quot; data-entity-2=&quot;ent0003&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;11&quot; id=&quot;lnk8&quot;&gt;&lt;path d=&quot;M198.5,165.03 C198.5,186.57 198.5,218.11 198.5,236.96&quot; fill=&quot;none&quot; id=&quot;R-to-F&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;198.5,241.96,202.5,232.96,198.5,236.96,194.5,232.96,198.5,241.96&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;52.6602&quot; x=&quot;199.5&quot; y=&quot;208.0669&quot;&gt;delta 추가&lt;/text&gt;&lt;/g&gt;&lt;!--link F to I--&gt;&lt;g data-entity-1=&quot;ent0003&quot; data-entity-2=&quot;ent0004&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;12&quot; id=&quot;lnk9&quot;&gt;&lt;path d=&quot;M198.5,268.29 C198.5,286.22 198.5,315.13 198.5,339.92&quot; fill=&quot;none&quot; id=&quot;F-to-I&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;198.5,344.92,202.5,335.92,198.5,339.92,194.5,335.92,198.5,344.92&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;78.5396&quot; x=&quot;199.5&quot; y=&quot;311.0669&quot;&gt;Pop → 캐시 갱신&lt;/text&gt;&lt;/g&gt;&lt;!--link I to H--&gt;&lt;g data-entity-1=&quot;ent0004&quot; data-entity-2=&quot;ent0005&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;13&quot; id=&quot;lnk10&quot;&gt;&lt;path d=&quot;M143.95,389.9 C119.16,398.78 92.15,413.39 77.5,437 C68.93,450.81 68.7864,464.2057 71.7164,478.9657&quot; fill=&quot;none&quot; id=&quot;I-to-H&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;72.69,483.87,74.8611,474.2634,71.7164,478.9657,67.0142,475.8211,72.69,483.87&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;195.895&quot; x=&quot;78.5&quot; y=&quot;450.0669&quot;&gt;OnAdd / OnUpdate / OnDelete&lt;/text&gt;&lt;/g&gt;&lt;!--reverse link I to L--&gt;&lt;g data-entity-1=&quot;ent0004&quot; data-entity-2=&quot;ent0006&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;14&quot; id=&quot;lnk11&quot;&gt;&lt;path d=&quot;M249.8375,410.2166 C261.3775,418.9466 269.1,426.08 278.5,437 C292.95,453.79 304.2,476.83 310.82,492.39&quot; fill=&quot;none&quot; id=&quot;I-backto-L&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;245.85,407.2,250.6143,415.8198,249.8375,410.2166,255.4408,409.4398,245.85,407.2&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;89.9146&quot; x=&quot;290.5&quot; y=&quot;450.0669&quot;&gt;읽기 (API 호출 없음)&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src LO_1IiD048Rl-nH3JYqIl8i81Gq911eIn8kNDJksnUrktDqMlNdgfHv413squ2FKIoc2B-CnhE_W917Hq-p-__yD_zYhYQguuLCezRJI2fKhtAr4C5WwhaOI6LDQ80I3B842pHnD0Dm2VTal0a6Ebv88tPavkvxXZA1Zb8v7IJHivOW9xlW5j-GaIk0DcdFLItN99HJ4Oh-D_LjyCcz6p9MGQ2p5xPWR59dH9LghJOV4Vv5qOjrFovInfdSO7a4E1z0ui0Tdt9LZbhToHB9eMeBVFjMREnQrSa9ofYluN3o2Vt_monNKcpU_V6NTjfZyeHe80ViqJoiw3DidNOmEANKOXf1Iogy_wjq6UasFhvUTtwx0Fo_y-hx_3G00?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;div&gt;&lt;h3 id=&quot;list--watch-한-번-받아오고-변화만-따라간다&quot;&gt;List + Watch: 한 번 받아오고, 변화만 따라간다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;가장 안쪽에는 &lt;strong&gt;Reflector&lt;/strong&gt;가 있다. Reflector가 하는 일은 두 단계다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;List&lt;/strong&gt; — 대상 리소스 전체를 한 번 조회하고, 응답에 담긴 &lt;code dir=&quot;auto&quot;&gt;resourceVersion&lt;/code&gt;(그 시점의 스냅샷 버전)을 기억한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Watch&lt;/strong&gt; — 그 &lt;code dir=&quot;auto&quot;&gt;resourceVersion&lt;/code&gt;부터 watch를 걸어, 이후 발생하는 변화 이벤트만 스트림으로 받는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;연결이 끊기거나 &lt;code dir=&quot;auto&quot;&gt;410 Gone&lt;/code&gt;이 오면 Reflector가 알아서 다시 List → Watch를 수행한다. 앞서 직접 짜야 했던 재연결·재동기화 로직이 여기 들어 있는 것이다.&lt;/p&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;SEQUENCE&quot; height=&quot;456px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 560 456&quot; width=&quot;560px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;1&quot; id=&quot;part1-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;API Server&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;382.4609&quot; width=&quot;8&quot; x=&quot;44.2544&quot; y=&quot;36.2969&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;48&quot; x2=&quot;48&quot; y1=&quot;36.2969&quot; y2=&quot;418.7578&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;R&quot; data-source-line=&quot;2&quot; id=&quot;part2-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;Reflector&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;382.4609&quot; width=&quot;8&quot; x=&quot;303.0352&quot; y=&quot;36.2969&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;306.395&quot; x2=&quot;306.395&quot; y1=&quot;36.2969&quot; y2=&quot;418.7578&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;F&quot; data-source-line=&quot;3&quot; id=&quot;part3-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;DeltaFIFO&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;382.4609&quot; width=&quot;8&quot; x=&quot;504.5728&quot; y=&quot;36.2969&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;508.27&quot; x2=&quot;508.27&quot; y1=&quot;36.2969&quot; y2=&quot;418.7578&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;1&quot; id=&quot;part1-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;86.5088&quot; x=&quot;5&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;12&quot; y=&quot;24.9951&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;1&quot; id=&quot;part1-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;86.5088&quot; x=&quot;5&quot; y=&quot;417.7578&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;12&quot; y=&quot;437.7529&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;R&quot; data-source-line=&quot;2&quot; id=&quot;part2-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;77.2803&quot; x=&quot;268.395&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;63.2803&quot; x=&quot;275.395&quot; y=&quot;24.9951&quot;&gt;Reflector&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;R&quot; data-source-line=&quot;2&quot; id=&quot;part2-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;77.2803&quot; x=&quot;268.395&quot; y=&quot;417.7578&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;63.2803&quot; x=&quot;275.395&quot; y=&quot;437.7529&quot;&gt;Reflector&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;F&quot; data-source-line=&quot;3&quot; id=&quot;part3-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;82.6055&quot; x=&quot;467.27&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;68.6055&quot; x=&quot;474.27&quot; y=&quot;24.9951&quot;&gt;DeltaFIFO&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;F&quot; data-source-line=&quot;3&quot; id=&quot;part3-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;82.6055&quot; x=&quot;467.27&quot; y=&quot;417.7578&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;68.6055&quot; x=&quot;474.27&quot; y=&quot;437.7529&quot;&gt;DeltaFIFO&lt;/text&gt;&lt;/g&gt;&lt;rect fill=&quot;#EEEEEE&quot; height=&quot;3&quot; width=&quot;554.8755&quot; x=&quot;0&quot; y=&quot;66.8633&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;0&quot; x2=&quot;554.8755&quot; y1=&quot;66.8633&quot; y2=&quot;66.8633&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;0&quot; x2=&quot;554.8755&quot; y1=&quot;69.8633&quot; y2=&quot;69.8633&quot;&gt;&lt;/line&gt;&lt;rect fill=&quot;#EEEEEE&quot; height=&quot;23.1328&quot; width=&quot;62.0581&quot; x=&quot;246.4087&quot; y=&quot;56.2969&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;43.5322&quot; x=&quot;252.4087&quot; y=&quot;72.3638&quot;&gt;초기 동기화&lt;/text&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part1&quot; data-source-line=&quot;6&quot; id=&quot;msg1&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;59.2544,106.5625,49.2544,110.5625,59.2544,114.5625,55.2544,110.5625&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;53.2544&quot; x2=&quot;306.0352&quot; y1=&quot;110.5625&quot; y2=&quot;110.5625&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;22.7246&quot; x=&quot;65.2544&quot; y=&quot;105.4966&quot;&gt;List&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;7&quot; id=&quot;msg2&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;295.0352,135.6953,305.0352,139.6953,295.0352,143.6953,299.0352,139.6953&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;48.2544&quot; x2=&quot;301.0352&quot; y1=&quot;139.6953&quot; y2=&quot;139.6953&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;160.1069&quot; x=&quot;55.2544&quot; y=&quot;134.6294&quot;&gt;전체 객체 + resourceVersion&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part3&quot; data-source-line=&quot;8&quot; id=&quot;msg3&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;496.5728,164.8281,506.5728,168.8281,496.5728,172.8281,500.5728,168.8281&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;307.0352&quot; x2=&quot;502.5728&quot; y1=&quot;168.8281&quot; y2=&quot;168.8281&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;97.4873&quot; x=&quot;314.0352&quot; y=&quot;163.7622&quot;&gt;Replace(전체 객체)&lt;/text&gt;&lt;/g&gt;&lt;rect fill=&quot;#EEEEEE&quot; height=&quot;3&quot; width=&quot;554.8755&quot; x=&quot;0&quot; y=&quot;197.3945&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;0&quot; x2=&quot;554.8755&quot; y1=&quot;197.3945&quot; y2=&quot;197.3945&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;0&quot; x2=&quot;554.8755&quot; y1=&quot;200.3945&quot; y2=&quot;200.3945&quot;&gt;&lt;/line&gt;&lt;rect fill=&quot;#EEEEEE&quot; height=&quot;23.1328&quot; width=&quot;54.2568&quot; x=&quot;250.3093&quot; y=&quot;186.8281&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;35.731&quot; x=&quot;256.3093&quot; y=&quot;202.895&quot;&gt;변화 추적&lt;/text&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part1&quot; data-source-line=&quot;11&quot; id=&quot;msg4&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;59.2544,237.0938,49.2544,241.0938,59.2544,245.0938,55.2544,241.0938&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;53.2544&quot; x2=&quot;306.0352&quot; y1=&quot;241.0938&quot; y2=&quot;241.0938&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;157.0601&quot; x=&quot;65.2544&quot; y=&quot;236.0278&quot;&gt;Watch(resourceVersion)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;12&quot; id=&quot;msg5&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;295.0352,266.2266,305.0352,270.2266,295.0352,274.2266,299.0352,270.2266&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;48.2544&quot; x2=&quot;301.0352&quot; y1=&quot;270.2266&quot; y2=&quot;270.2266&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;234.7808&quot; x=&quot;55.2544&quot; y=&quot;265.1606&quot;&gt;ADDED / MODIFIED / DELETED (스트림)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part3&quot; data-source-line=&quot;13&quot; id=&quot;msg6&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;496.5728,295.3594,506.5728,299.3594,496.5728,303.3594,500.5728,299.3594&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;307.0352&quot; x2=&quot;502.5728&quot; y1=&quot;299.3594&quot; y2=&quot;299.3594&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;177.5376&quot; x=&quot;314.0352&quot; y=&quot;294.2935&quot;&gt;Add / Update / Delete delta&lt;/text&gt;&lt;/g&gt;&lt;rect fill=&quot;#EEEEEE&quot; height=&quot;3&quot; width=&quot;554.8755&quot; x=&quot;0&quot; y=&quot;327.9258&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;0&quot; x2=&quot;554.8755&quot; y1=&quot;327.9258&quot; y2=&quot;327.9258&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;0&quot; x2=&quot;554.8755&quot; y1=&quot;330.9258&quot; y2=&quot;330.9258&quot;&gt;&lt;/line&gt;&lt;rect fill=&quot;#EEEEEE&quot; height=&quot;23.1328&quot; width=&quot;54.2568&quot; x=&quot;250.3093&quot; y=&quot;317.3594&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;35.731&quot; x=&quot;256.3093&quot; y=&quot;333.4263&quot;&gt;연결 끊김&lt;/text&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;16&quot; id=&quot;msg7&quot;&gt;&lt;line x1=&quot;289.0352&quot; x2=&quot;299.0352&quot; y1=&quot;366.625&quot; y2=&quot;376.625&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;289.0352&quot; x2=&quot;299.0352&quot; y1=&quot;376.625&quot; y2=&quot;366.625&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;48.2544&quot; x2=&quot;294.0352&quot; y1=&quot;371.625&quot; y2=&quot;371.625&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;111.1919&quot; x=&quot;55.2544&quot; y=&quot;366.5591&quot;&gt;410 Gone / 연결 종료&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part1&quot; data-source-line=&quot;17&quot; id=&quot;msg8&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;59.2544,396.7578,49.2544,400.7578,59.2544,404.7578,55.2544,400.7578&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;53.2544&quot; x2=&quot;306.0352&quot; y1=&quot;400.7578&quot; y2=&quot;400.7578&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;102.9209&quot; x=&quot;65.2544&quot; y=&quot;395.6919&quot;&gt;다시 List → Watch&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src NL1DIyD04BtdLmmz9KXHmPCG8P140fLA_3eloOY1a8JDAXvRK95K4AGW1OMYnNEeHIhuQpmcs__WRbAmwMdUpfkPz_ObXBB02n8ICMWPXmuS8Rr2sWAIWdWgoIhludc87ejhscsI9eQCs8xThKXRKNGT-Eo-cETGFev4MOo6eEkA2-qzUHfseHEaJ96mBLgkQF3n3P_EeCXVPTa0YcbyIJqyHPe6SLJlscBGnIGa7ggD1QtIB3zxKeb_3VculoPuHfXteQvTrHeM3DEqJDY4Wwxfs4u5JQjZ7GkaycoooERbnu_sxyJmVJ5oalY4ePp540NmPHXr1CzvCHKHzBFYkoVjBCMkAxMTxItOZoEvkHpaRxVb-yEgwN8muOENAYpulNkgF_47?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;div&gt;&lt;h3 id=&quot;reflector--deltafifo--indexer&quot;&gt;Reflector → DeltaFIFO → Indexer&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;Reflector가 받은 변화는 곧장 핸들러로 가지 않고 &lt;strong&gt;DeltaFIFO&lt;/strong&gt;라는 큐를 거친다. DeltaFIFO는 객체(key)별로 변화(delta)를 &lt;strong&gt;순서대로&lt;/strong&gt; 쌓아두는 큐다. 같은 객체에 여러 변화가 빠르게 일어나도 순서가 보장되고, 짧은 시간의 중복 이벤트를 합쳐 처리할 수 있다.&lt;/p&gt;
&lt;p&gt;큐에 쌓인 delta는 Informer의 컨트롤러 루프가 하나씩 꺼내(Pop) 두 가지 일을 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Indexer(로컬 캐시)를 갱신&lt;/strong&gt; — Added/Updated면 저장, Deleted면 제거.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;이벤트 핸들러 호출&lt;/strong&gt; — 등록된 &lt;code dir=&quot;auto&quot;&gt;ResourceEventHandler&lt;/code&gt;(OnAdd/OnUpdate/OnDelete)에 변화를 전달.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;여기서 중요한 순서가 있다. &lt;strong&gt;캐시를 먼저 갱신한 뒤 핸들러를 부른다.&lt;/strong&gt; 그래서 핸들러 안에서 Lister로 캐시를 읽으면, 방금 들어온 변화가 이미 반영된 상태를 볼 수 있다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;indexer와-lister-읽기는-캐시에서&quot;&gt;Indexer와 Lister: 읽기는 캐시에서&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Indexer&lt;/strong&gt;는 단순한 key-value 저장소(ThreadSafeStore)에 &lt;strong&gt;인덱스&lt;/strong&gt;를 더한 것이다. 네임스페이스별, 레이블별처럼 자주 쓰는 기준으로 객체를 빠르게 찾도록 색인을 만들어 둔다.&lt;/p&gt;
&lt;p&gt;컨트롤러가 “현재 상태”를 읽을 때 쓰는 &lt;strong&gt;Lister&lt;/strong&gt;는 이 Indexer를 감싼 읽기 전용 뷰다. 즉 &lt;code dir=&quot;auto&quot;&gt;lister.list()&lt;/code&gt;나 &lt;code dir=&quot;auto&quot;&gt;lister.get(name)&lt;/code&gt;은 &lt;strong&gt;API 서버가 아니라 메모리에서&lt;/strong&gt; 응답한다. 직접 watch를 쓸 때 “전체가 필요하면 또 List를 날려야 했던” 문제를 해결한다.&lt;/p&gt;
&lt;p&gt;또 Informer는 주기적으로 &lt;strong&gt;resync&lt;/strong&gt;를 수행하는데, 이때도 API 서버를 부르지 않는다. 로컬 캐시에 있는 객체들에 대해 Sync delta를 다시 흘려보내 핸들러를 한 번씩 더 호출할 뿐이다. “혹시 놓친 reconcile이 있으면 다시 맞춰라”는 안전장치이고, 비용은 전부 로컬에서 치른다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;sharedinformer-하나의-watch를-나눠-쓴다&quot;&gt;SharedInformer: 하나의 watch를 나눠 쓴다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;마지막 조각이 &lt;strong&gt;SharedInformer&lt;/strong&gt;다. 같은 리소스(예: Pod)를 여러 컨트롤러가 본다면, informer를 각자 만들 필요 없이 &lt;strong&gt;하나의 informer(= 하나의 List+Watch, 하나의 캐시)를 공유&lt;/strong&gt;하고, 이벤트만 각 리스너에게 나눠준다.&lt;/p&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;DESCRIPTION&quot; height=&quot;314px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 579 314&quot; width=&quot;579px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;!--entity API--&gt;&lt;g data-qualified-name=&quot;API&quot; data-source-line=&quot;3&quot; id=&quot;ent0001&quot;&gt;&lt;polygon fill=&quot;#F1F1F1&quot; points=&quot;209,16,219,6,321.5088,6,321.5088,42.2969,311.5088,52.2969,209,52.2969,209,16&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;311.5088&quot; x2=&quot;321.5088&quot; y1=&quot;16&quot; y2=&quot;6&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;209&quot; x2=&quot;311.5088&quot; y1=&quot;16&quot; y2=&quot;16&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;311.5088&quot; x2=&quot;311.5088&quot; y1=&quot;16&quot; y2=&quot;52.2969&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;224&quot; y=&quot;38.9951&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;!--entity S--&gt;&lt;g data-qualified-name=&quot;S&quot; data-source-line=&quot;4&quot; id=&quot;ent0002&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;52.5938&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;167.9912&quot; x=&quot;181.5&quot; y=&quot;129&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;109.8535&quot; x=&quot;191.5&quot; y=&quot;151.9951&quot;&gt;SharedInformer&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;147.9912&quot; x=&quot;191.5&quot; y=&quot;168.292&quot;&gt;(Reflector + Indexer)&lt;/text&gt;&lt;/g&gt;&lt;!--entity A--&gt;&lt;g data-qualified-name=&quot;A&quot; data-source-line=&quot;5&quot; id=&quot;ent0003&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;103.2002&quot; x=&quot;7&quot; y=&quot;261&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.2002&quot; x=&quot;17&quot; y=&quot;283.9951&quot;&gt;Controller A&lt;/text&gt;&lt;/g&gt;&lt;!--entity B--&gt;&lt;g data-qualified-name=&quot;B&quot; data-source-line=&quot;6&quot; id=&quot;ent0004&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;103.2275&quot; x=&quot;145&quot; y=&quot;261&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.2275&quot; x=&quot;155&quot; y=&quot;283.9951&quot;&gt;Controller B&lt;/text&gt;&lt;/g&gt;&lt;!--entity C--&gt;&lt;g data-qualified-name=&quot;C&quot; data-source-line=&quot;7&quot; id=&quot;ent0005&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;103.3984&quot; x=&quot;283&quot; y=&quot;261&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.3984&quot; x=&quot;293&quot; y=&quot;283.9951&quot;&gt;Controller C&lt;/text&gt;&lt;/g&gt;&lt;g data-qualified-name=&quot;GMN10&quot; data-source-line=&quot;15&quot; id=&quot;ent0011&quot;&gt;&lt;path d=&quot;M421,259 L421,299.2656 A0,0 0 0 0 421,299.2656 L564.0527,299.2656 A0,0 0 0 0 564.0527,299.2656 L564.0527,269 L554.0527,259 L460.96,259 L313.36,182.12 L452.96,259 L421,259 A0,0 0 0 0 421,259&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M554.0527,259 L554.0527,269 L564.0527,269 L554.0527,259&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;122.0527&quot; x=&quot;427&quot; y=&quot;276.0669&quot;&gt;watch 1개 + 캐시 1개를&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;70.6748&quot; x=&quot;427&quot; y=&quot;291.1997&quot;&gt;세 컨트롤러가 공유&lt;/text&gt;&lt;/g&gt;&lt;!--link API to S--&gt;&lt;g data-entity-1=&quot;ent0001&quot; data-entity-2=&quot;ent0002&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;9&quot; id=&quot;lnk6&quot;&gt;&lt;path d=&quot;M265.5,52.26 C265.5,73.66 265.5,101 265.5,123.56&quot; fill=&quot;none&quot; id=&quot;API-to-S&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;265.5,128.56,269.5,119.56,265.5,123.56,261.5,119.56,265.5,128.56&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;89.1655&quot; x=&quot;266.5&quot; y=&quot;95.0669&quot;&gt;watch (연결 1개)&lt;/text&gt;&lt;/g&gt;&lt;!--link S to A--&gt;&lt;g data-entity-1=&quot;ent0002&quot; data-entity-2=&quot;ent0003&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;10&quot; id=&quot;lnk7&quot;&gt;&lt;path d=&quot;M221.85,182.12 C181.52,205.79 127.0318,237.7685 92.0818,258.2885&quot; fill=&quot;none&quot; id=&quot;S-to-A&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;87.77,260.82,97.5564,259.7126,92.0818,258.2885,93.5059,252.8138,87.77,260.82&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;43.1387&quot; x=&quot;169.5&quot; y=&quot;225.0669&quot;&gt;이벤트 분배&lt;/text&gt;&lt;/g&gt;&lt;!--link S to B--&gt;&lt;g data-entity-1=&quot;ent0002&quot; data-entity-2=&quot;ent0004&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;11&quot; id=&quot;lnk8&quot;&gt;&lt;path d=&quot;M250.87,182.27 C237.42,205.95 220.3299,236.0527 208.6999,256.5227&quot; fill=&quot;none&quot; id=&quot;S-to-B&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;206.23,260.87,214.1538,255.0207,208.6999,256.5227,207.198,251.0688,206.23,260.87&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;43.1387&quot; x=&quot;234.5&quot; y=&quot;225.0669&quot;&gt;이벤트 분배&lt;/text&gt;&lt;/g&gt;&lt;!--link S to C--&gt;&lt;g data-entity-1=&quot;ent0002&quot; data-entity-2=&quot;ent0005&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;12&quot; id=&quot;lnk9&quot;&gt;&lt;path d=&quot;M280.13,182.27 C293.58,205.95 310.6701,236.0527 322.3001,256.5227&quot; fill=&quot;none&quot; id=&quot;S-to-C&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;324.77,260.87,323.802,251.0688,322.3001,256.5227,316.8462,255.0207,324.77,260.87&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;43.1387&quot; x=&quot;306.5&quot; y=&quot;225.0669&quot;&gt;이벤트 분배&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src TT2zJiCm50NWtKznbAaLwi3AW9HaoeRmocAI6uf8xCgr-DaOEi72nX84I4LYOB3AZohWYMBp3ZYXGWnqlFfqplNrzEXGJeGMDN9LJvGaQRWvgmYQSYFaGKMCILKGedWd0oTzJ3g2c2ACx3U2Y8-5fYAJfT8rwJqvsAMo2XLAOmEPBEYKzB0Fyh-nL4cZLLMHHlnJkqQJNfCrclQQCjOzSZJQ1iSMJeJ9nnZuQzikB3PRsmmPxpKEwczUt6B-TR64UvivQrUKhAVqFmgVOmZxoXXLGvNXEgmMTml3xVxzobys_UGUFeFwsHB-up5qkFkvktrgxJdQvrVVt36I1Rg-Rm00?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;p&gt;앞의 “컨트롤러마다 watch 3개” 그림과 비교하면 차이가 분명하다. &lt;strong&gt;연결도 1개, 캐시도 1개&lt;/strong&gt;이다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;informer가-api-서버-부하를-줄이는-원리&quot;&gt;Informer가 API 서버 부하를 줄이는 원리&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;정리하면 Informer가 API 서버 부하를 줄이는 방식은 세 가지다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;폴링이 아니라 List 1회 + Watch 스트림.&lt;/strong&gt; 초기 한 번만 전체를 받고, 그 뒤로는 변화분만 흘러온다. “1초마다 전체 List”같은 폴링 트래픽이 사라진다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;읽기는 전부 로컬 캐시(Lister).&lt;/strong&gt; reconcile 도중 현재 상태를 아무리 자주 조회해도 API 호출은 0이다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SharedInformer로 연결·캐시 공유.&lt;/strong&gt; 한 리소스를 N개 컨트롤러가 봐도 watch는 1개다. resync도 로컬이라 추가 호출이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;SEQUENCE&quot; height=&quot;428px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 375 428&quot; width=&quot;375px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;C&quot; data-source-line=&quot;1&quot; id=&quot;part1-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;Controller&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;322.1953&quot; width=&quot;8&quot; x=&quot;42.5864&quot; y=&quot;52.5938&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;46&quot; x2=&quot;46&quot; y1=&quot;52.5938&quot; y2=&quot;374.7891&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;INF&quot; data-source-line=&quot;2&quot; id=&quot;part2-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;Informer&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;322.1953&quot; width=&quot;8&quot; x=&quot;148.8945&quot; y=&quot;52.5938&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;152.1729&quot; x2=&quot;152.1729&quot; y1=&quot;52.5938&quot; y2=&quot;374.7891&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;3&quot; id=&quot;part3-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;API Server&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;322.1953&quot; width=&quot;8&quot; x=&quot;259.7178&quot; y=&quot;52.5938&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;263.4634&quot; x2=&quot;263.4634&quot; y1=&quot;52.5938&quot; y2=&quot;374.7891&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;C&quot; data-source-line=&quot;1&quot; id=&quot;part1-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;83.1729&quot; x=&quot;5&quot; y=&quot;21.2969&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;69.1729&quot; x=&quot;12&quot; y=&quot;41.292&quot;&gt;Controller&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;C&quot; data-source-line=&quot;1&quot; id=&quot;part1-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;83.1729&quot; x=&quot;5&quot; y=&quot;373.7891&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;69.1729&quot; x=&quot;12&quot; y=&quot;393.7842&quot;&gt;Controller&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;INF&quot; data-source-line=&quot;2&quot; id=&quot;part2-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;46.5938&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;109.4434&quot; x=&quot;98.1729&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;60.2588&quot; x=&quot;122.7651&quot; y=&quot;24.9951&quot;&gt;Informer&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;95.4434&quot; x=&quot;105.1729&quot; y=&quot;41.292&quot;&gt;(Local Cache)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;INF&quot; data-source-line=&quot;2&quot; id=&quot;part2-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;46.5938&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;109.4434&quot; x=&quot;98.1729&quot; y=&quot;373.7891&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;60.2588&quot; x=&quot;122.7651&quot; y=&quot;393.7842&quot;&gt;Informer&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;95.4434&quot; x=&quot;105.1729&quot; y=&quot;410.0811&quot;&gt;(Local Cache)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;3&quot; id=&quot;part3-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;86.5088&quot; x=&quot;220.4634&quot; y=&quot;21.2969&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;227.4634&quot; y=&quot;41.292&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;3&quot; id=&quot;part3-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;86.5088&quot; x=&quot;220.4634&quot; y=&quot;373.7891&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;227.4634&quot; y=&quot;393.7842&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;rect fill=&quot;#EEEEEE&quot; height=&quot;3&quot; width=&quot;369.6387&quot; x=&quot;0&quot; y=&quot;83.1602&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;0&quot; x2=&quot;369.6387&quot; y1=&quot;83.1602&quot; y2=&quot;83.1602&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;0&quot; x2=&quot;369.6387&quot; y1=&quot;86.1602&quot; y2=&quot;86.1602&quot;&gt;&lt;/line&gt;&lt;rect fill=&quot;#EEEEEE&quot; height=&quot;23.1328&quot; width=&quot;132.1426&quot; x=&quot;118.748&quot; y=&quot;72.5938&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;113.6167&quot; x=&quot;124.748&quot; y=&quot;88.6606&quot;&gt;직접 watch / 폴링 방식&lt;/text&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part3&quot; data-source-line=&quot;6&quot; id=&quot;msg1&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;251.7178,122.8594,261.7178,126.8594,251.7178,130.8594,255.7178,126.8594&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;46.5864&quot; x2=&quot;257.7178&quot; y1=&quot;126.8594&quot; y2=&quot;126.8594&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;52.603&quot; x=&quot;53.5864&quot; y=&quot;121.7935&quot;&gt;List (반복)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part3&quot; data-source-line=&quot;7&quot; id=&quot;msg2&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;251.7178,151.9922,261.7178,155.9922,251.7178,159.9922,255.7178,155.9922&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;46.5864&quot; x2=&quot;257.7178&quot; y1=&quot;155.9922&quot; y2=&quot;155.9922&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;80.5835&quot; x=&quot;53.5864&quot; y=&quot;150.9263&quot;&gt;Get (읽을 때마다)&lt;/text&gt;&lt;/g&gt;&lt;path d=&quot;M268,168.9922 L268,193.9922 L364,193.9922 L364,178.9922 L354,168.9922 L268,168.9922&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M354,168.9922 L354,178.9922 L364,178.9922 L354,168.9922&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;75.6387&quot; x=&quot;274&quot; y=&quot;186.0591&quot;&gt;읽기마다 API 부하&lt;/text&gt;&lt;rect fill=&quot;#EEEEEE&quot; height=&quot;3&quot; width=&quot;369.6387&quot; x=&quot;0&quot; y=&quot;219.6914&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;0&quot; x2=&quot;369.6387&quot; y1=&quot;219.6914&quot; y2=&quot;219.6914&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;0&quot; x2=&quot;369.6387&quot; y1=&quot;222.6914&quot; y2=&quot;222.6914&quot;&gt;&lt;/line&gt;&lt;rect fill=&quot;#EEEEEE&quot; height=&quot;23.1328&quot; width=&quot;102.5181&quot; x=&quot;133.5603&quot; y=&quot;209.125&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.9922&quot; x=&quot;139.5603&quot; y=&quot;225.1919&quot;&gt;Informer 방식&lt;/text&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;11&quot; id=&quot;msg3&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;140.8945,259.3906,150.8945,263.3906,140.8945,267.3906,144.8945,263.3906&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;46.5864&quot; x2=&quot;146.8945&quot; y1=&quot;263.3906&quot; y2=&quot;263.3906&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;73.3726&quot; x=&quot;53.5864&quot; y=&quot;258.3247&quot;&gt;list() / get()&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part1&quot; data-source-line=&quot;12&quot; id=&quot;msg4&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;57.5864,288.5234,47.5864,292.5234,57.5864,296.5234,53.5864,292.5234&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;51.5864&quot; x2=&quot;151.8945&quot; y1=&quot;292.5234&quot; y2=&quot;292.5234&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;70.6748&quot; x=&quot;63.5864&quot; y=&quot;287.4575&quot;&gt;캐시에서 즉시 응답&lt;/text&gt;&lt;/g&gt;&lt;path d=&quot;M157,305.5234 L157,330.5234 L238,330.5234 L238,315.5234 L228,305.5234 L157,305.5234&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M228,305.5234 L228,315.5234 L238,315.5234 L228,305.5234&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;60.0361&quot; x=&quot;163&quot; y=&quot;322.5903&quot;&gt;API 호출 없음&lt;/text&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part3&quot; data-source-line=&quot;14&quot; id=&quot;msg5&quot;&gt;&lt;polygon fill=&quot;#008000&quot; points=&quot;251.7178,352.7891,261.7178,356.7891,251.7178,360.7891,255.7178,356.7891&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;152.8945&quot; x2=&quot;257.7178&quot; y1=&quot;356.7891&quot; y2=&quot;356.7891&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;86.8232&quot; x=&quot;159.8945&quot; y=&quot;351.7231&quot;&gt;watch 1개만 유지&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src NP31IiD048RlUOgXNf934Qz210aeWIA2H_KGmZO9nDsmNVJQGX4b0Tj3QGu9z206Rm5jAO8lr9syWvkaXThJClD_-_yp4pbS16uGELH0ns9KS1Q6X7V06O2bHVkgJVkCFn1-H_KUSvqGBCVrYT6WzjN5V_ZyseORmX-tLghKDDC4B4QucCAJ8rmVZg5w-vR526JvYN4EfgbPq3shOJY5NZ0GeCiobLzBOr-u9AgF-I_cOv2pH1OlCdutDCe40HvulW3MtwAAsgpBbcXQSZMiPcapocwTmtIrYlePgdJTK2DwHBrQtUmgqQfDVoOOPpYVu3W3_7XLXGgQodXvC4FhLCTMwHfNYfe_OvwqPhT77YU4tk-sQazoiYapMII0sGABuHy0?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;p&gt;결과적으로 클러스터에 컨트롤러가 수십 개 떠 있어도, API 서버 입장에서는 &lt;strong&gt;리소스 종류별 watch 몇 개&lt;/strong&gt;만 감당하면 된다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;api-서버의-watch-cache가-etcd-부하를-줄이는-원리&quot;&gt;API 서버의 watch cache가 etcd 부하를 줄이는 원리&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;여기서 한 단계 더 내려가 보자. 컨트롤러들의 watch를 API 서버가 받아주는 건 알겠는데, &lt;strong&gt;그 API 서버는 etcd를 어떻게 감당할까?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;만약 API 서버가 클라이언트 watch 하나하나를 그대로 etcd watch로 옮겼다면, 클라이언트가 1000개면 etcd watch도 1000개가 됐을 것이다. 실제로는 그렇지 않다. API 서버 내부에는 &lt;strong&gt;watch cache(cacher)&lt;/strong&gt; 라는, Informer와 똑 닮은 구조가 들어 있기 때문이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 서버는 리소스 종류별로 &lt;strong&gt;etcd에 watch를 단 하나&lt;/strong&gt; 건다(storage layer의 cacher).&lt;/li&gt;
&lt;li&gt;그 이벤트를 메모리의 &lt;strong&gt;watch cache(최근 이벤트 링 버퍼 + 현재 상태 저장소)&lt;/strong&gt; 에 쌓는다.&lt;/li&gt;
&lt;li&gt;모든 클라이언트 watch는 etcd가 아니라 이 &lt;strong&gt;watch cache가 직접 서빙&lt;/strong&gt;한다.&lt;/li&gt;
&lt;li&gt;게다가 &lt;code dir=&quot;auto&quot;&gt;resourceVersion=0&lt;/code&gt; 같은 List 요청은 etcd 쿼럼 읽기 대신 &lt;strong&gt;watch cache에서 응답&lt;/strong&gt;할 수 있어, 읽기 부하까지 줄인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉 API 서버 안에도 “Reflector → 캐시 → 다수의 watcher에게 분배”라는 &lt;strong&gt;Informer와 동일한 fan-in 패턴&lt;/strong&gt;이 한 겹 더 있는 것이다.&lt;/p&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;DESCRIPTION&quot; height=&quot;336px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 436 336&quot; width=&quot;436px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;!--cluster API--&gt;&lt;g data-qualified-name=&quot;API&quot; data-source-line=&quot;4&quot; id=&quot;ent0002&quot;&gt;&lt;polygon fill=&quot;none&quot; points=&quot;87,124,97,114,246,114,246,213,236,223,87,223,87,124&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;236&quot; x2=&quot;246&quot; y1=&quot;124&quot; y2=&quot;114&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;87&quot; x2=&quot;236&quot; y1=&quot;124&quot; y2=&quot;124&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;236&quot; x2=&quot;236&quot; y1=&quot;124&quot; y2=&quot;223&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; font-weight=&quot;700&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.1865&quot; x=&quot;120.9067&quot; y=&quot;139.9951&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;!--entity C--&gt;&lt;g data-qualified-name=&quot;API.C&quot; data-source-line=&quot;5&quot; id=&quot;ent0003&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;52.5938&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;118.9229&quot; x=&quot;108.5&quot; y=&quot;154&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;49.2939&quot; x=&quot;118.5&quot; y=&quot;176.9951&quot;&gt;Cacher&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;98.9229&quot; x=&quot;118.5&quot; y=&quot;193.292&quot;&gt;(watch cache)&lt;/text&gt;&lt;/g&gt;&lt;!--entity ETCD--&gt;&lt;g data-qualified-name=&quot;ETCD&quot; data-source-line=&quot;3&quot; id=&quot;ent0001&quot;&gt;&lt;path d=&quot;M142.5,16 C142.5,6 167.8433,6 167.8433,6 C167.8433,6 193.1865,6 193.1865,16 L193.1865,41.2969 C193.1865,51.2969 167.8433,51.2969 167.8433,51.2969 C167.8433,51.2969 142.5,51.2969 142.5,41.2969 L142.5,16&quot; fill=&quot;#F1F1F1&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M142.5,16 C142.5,26 167.8433,26 167.8433,26 C167.8433,26 193.1865,26 193.1865,16&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;30.6865&quot; x=&quot;152.5&quot; y=&quot;42.9951&quot;&gt;etcd&lt;/text&gt;&lt;/g&gt;&lt;!--entity IA--&gt;&lt;g data-qualified-name=&quot;IA&quot; data-source-line=&quot;7&quot; id=&quot;ent0004&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;143.8809&quot; x=&quot;7&quot; y=&quot;286&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;123.8809&quot; x=&quot;17&quot; y=&quot;308.9951&quot;&gt;SharedInformer A&lt;/text&gt;&lt;/g&gt;&lt;!--entity IB--&gt;&lt;g data-qualified-name=&quot;IB&quot; data-source-line=&quot;8&quot; id=&quot;ent0005&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;143.9082&quot; x=&quot;186&quot; y=&quot;286&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;123.9082&quot; x=&quot;196&quot; y=&quot;308.9951&quot;&gt;SharedInformer B&lt;/text&gt;&lt;/g&gt;&lt;g data-qualified-name=&quot;GMN9&quot; data-source-line=&quot;15&quot; id=&quot;ent0010&quot;&gt;&lt;path d=&quot;M263,160.5 L263,176.5 L227.81,180.5 L263,184.5 L263,200.7656 A0,0 0 0 0 263,200.7656 L421.2935,200.7656 A0,0 0 0 0 421.2935,200.7656 L421.2935,170.5 L411.2935,160.5 L263,160.5 A0,0 0 0 0 263,160.5&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M411.2935,160.5 L411.2935,170.5 L421.2935,170.5 L411.2935,160.5&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;137.2935&quot; x=&quot;269&quot; y=&quot;177.5669&quot;&gt;클라이언트 watch가 수천 개여도&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;135.0527&quot; x=&quot;269&quot; y=&quot;192.6997&quot;&gt;etcd watch는 리소스별 1개&lt;/text&gt;&lt;/g&gt;&lt;!--link ETCD to C--&gt;&lt;g data-entity-1=&quot;ent0001&quot; data-entity-2=&quot;ent0003&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;10&quot; id=&quot;lnk6&quot;&gt;&lt;path d=&quot;M168,51.31 C168,78.53 168,120.01 168,148.89&quot; fill=&quot;none&quot; id=&quot;ETCD-to-C&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;168,153.89,172,144.89,168,148.89,164,144.89,168,153.89&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;104.7681&quot; x=&quot;169&quot; y=&quot;94.0669&quot;&gt;watch 1개 (리소스별)&lt;/text&gt;&lt;/g&gt;&lt;!--link C to IA--&gt;&lt;g data-entity-1=&quot;ent0003&quot; data-entity-2=&quot;ent0004&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;11&quot; id=&quot;lnk7&quot;&gt;&lt;path d=&quot;M148.63,207.18 C141.2,217.1 132.67,228.55 125,239 C113.43,254.78 103.3344,268.8301 94.1144,281.7501&quot; fill=&quot;none&quot; id=&quot;C-to-IA&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;91.21,285.82,99.6939,280.8176,94.1144,281.7501,93.182,276.1706,91.21,285.82&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;88.6958&quot; x=&quot;126&quot; y=&quot;252.0669&quot;&gt;watch 서빙 (캐시)&lt;/text&gt;&lt;/g&gt;&lt;!--link C to IB--&gt;&lt;g data-entity-1=&quot;ent0003&quot; data-entity-2=&quot;ent0005&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;12&quot; id=&quot;lnk8&quot;&gt;&lt;path d=&quot;M192.38,207.01 C201.17,216.7 210.91,228.05 219,239 C230.21,254.17 238.7838,268.3349 246.2338,281.5449&quot; fill=&quot;none&quot; id=&quot;C-to-IB&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;248.69,285.9,247.753,276.0958,246.2338,281.5449,240.7848,280.0257,248.69,285.9&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;88.6958&quot; x=&quot;231&quot; y=&quot;252.0669&quot;&gt;watch 서빙 (캐시)&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src VT0_IyD060Rm_Jx5GwPqwE3g82JH8PiGHvVpScsApQLS3aL4K0WYKT11_0Ff9auTIkrGnK_Kktu7BmdKBZg-Tx-xztdV_7WWHbJI52nBHvdWGaNgRCWXELDKz8USa9WgUaHp3eSh5ZkWEVOEWbqYijYUUViX8YvFk6nkwlASuFSxE05b2PU7mZsbYYLWTTbfS40ko0QC4Yfv78fU9bCkuJKczFuXVajyGkf4w7Pt460RRQEjvRI2gzydvlhEb6_we-YGe36XjqQcgFJdArppzM1kgpNm_mHsR6NNC-Wd2bdFZW2ihkPw_6t6S_EqM9MBzk5oUWbpys9c5MmEyppHzuMrzGfReCj7R8PhyX8kOjGTVW00?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;div&gt;&lt;h2 id=&quot;정리-같은-패턴이-계층으로-쌓인다&quot;&gt;정리: 같은 패턴이 계층으로 쌓인다&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;처음의 질문 — “리소스 변화를 어떻게 감지할 것인가” — 에 대한 Kubernetes의 답은 결국 &lt;strong&gt;계층적 캐싱&lt;/strong&gt;이다. 같은 List+Watch 패턴이 두 겹으로 포개져 있다.&lt;/p&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;DESCRIPTION&quot; height=&quot;448px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 262 448&quot; width=&quot;262px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;!--entity ETCD--&gt;&lt;g data-qualified-name=&quot;ETCD&quot; data-source-line=&quot;3&quot; id=&quot;ent0001&quot;&gt;&lt;path d=&quot;M102,16 C102,6 127.3433,6 127.3433,6 C127.3433,6 152.6865,6 152.6865,16 L152.6865,41.2969 C152.6865,51.2969 127.3433,51.2969 127.3433,51.2969 C127.3433,51.2969 102,51.2969 102,41.2969 L102,16&quot; fill=&quot;#F1F1F1&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M102,16 C102,26 127.3433,26 127.3433,26 C127.3433,26 152.6865,26 152.6865,16&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;30.6865&quot; x=&quot;112&quot; y=&quot;42.9951&quot;&gt;etcd&lt;/text&gt;&lt;/g&gt;&lt;!--entity API--&gt;&lt;g data-qualified-name=&quot;API&quot; data-source-line=&quot;4&quot; id=&quot;ent0002&quot;&gt;&lt;polygon fill=&quot;#F1F1F1&quot; points=&quot;58,138,68,128,196.9229,128,196.9229,180.5938,186.9229,190.5938,58,190.5938,58,138&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;186.9229&quot; x2=&quot;196.9229&quot; y1=&quot;138&quot; y2=&quot;128&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;58&quot; x2=&quot;186.9229&quot; y1=&quot;138&quot; y2=&quot;138&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;186.9229&quot; x2=&quot;186.9229&quot; y1=&quot;138&quot; y2=&quot;190.5938&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;73&quot; y=&quot;160.9951&quot;&gt;API Server&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;98.9229&quot; x=&quot;73&quot; y=&quot;177.292&quot;&gt;(watch cache)&lt;/text&gt;&lt;/g&gt;&lt;!--entity INF--&gt;&lt;g data-qualified-name=&quot;INF&quot; data-source-line=&quot;5&quot; id=&quot;ent0003&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;52.5938&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;154.1416&quot; x=&quot;50.5&quot; y=&quot;268&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;81.5117&quot; x=&quot;60.5&quot; y=&quot;290.9951&quot;&gt;Informer 캐시&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;134.1416&quot; x=&quot;60.5&quot; y=&quot;307.292&quot;&gt;(client-go / fabric8)&lt;/text&gt;&lt;/g&gt;&lt;!--entity A--&gt;&lt;g data-qualified-name=&quot;A&quot; data-source-line=&quot;6&quot; id=&quot;ent0004&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;103.2002&quot; x=&quot;7&quot; y=&quot;398&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.2002&quot; x=&quot;17&quot; y=&quot;420.9951&quot;&gt;Controller A&lt;/text&gt;&lt;/g&gt;&lt;!--entity B--&gt;&lt;g data-qualified-name=&quot;B&quot; data-source-line=&quot;7&quot; id=&quot;ent0005&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;103.2275&quot; x=&quot;145&quot; y=&quot;398&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;83.2275&quot; x=&quot;155&quot; y=&quot;420.9951&quot;&gt;Controller B&lt;/text&gt;&lt;/g&gt;&lt;!--reverse link ETCD to API--&gt;&lt;g data-entity-1=&quot;ent0001&quot; data-entity-2=&quot;ent0002&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;9&quot; id=&quot;lnk6&quot;&gt;&lt;path d=&quot;M127.5,56.09 C127.5,77.04 127.5,104.03 127.5,127.65&quot; fill=&quot;none&quot; id=&quot;ETCD-backto-API&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;127.5,51.09,123.5,60.09,127.5,56.09,131.5,60.09,127.5,51.09&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;104.7681&quot; x=&quot;128.5&quot; y=&quot;94.0669&quot;&gt;watch 1개 (리소스별)&lt;/text&gt;&lt;/g&gt;&lt;!--reverse link API to INF--&gt;&lt;g data-entity-1=&quot;ent0002&quot; data-entity-2=&quot;ent0003&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;10&quot; id=&quot;lnk7&quot;&gt;&lt;path d=&quot;M127.5,196.01 C127.5,219.17 127.5,245.67 127.5,267.61&quot; fill=&quot;none&quot; id=&quot;API-backto-INF&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;127.5,191.01,123.5,200.01,127.5,196.01,131.5,200.01,127.5,191.01&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;89.1655&quot; x=&quot;128.5&quot; y=&quot;234.0669&quot;&gt;watch 1개 (공유)&lt;/text&gt;&lt;/g&gt;&lt;!--reverse link INF to A--&gt;&lt;g data-entity-1=&quot;ent0003&quot; data-entity-2=&quot;ent0004&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;11&quot; id=&quot;lnk8&quot;&gt;&lt;path d=&quot;M94.1732,324.7141 C85.6032,333.4241 80.48,340.15 74.5,351 C66.43,365.64 62.36,384.4 60.35,397.82&quot; fill=&quot;none&quot; id=&quot;INF-backto-A&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;97.68,321.15,88.5166,324.7599,94.1732,324.7141,94.2191,330.3707,97.68,321.15&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;77.8984&quot; x=&quot;75.5&quot; y=&quot;364.0669&quot;&gt;캐시 읽기 + 이벤트&lt;/text&gt;&lt;/g&gt;&lt;!--reverse link INF to B--&gt;&lt;g data-entity-1=&quot;ent0003&quot; data-entity-2=&quot;ent0005&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;12&quot; id=&quot;lnk9&quot;&gt;&lt;path d=&quot;M144.801,325.4696 C158.191,348.6496 175.02,377.8 186.61,397.88&quot; fill=&quot;none&quot; id=&quot;INF-backto-B&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;142.3,321.14,143.3381,330.934,144.801,325.4696,150.2654,326.9324,142.3,321.14&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;77.8984&quot; x=&quot;168.5&quot; y=&quot;364.0669&quot;&gt;캐시 읽기 + 이벤트&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src VOunIyD068Rt-nKVcHeaI3SHbwOgP2c5EhfylNnjWvUxSZagxk9G7Vm14HoAaqEn2aNe7qhE_-1T0aK4tOxlUTxttk8gatFKc0DN-Ln9acPaRWI19cvGJWKnbgB1CHO40HcU1e05d5tqJvbKgRlrXWcCI2z8NyhEDHe-0uvyHc4ZEihspH0aSg9qJXhivwEzBrs2YyoHQAhW42OurXa_QfF9uFndig-aqKe8b-srpN_GkA4nOtuZd4IHtm37q4xhLkiIElNBgxrxiCjLlRaDcUVUSp_-yghDXosVG-P9q-HuknpiqwxQhk70FTxhjzNNShktul-iRm00?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;컨트롤러 ↔ Informer&lt;/strong&gt;: 수많은 reconcile의 읽기를 로컬 캐시가 받아내고, API 서버에는 watch 1개만 남긴다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Informer ↔ API 서버 watch cache&lt;/strong&gt;: 수많은 클라이언트 watch를 watch cache가 받아내고, etcd에는 watch 1개만 남긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 계층이 위층의 watch를 &lt;strong&gt;fan-in으로 합쳐&lt;/strong&gt; 아래층 부하를 줄인다. 직접 watch를 걸던 처음 코드에서 떠안아야 했던 재연결·캐시·연결 공유 문제는, 사실 Kubernetes가 클러스터 전체 규모에서 이미 풀어둔 문제였던 셈이다.&lt;/p&gt;</content:encoded><category>Kubernetes</category><category>Informer</category><category>client-go</category><category>Controller</category></item><item><title>Kubernetes Informer를 쓸 때 주의할 점</title><link>https://ycra-dev.github.io/blog/informer-pitfalls/</link><guid isPermaLink="true">https://ycra-dev.github.io/blog/informer-pitfalls/</guid><description>Informer는 &apos;List+Watch+캐시&apos;를 대신 해주지만 몇 가지 가정을 강요한다. 캐시를 직접 고치지 말 것, 모든 이벤트를 본다고 믿지 말 것 — 틀리기 쉬운 지점들을 정리해보았다.

</description><pubDate>Sun, 21 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://ycra-dev.github.io/blog/kubernetes-informer/&quot;&gt;이전 글&lt;/a&gt;에서는 Informer가 List+Watch와 로컬 캐시로 어떻게 API 서버, etcd 부하를 줄이는지 살펴보았다. 그런데 Informer를 사용할 때 주의할 점이 있다. Informer는 &lt;strong&gt;“이렇게 써야만 안전하다”는 몇 가지 가정&lt;/strong&gt;을 포함하고 있다. 그 가정을 간과하면 코드는 평소엔 잘 돌다가, 연결이 한 번 끊기거나 부하가 몰리는 순간에 &lt;strong&gt;소리 없이 어긋날 수 있다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;이 글은 Informer 기반 컨트롤러를 짤 때 틀리기 쉬운 지점들을 모아봤다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;1-모든-이벤트를-본다고-믿지-마라-순서유실&quot;&gt;1. 모든 이벤트를 본다고 믿지 마라 (순서·유실)&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Informer는 “발생한 모든 변화 이벤트”를 보장하지 않는다.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;순서는-누가-보장하나&quot;&gt;순서는 누가 보장하나&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;watch 스트림의 순서는 Informer가 만드는 게 아니라 &lt;strong&gt;API 서버(궁극적으로 etcd)&lt;/strong&gt; 가 보장한다. etcd는 단일 일관 로그라 모든 변경에 단조 증가하는 revision이 붙고, 이게 &lt;code dir=&quot;auto&quot;&gt;resourceVersion&lt;/code&gt;으로 노출된다. API 서버의 watch cache가 이 순서를 보존해 여러 watcher에게 똑같은 순서로 보낸다. Reflector는 &lt;strong&gt;받은 순서 그대로&lt;/strong&gt; DeltaFIFO에 넣을 뿐이다.&lt;/p&gt;
&lt;p&gt;단, 여기서 “순서”는 &lt;strong&gt;단일 watch 스트림 안에서 이벤트가 도착하는 순서 그 자체&lt;/strong&gt;를 뜻한다. 순서를 보장하는 건 스트림이지, &lt;code dir=&quot;auto&quot;&gt;resourceVersion&lt;/code&gt; 값을 비교해서 얻는 게 아니다. 서로 다른 리소스 종류(Pod와 Node 등) 사이의 이벤트 순서 역시 아무것도 보장되지 않는다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;끊긴-사이의-이벤트는-유실된다&quot;&gt;끊긴 사이의 이벤트는 유실된다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;API 서버의 watch cache는 &lt;strong&gt;고정 크기 링 버퍼&lt;/strong&gt;다. watcher가 느리거나 연결이 끊겨서 재개 시 요청한 &lt;code dir=&quot;auto&quot;&gt;resourceVersion&lt;/code&gt;이 버퍼의 가장 오래된 항목보다 더 옛날이면, API 서버는 &lt;code dir=&quot;auto&quot;&gt;410 Gone&lt;/code&gt;(“too old resource version”)을 돌려준다. 그러면 Reflector는 &lt;strong&gt;전체를 다시 List(relist)&lt;/strong&gt; 해서 최신 스냅샷 + 새 &lt;code dir=&quot;auto&quot;&gt;resourceVersion&lt;/code&gt;을 확보한 뒤 watch를 재개한다.&lt;/p&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;SEQUENCE&quot; height=&quot;392px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 586 392&quot; width=&quot;586px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;1&quot; id=&quot;part1-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;API Server&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;318.4609&quot; width=&quot;8&quot; x=&quot;44.2544&quot; y=&quot;36.2969&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;48&quot; x2=&quot;48&quot; y1=&quot;36.2969&quot; y2=&quot;354.7578&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;R&quot; data-source-line=&quot;2&quot; id=&quot;part2-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;Reflector&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;318.4609&quot; width=&quot;8&quot; x=&quot;207.9155&quot; y=&quot;36.2969&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;211.2754&quot; x2=&quot;211.2754&quot; y1=&quot;36.2969&quot; y2=&quot;354.7578&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;F&quot; data-source-line=&quot;3&quot; id=&quot;part3-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;DeltaFIFO&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;318.4609&quot; width=&quot;8&quot; x=&quot;363.5215&quot; y=&quot;36.2969&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;367.2188&quot; x2=&quot;367.2188&quot; y1=&quot;36.2969&quot; y2=&quot;354.7578&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part4&quot; data-qualified-name=&quot;I&quot; data-source-line=&quot;4&quot; id=&quot;part4-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;Indexer&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;318.4609&quot; width=&quot;8&quot; x=&quot;538.145&quot; y=&quot;36.2969&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;541.5669&quot; x2=&quot;541.5669&quot; y1=&quot;36.2969&quot; y2=&quot;354.7578&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;1&quot; id=&quot;part1-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;86.5088&quot; x=&quot;5&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;12&quot; y=&quot;24.9951&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;1&quot; id=&quot;part1-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;86.5088&quot; x=&quot;5&quot; y=&quot;353.7578&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;12&quot; y=&quot;373.7529&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;R&quot; data-source-line=&quot;2&quot; id=&quot;part2-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;77.2803&quot; x=&quot;173.2754&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;63.2803&quot; x=&quot;180.2754&quot; y=&quot;24.9951&quot;&gt;Reflector&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;R&quot; data-source-line=&quot;2&quot; id=&quot;part2-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;77.2803&quot; x=&quot;173.2754&quot; y=&quot;353.7578&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;63.2803&quot; x=&quot;180.2754&quot; y=&quot;373.7529&quot;&gt;Reflector&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;F&quot; data-source-line=&quot;3&quot; id=&quot;part3-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;82.6055&quot; x=&quot;326.2188&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;68.6055&quot; x=&quot;333.2188&quot; y=&quot;24.9951&quot;&gt;DeltaFIFO&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;F&quot; data-source-line=&quot;3&quot; id=&quot;part3-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;82.6055&quot; x=&quot;326.2188&quot; y=&quot;353.7578&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;68.6055&quot; x=&quot;333.2188&quot; y=&quot;373.7529&quot;&gt;DeltaFIFO&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part4&quot; data-qualified-name=&quot;I&quot; data-source-line=&quot;4&quot; id=&quot;part4-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;67.1563&quot; x=&quot;508.5669&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;53.1563&quot; x=&quot;515.5669&quot; y=&quot;24.9951&quot;&gt;Indexer&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part4&quot; data-qualified-name=&quot;I&quot; data-source-line=&quot;4&quot; id=&quot;part4-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;67.1563&quot; x=&quot;508.5669&quot; y=&quot;353.7578&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;53.1563&quot; x=&quot;515.5669&quot; y=&quot;373.7529&quot;&gt;Indexer&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part1&quot; data-source-line=&quot;6&quot; id=&quot;msg1&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;59.2544,63.4297,49.2544,67.4297,59.2544,71.4297,55.2544,67.4297&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;53.2544&quot; x2=&quot;210.9155&quot; y1=&quot;67.4297&quot; y2=&quot;67.4297&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;100.1914&quot; x=&quot;65.2544&quot; y=&quot;62.3638&quot;&gt;Watch(rv=100)&lt;/text&gt;&lt;/g&gt;&lt;path d=&quot;M53,80.4297 L53,135.4297 L189,135.4297 L189,90.4297 L179,80.4297 L53,80.4297&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M179,80.4297 L179,90.4297 L189,90.4297 L179,80.4297&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;62.8735&quot; x=&quot;59&quot; y=&quot;97.4966&quot;&gt;연결이 끊긴 사이&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;115.5718&quot; x=&quot;59&quot; y=&quot;112.6294&quot;&gt;Pod B가 생성됐다가 삭제됨&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;102.8574&quot; x=&quot;59&quot; y=&quot;127.7622&quot;&gt;(rv=120 → 121)&lt;/text&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;12&quot; id=&quot;msg2&quot;&gt;&lt;line x1=&quot;193.9155&quot; x2=&quot;203.9155&quot; y1=&quot;156.9609&quot; y2=&quot;166.9609&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;193.9155&quot; x2=&quot;203.9155&quot; y1=&quot;166.9609&quot; y2=&quot;156.9609&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;48.2544&quot; x2=&quot;198.9155&quot; y1=&quot;161.9609&quot; y2=&quot;161.9609&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;35.3374&quot; x=&quot;55.2544&quot; y=&quot;156.895&quot;&gt;연결 종료&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part1&quot; data-source-line=&quot;13&quot; id=&quot;msg3&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;59.2544,187.0938,49.2544,191.0938,59.2544,195.0938,55.2544,191.0938&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;53.2544&quot; x2=&quot;210.9155&quot; y1=&quot;191.0938&quot; y2=&quot;191.0938&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;139.6611&quot; x=&quot;65.2544&quot; y=&quot;186.0278&quot;&gt;Watch(rv=100) 재개 시도&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;14&quot; id=&quot;msg4&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;199.9155,216.2266,209.9155,220.2266,199.9155,224.2266,203.9155,220.2266&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;48.2544&quot; x2=&quot;205.9155&quot; y1=&quot;220.2266&quot; y2=&quot;220.2266&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;137.7949&quot; x=&quot;55.2544&quot; y=&quot;215.1606&quot;&gt;410 Gone (너무 오래된 rv)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part1&quot; data-source-line=&quot;15&quot; id=&quot;msg5&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;59.2544,245.3594,49.2544,249.3594,59.2544,253.3594,55.2544,249.3594&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;53.2544&quot; x2=&quot;210.9155&quot; y1=&quot;249.3594&quot; y2=&quot;249.3594&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;69.437&quot; x=&quot;65.2544&quot; y=&quot;244.2935&quot;&gt;List (relist)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;16&quot; id=&quot;msg6&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;199.9155,274.4922,209.9155,278.4922,199.9155,282.4922,203.9155,278.4922&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;48.2544&quot; x2=&quot;205.9155&quot; y1=&quot;278.4922&quot; y2=&quot;278.4922&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;122.9731&quot; x=&quot;55.2544&quot; y=&quot;273.4263&quot;&gt;현재 객체 전체 + rv=130&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part3&quot; data-source-line=&quot;17&quot; id=&quot;msg7&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;355.5215,303.625,365.5215,307.625,355.5215,311.625,359.5215,307.625&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;211.9155&quot; x2=&quot;361.5215&quot; y1=&quot;307.625&quot; y2=&quot;307.625&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;131.606&quot; x=&quot;218.9155&quot; y=&quot;302.5591&quot;&gt;Replace([A, C], 130)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part3&quot; data-entity-2=&quot;part4&quot; data-source-line=&quot;18&quot; id=&quot;msg8&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;530.145,332.7578,540.145,336.7578,530.145,340.7578,534.145,336.7578&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;367.5215&quot; x2=&quot;536.145&quot; y1=&quot;336.7578&quot; y2=&quot;336.7578&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;150.6235&quot; x=&quot;374.5215&quot; y=&quot;331.6919&quot;&gt;set-difference로 B 삭제 감지&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src TP2nIiDG58RtFCMXKuCD9DL9iD0gaO1WYOE3E8Jan0P24j9BwLXh5B62mSKEQQbQqA52B1ry7iVSwpjuarPe1wV3uJjy_yy9Z90ufXCO7e5IlQd12OOT34jWj86lGh3ETRHTD8c_nFecF42N6Agc7XTGtOIQPs5tfTK4GGUfbkjX5quDOhRAOMTFaMLHy7o243eNBGA-NHG0O4zfDa_PQ07qyYxxMW3hp_Z6ITEte96bFM1N8nPzqZYcWscnzp_O9A7nEpygt5KPlcyUGQagee2U1NcEa1UGf5eNTDvZcGBivPg-tlzR4DXubgK9i4527w8_Go7OKMGuz3s4CerkwOpN74xfU4ZZ1CAEk6uySjg4rqANJt73yJECU01awOZDkM0IvMCByl1jUQbG-PcEWMkOM3whLs3_l08SYeAQqrpVHY9PZcrZY9w9z3c1nkeXN1opjzul?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;p&gt;결과적으로 로컬 캐시의 &lt;strong&gt;정합성은 회복&lt;/strong&gt;되지만, &lt;strong&gt;두 List 사이의 개별 transition은 못 본다.&lt;/strong&gt; 위 그림처럼 끊긴 사이에 Pod B가 생겼다 사라지면, B의 &lt;strong&gt;존재 자체를 통째로 놓친다.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;그래서-컨트롤러는-level-triggered여야-한다&quot;&gt;그래서 컨트롤러는 level-triggered여야 한다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;이 때문에 Informer 기반 컨트롤러의 제1원칙은 &lt;strong&gt;“모든 이벤트를 본다고 가정하지 마라”&lt;/strong&gt; 다. 이벤트마다 반응하는 edge-triggered가 아니라, &lt;strong&gt;관측된 현재 상태에서 desired 상태로 수렴(idempotent reconcile)&lt;/strong&gt; 하는 level-triggered로 짜야 한다. 핸들러는 “무엇이 바뀌었나”가 아니라 “지금 상태가 어떤가”를 기준으로 동작해야 한다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;relist가-놓친-삭제를-메우는-법-tombstone&quot;&gt;relist가 놓친 삭제를 메우는 법: tombstone&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;위 그림의 “set-difference로 B 삭제 감지”가 핵심이다. &lt;code dir=&quot;auto&quot;&gt;DeltaFIFO.Replace()&lt;/code&gt;는 새 List를 단순 upsert만 하지 않는다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;새 List의 key 집합을 만든다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;현재 store에는 있는데 새 List엔 없는 key&lt;/strong&gt;(= 끊긴 사이 삭제된 것)를 찾는다.&lt;/li&gt;
&lt;li&gt;그 key에 대해 &lt;code dir=&quot;auto&quot;&gt;Deleted&lt;/code&gt; delta를 합성하되, 삭제된 객체의 최종 상태를 모르므로 &lt;strong&gt;마지막으로 알던 상태를 &lt;code dir=&quot;auto&quot;&gt;DeletedFinalStateUnknown&lt;/code&gt;(통칭 tombstone)으로 감싸서&lt;/strong&gt; 큐에 넣는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그래서 &lt;strong&gt;삭제 핸들러에 들어오는 객체는 진짜 타입이 아니라 tombstone일 수 있다.&lt;/strong&gt; 이걸 처리 안 하면 타입 에러가 나거나 삭제가 무시될 수 있다.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;func&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;onDelete&lt;/span&gt;&lt;span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;obj&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;interface&lt;/span&gt;&lt;span&gt;{}) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;obj&lt;/span&gt;&lt;span&gt;.(&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;v1.Pod)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;tombstone&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;obj&lt;/span&gt;&lt;span&gt;.(cache.DeletedFinalStateUnknown)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// 알 수 없는 타입&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;tombstone&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Obj&lt;/span&gt;&lt;span&gt;.(&lt;/span&gt;&lt;span&gt;*&lt;/span&gt;&lt;span&gt;v1.Pod) &lt;/span&gt;&lt;span&gt;// 마지막으로 알던 상태&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;ok&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;            &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;        &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;// pod의 key 정도만 신뢰 (최신 상태는 아닐 수 있음)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;fabric8의 &lt;code dir=&quot;auto&quot;&gt;onDelete(Pod pod, boolean finalStateUnknown)&lt;/code&gt;에서 두 번째 인자 &lt;code dir=&quot;auto&quot;&gt;finalStateUnknown&lt;/code&gt;이 바로 이 tombstone 여부다. &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt;면 “watch가 놓친 삭제를 relist로 추론한 것”이라는 신호다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;2-캐시를-직접-수정하지-마라&quot;&gt;2. 캐시를 직접 수정하지 마라&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Lister·Indexer가 돌려주는 객체는 &lt;strong&gt;캐시 안의 객체를 그대로(포인터로)&lt;/strong&gt; 준 것이다. 복사본이 아니다.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;_&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;lister&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Pods&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;Get&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;my-pod&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Status&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Phase&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Running&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// 절대 금지: 공유 캐시를 오염시킨다&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;이렇게 하면 &lt;strong&gt;다른 모든 핸들러·컨트롤러가 보는 캐시를 함께 바꿔버리고&lt;/strong&gt;, 동시에 읽는 쪽과 data race가 난다. 수정이 필요하면 반드시 먼저 복사한다.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;:=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;original&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;DeepCopy&lt;/span&gt;&lt;span&gt;()&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Status&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Phase&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;Running&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// 복사본을 수정&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;client&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;CoreV1&lt;/span&gt;&lt;span&gt;().&lt;/span&gt;&lt;span&gt;Pods&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;default&lt;/span&gt;&lt;span&gt;&quot;&lt;/span&gt;&lt;span&gt;).&lt;/span&gt;&lt;span&gt;UpdateStatus&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;ctx&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;pod&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;...&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;Informer 버그의 상당수가 여기서 나온다. &lt;strong&gt;“읽은 객체는 내 것이 아니다”&lt;/strong&gt; 를 항상 기억하자.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;3-캐시는-항상-조금-늦다-read-after-write&quot;&gt;3. 캐시는 항상 조금 늦다 (read-after-write)&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Lister는 로컬 캐시를 읽는다. 그리고 캐시는 watch를 통해 채워지므로 &lt;strong&gt;etcd보다 항상 조금 뒤처져 있다.&lt;/strong&gt; 방금 내가 &lt;code dir=&quot;auto&quot;&gt;Create()&lt;/code&gt;한 객체가 Lister에는 아직 안 보일 수 있다는 뜻이다.&lt;/p&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;SEQUENCE&quot; height=&quot;295px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 347 295&quot; width=&quot;347px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;C&quot; data-source-line=&quot;1&quot; id=&quot;part1-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;Controller&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;221.9297&quot; width=&quot;8&quot; x=&quot;42.5864&quot; y=&quot;36.2969&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;46&quot; x2=&quot;46&quot; y1=&quot;36.2969&quot; y2=&quot;258.2266&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;2&quot; id=&quot;part2-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;API Server&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;221.9297&quot; width=&quot;8&quot; x=&quot;157.2944&quot; y=&quot;36.2969&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;161.04&quot; x2=&quot;161.04&quot; y1=&quot;36.2969&quot; y2=&quot;258.2266&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;CACHE&quot; data-source-line=&quot;3&quot; id=&quot;part3-lifeline&quot;&gt;&lt;g&gt;&lt;title&gt;Informer Cache&lt;/title&gt;&lt;rect fill=&quot;#000000&quot; fill-opacity=&quot;0&quot; height=&quot;221.9297&quot; width=&quot;8&quot; x=&quot;271.6724&quot; y=&quot;36.2969&quot;&gt;&lt;/rect&gt;&lt;line x1=&quot;275.5488&quot; x2=&quot;275.5488&quot; y1=&quot;36.2969&quot; y2=&quot;258.2266&quot;&gt;&lt;/line&gt;&lt;/g&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;C&quot; data-source-line=&quot;1&quot; id=&quot;part1-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;83.1729&quot; x=&quot;5&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;69.1729&quot; x=&quot;12&quot; y=&quot;24.9951&quot;&gt;Controller&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part1&quot; data-qualified-name=&quot;C&quot; data-source-line=&quot;1&quot; id=&quot;part1-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;83.1729&quot; x=&quot;5&quot; y=&quot;257.2266&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;69.1729&quot; x=&quot;12&quot; y=&quot;277.2217&quot;&gt;Controller&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;2&quot; id=&quot;part2-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;86.5088&quot; x=&quot;118.04&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;125.04&quot; y=&quot;24.9951&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part2&quot; data-qualified-name=&quot;API&quot; data-source-line=&quot;2&quot; id=&quot;part2-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;86.5088&quot; x=&quot;118.04&quot; y=&quot;257.2266&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;125.04&quot; y=&quot;277.2217&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;CACHE&quot; data-source-line=&quot;3&quot; id=&quot;part3-head&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;122.2471&quot; x=&quot;214.5488&quot; y=&quot;5&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;108.2471&quot; x=&quot;221.5488&quot; y=&quot;24.9951&quot;&gt;Informer Cache&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-uid=&quot;part3&quot; data-qualified-name=&quot;CACHE&quot; data-source-line=&quot;3&quot; id=&quot;part3-tail&quot;&gt;&lt;rect fill=&quot;#E2E2F0&quot; height=&quot;30.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;122.2471&quot; x=&quot;214.5488&quot; y=&quot;257.2266&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;108.2471&quot; x=&quot;221.5488&quot; y=&quot;277.2217&quot;&gt;Informer Cache&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part2&quot; data-source-line=&quot;5&quot; id=&quot;msg1&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;149.2944,63.4297,159.2944,67.4297,149.2944,71.4297,153.2944,67.4297&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;46.5864&quot; x2=&quot;155.2944&quot; y1=&quot;67.4297&quot; y2=&quot;67.4297&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;90.708&quot; x=&quot;53.5864&quot; y=&quot;62.3638&quot;&gt;Create(Pod X)&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part2&quot; data-entity-2=&quot;part1&quot; data-source-line=&quot;6&quot; id=&quot;msg2&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;57.5864,92.5625,47.5864,96.5625,57.5864,100.5625,53.5864,96.5625&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;51.5864&quot; x2=&quot;160.2944&quot; y1=&quot;96.5625&quot; y2=&quot;96.5625&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;80.6787&quot; x=&quot;63.5864&quot; y=&quot;91.4966&quot;&gt;201 Created&lt;/text&gt;&lt;/g&gt;&lt;path d=&quot;M51,109.5625 L51,149.5625 L205,149.5625 L205,119.5625 L195,109.5625 L51,109.5625&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M195,109.5625 L195,119.5625 L205,119.5625 L195,109.5625&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;133.8594&quot; x=&quot;57&quot; y=&quot;126.6294&quot;&gt;곧바로 다시 reconcile이 돌아&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;95.4116&quot; x=&quot;57&quot; y=&quot;141.7622&quot;&gt;&quot;Pod X 있나?&quot; 확인&lt;/text&gt;&lt;g data-entity-1=&quot;part1&quot; data-entity-2=&quot;part3&quot; data-source-line=&quot;11&quot; id=&quot;msg3&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;263.6724,171.9609,273.6724,175.9609,263.6724,179.9609,267.6724,175.9609&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;46.5864&quot; x2=&quot;269.6724&quot; y1=&quot;175.9609&quot; y2=&quot;175.9609&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;29.2373&quot; x=&quot;53.5864&quot; y=&quot;170.895&quot;&gt;list()&lt;/text&gt;&lt;/g&gt;&lt;g data-entity-1=&quot;part3&quot; data-entity-2=&quot;part1&quot; data-source-line=&quot;12&quot; id=&quot;msg4&quot;&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;57.5864,201.0938,47.5864,205.0938,57.5864,209.0938,53.5864,205.0938&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;51.5864&quot; x2=&quot;274.6724&quot; y1=&quot;205.0938&quot; y2=&quot;205.0938&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;166.1372&quot; x=&quot;63.5864&quot; y=&quot;200.0278&quot;&gt;(아직 X 없음 — watch가 안 따라옴)&lt;/text&gt;&lt;/g&gt;&lt;path d=&quot;M51,218.0938 L51,243.0938 L237,243.0938 L237,228.0938 L227,218.0938 L51,218.0938&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M227,218.0938 L227,228.0938 L237,228.0938 L227,218.0938&quot; fill=&quot;#FEFFDD&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;165.2612&quot; x=&quot;57&quot; y=&quot;235.1606&quot;&gt;&quot;없네?&quot; → Pod X를 또 생성 (중복!)&lt;/text&gt;&lt;!--?plantuml-src ROvFIm916CRlyocUzhHx4AfZXqA686z2bwx3xfWBsuoCGrsj5X4K-eEIWHi4XbT5Wmv-6e_Ez1sQTIqAEhxlxtsVvzTWIiTXt612mwDIQ2MJX2iFh0bA6hzfkLh1ALUNM-h6lxmYQb9TS0NAmZel4ihqv9WGYj9XVey3KCMPvdvLHZWBIBuhEKOTsT_TszA82AavL7nUrv0rvm6ivnCpxPlN4KntRBiZA1vA4SO9jza2vgvd1waxypR1i2yTSpCyyl3vFB3P1-4YGfvPc6oiN6CIDxKVa6Byrl1Ta9rSvo5FRPlri6hrSSLqM5zFMx233an_QhAb7Iw2VpmzzsNIiQjUjH-niJ5lIvZXFUnjPjEPAnW_cFdxJl1ZzGK0?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;p&gt;이 read-after-write 불일치를 모르면 &lt;strong&gt;같은 리소스를 중복 생성&lt;/strong&gt;하는 사고가 난다. Kubernetes 본체의 ReplicaSet 컨트롤러는 이를 &lt;strong&gt;expectations 패턴&lt;/strong&gt;(내가 방금 만든/지운 개수를 따로 기억해 두고, 캐시가 그만큼 따라올 때까지 추가 동작을 보류)으로 보정한다. 직접 짤 때도 “캐시가 내 쓰기를 아직 못 봤을 수 있다”를 전제로 멱등하게 설계해야 한다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;4-시작-전에-waitforcachesync-하라&quot;&gt;4. 시작 전에 &lt;code dir=&quot;auto&quot;&gt;WaitForCacheSync&lt;/code&gt; 하라&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;초기 List가 캐시를 다 채우기 전에 reconcile을 시작하면, Lister가 &lt;strong&gt;빈 결과&lt;/strong&gt;를 준다. 그러면 멀쩡히 존재하는 리소스를 “없음”으로 오판해서, 다시 만들거나 심지어 지우는 사고로 이어진다.&lt;/p&gt;
&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;go&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;informer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;Run&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;stopCh&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;cache&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;WaitForCacheSync&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;stopCh&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;informer&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;HasSynced&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;// 동기화 실패&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;}&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;span&gt;// 이제부터 워커 시작&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;
&lt;p&gt;단, &lt;code dir=&quot;auto&quot;&gt;HasSynced&lt;/code&gt;는 &lt;strong&gt;최초 1회 동기화 완료&lt;/strong&gt;를 알리는 게이트일 뿐이다. “지금도 최신”을 보장하지 않는다. 이후 watch가 끊겼다 relist해도 &lt;code dir=&quot;auto&quot;&gt;HasSynced&lt;/code&gt;는 계속 &lt;code dir=&quot;auto&quot;&gt;true&lt;/code&gt;다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;5-핸들러에서-무거운-일을-하지-마라-workqueue-패턴&quot;&gt;5. 핸들러에서 무거운 일을 하지 마라 (workqueue 패턴)&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;이벤트 핸들러는 &lt;strong&gt;key만 큐에 넣고 즉시 리턴&lt;/strong&gt;해야 한다. 실제 작업(API 호출, 재시도, 외부 시스템 연동)은 별도 워커 goroutine에서 한다.&lt;/p&gt;
&lt;figure&gt;&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot; contentStyleType=&quot;text/css&quot; data-diagram-type=&quot;DESCRIPTION&quot; height=&quot;477px&quot; preserveAspectRatio=&quot;none&quot; version=&quot;1.1&quot; viewBox=&quot;0 0 256 477&quot; width=&quot;256px&quot; zoomAndPan=&quot;magnify&quot;&gt;&lt;!--?plantuml 1.2026.6?--&gt;&lt;defs&gt;&lt;/defs&gt;&lt;g&gt;&lt;!--cluster Worker Pool--&gt;&lt;g data-qualified-name=&quot;Worker Pool&quot; data-source-line=&quot;6&quot; id=&quot;ent0003&quot;&gt;&lt;rect fill=&quot;none&quot; height=&quot;87&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;235&quot; x=&quot;7&quot; y=&quot;241&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; font-weight=&quot;700&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;96.8447&quot; x=&quot;76.0776&quot; y=&quot;255.9951&quot;&gt;Worker Pool&lt;/text&gt;&lt;/g&gt;&lt;!--entity W1--&gt;&lt;g data-qualified-name=&quot;Worker Pool.W1&quot; data-source-line=&quot;7&quot; id=&quot;ent0004&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;83.998&quot; x=&quot;23&quot; y=&quot;276&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;63.998&quot; x=&quot;33&quot; y=&quot;298.9951&quot;&gt;Worker 1&lt;/text&gt;&lt;/g&gt;&lt;!--entity W2--&gt;&lt;g data-qualified-name=&quot;Worker Pool.W2&quot; data-source-line=&quot;8&quot; id=&quot;ent0005&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;83.998&quot; x=&quot;142&quot; y=&quot;276&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;63.998&quot; x=&quot;152&quot; y=&quot;298.9951&quot;&gt;Worker 2&lt;/text&gt;&lt;/g&gt;&lt;!--entity H--&gt;&lt;g data-qualified-name=&quot;H&quot; data-source-line=&quot;4&quot; id=&quot;ent0001&quot;&gt;&lt;rect fill=&quot;#F1F1F1&quot; height=&quot;36.2969&quot; rx=&quot;2.5&quot; ry=&quot;2.5&quot; width=&quot;119.6816&quot; x=&quot;64&quot; y=&quot;7&quot;&gt;&lt;/rect&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;99.6816&quot; x=&quot;74&quot; y=&quot;29.9951&quot;&gt;Event Handler&lt;/text&gt;&lt;/g&gt;&lt;!--entity Q--&gt;&lt;g data-qualified-name=&quot;Q&quot; data-source-line=&quot;5&quot; id=&quot;ent0002&quot;&gt;&lt;path d=&quot;M72.5,135 L175.209,135 C180.209,135 180.209,156.2969 180.209,156.2969 C180.209,156.2969 180.209,177.5938 175.209,177.5938 L72.5,177.5938 C67.5,177.5938 67.5,156.2969 67.5,156.2969 C67.5,156.2969 67.5,135 72.5,135&quot; fill=&quot;#F1F1F1&quot;&gt;&lt;/path&gt;&lt;path d=&quot;M175.209,135 C170.209,135 170.209,156.2969 170.209,156.2969 C170.209,177.5938 175.209,177.5938 175.209,177.5938&quot; fill=&quot;none&quot;&gt;&lt;/path&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;82.2637&quot; x=&quot;72.5&quot; y=&quot;152.9951&quot;&gt;WorkQueue&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;92.709&quot; x=&quot;72.5&quot; y=&quot;169.292&quot;&gt;(rate-limited)&lt;/text&gt;&lt;/g&gt;&lt;!--entity API--&gt;&lt;g data-qualified-name=&quot;API&quot; data-source-line=&quot;10&quot; id=&quot;ent0006&quot;&gt;&lt;polygon fill=&quot;#F1F1F1&quot; points=&quot;67.5,416,77.5,406,180.0088,406,180.0088,442.2969,170.0088,452.2969,67.5,452.2969,67.5,416&quot;&gt;&lt;/polygon&gt;&lt;line x1=&quot;170.0088&quot; x2=&quot;180.0088&quot; y1=&quot;416&quot; y2=&quot;406&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;67.5&quot; x2=&quot;170.0088&quot; y1=&quot;416&quot; y2=&quot;416&quot;&gt;&lt;/line&gt;&lt;line x1=&quot;170.0088&quot; x2=&quot;170.0088&quot; y1=&quot;416&quot; y2=&quot;452.2969&quot;&gt;&lt;/line&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;14&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;72.5088&quot; x=&quot;82.5&quot; y=&quot;438.9951&quot;&gt;API Server&lt;/text&gt;&lt;/g&gt;&lt;!--link H to Q--&gt;&lt;g data-entity-1=&quot;ent0001&quot; data-entity-2=&quot;ent0002&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;12&quot; id=&quot;lnk7&quot;&gt;&lt;path d=&quot;M124,43.2 C124,66.86 124,104.42 124,129.81&quot; fill=&quot;none&quot; id=&quot;H-to-Q&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;124,134.81,128,125.81,124,129.81,120,125.81,124,134.81&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;90.3271&quot; x=&quot;125&quot; y=&quot;86.0669&quot;&gt;enqueue(key)&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;53.2822&quot; x=&quot;143.5225&quot; y=&quot;101.1997&quot;&gt;(가볍고 빠름)&lt;/text&gt;&lt;/g&gt;&lt;!--link Q to W1--&gt;&lt;g data-entity-1=&quot;ent0002&quot; data-entity-2=&quot;ent0004&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;13&quot; id=&quot;lnk8&quot;&gt;&lt;path d=&quot;M115.04,178.08 C103.43,204.73 85.4064,246.1358 74.5364,271.0958&quot; fill=&quot;none&quot; id=&quot;Q-to-W1&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;72.54,275.68,79.8008,269.0256,74.5364,271.0958,72.4662,265.8314,72.54,275.68&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;23.2197&quot; x=&quot;103&quot; y=&quot;221.0669&quot;&gt;key&lt;/text&gt;&lt;/g&gt;&lt;!--link Q to W2--&gt;&lt;g data-entity-1=&quot;ent0002&quot; data-entity-2=&quot;ent0005&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;14&quot; id=&quot;lnk9&quot;&gt;&lt;path d=&quot;M133.11,178.08 C144.91,204.73 163.2559,246.148 174.3059,271.108&quot; fill=&quot;none&quot; id=&quot;Q-to-W2&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;176.33,275.68,176.3443,265.8312,174.3059,271.108,169.0291,269.0697,176.33,275.68&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;23.2197&quot; x=&quot;153&quot; y=&quot;221.0669&quot;&gt;key&lt;/text&gt;&lt;/g&gt;&lt;!--link W1 to API--&gt;&lt;g data-entity-1=&quot;ent0004&quot; data-entity-2=&quot;ent0006&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;15&quot; id=&quot;lnk10&quot;&gt;&lt;path d=&quot;M64.09,312.29 C63.88,329.3 65.42,355.61 75,376 C80.22,387.11 85.1882,393.8807 93.6682,402.3307&quot; fill=&quot;none&quot; id=&quot;W1-to-API&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;97.21,405.86,93.6582,396.6739,93.6682,402.3307,88.0114,402.3408,97.21,405.86&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;59.0522&quot; x=&quot;76&quot; y=&quot;357.0669&quot;&gt;reconcile&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;45.481&quot; x=&quot;82.7856&quot; y=&quot;372.1997&quot;&gt;(무거운 일)&lt;/text&gt;&lt;/g&gt;&lt;!--link W2 to API--&gt;&lt;g data-entity-1=&quot;ent0005&quot; data-entity-2=&quot;ent0006&quot; data-link-type=&quot;dependency&quot; data-source-line=&quot;16&quot; id=&quot;lnk11&quot;&gt;&lt;path d=&quot;M176.28,312.12 C165.52,335.97 148.0453,374.682 136.1453,401.072&quot; fill=&quot;none&quot; id=&quot;W2-to-API&quot;&gt;&lt;/path&gt;&lt;polygon fill=&quot;#181818&quot; points=&quot;134.09,405.63,141.436,399.0698,136.1453,401.072,134.1432,395.7813,134.09,405.63&quot;&gt;&lt;/polygon&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;59.0522&quot; x=&quot;162&quot; y=&quot;357.0669&quot;&gt;reconcile&lt;/text&gt;&lt;text fill=&quot;#000000&quot; font-family=&quot;sans-serif&quot; font-size=&quot;13&quot; lengthAdjust=&quot;spacing&quot; textLength=&quot;45.481&quot; x=&quot;168.7856&quot; y=&quot;372.1997&quot;&gt;(무거운 일)&lt;/text&gt;&lt;/g&gt;&lt;!--?plantuml-src ZP2nJiCm54NtznLFcPeXGp9sG6902bkZ3bbOJFA4h3X-mNMBAeJ4mCeF825rOc8eQXSalYZfHs2dQTI1WSdsk-VwNdjHITq88seik6vOaxPpkrQ4XWehz9qYiDoWPRnbQxd6Kdf5iWOO7HXShVoI2bqgCW6A1QPmlwGbOP2pgJA_lT4J8on5IjRIKXdsM0PZq14bWpDc5U0Z89ub35hSk_BuJp4vYWayWURINNWvkyOvcTNGoHq1KenAVj3H1MOuHT9zpKb5wz0LRBVFtUwrtMsm-zvq7oyXP2EUnuvttFae6KPEEyryvjH_7-j2Al_gxdFVVcqFRtiylF-4u4p_PNy1?--&gt;&lt;/g&gt;&lt;/svg&gt;&lt;/figure&gt;
&lt;p&gt;핸들러 안에서 동기적으로 무거운 작업을 하면, &lt;strong&gt;자기 리스너 버퍼가 무한정 커지고&lt;/strong&gt; 전체 이벤트 처리가 밀린다. workqueue를 거치면 (1) 재시도·rate-limit·backoff를 큐가 대신 해주고, (2) &lt;strong&gt;같은 key의 중복 처리를 자동으로 합쳐주며&lt;/strong&gt;(dedup), (3) 워커 수로 처리량을 조절할 수 있다. controller-runtime이 강제하는 구조가 바로 이것이다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;6-resync는-api-서버-재조회가-아니다&quot;&gt;6. resync는 API 서버 재조회가 아니다&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;가장 흔한 오해. &lt;code dir=&quot;auto&quot;&gt;resyncPeriod&lt;/code&gt;를 짧게 잡으면 최신 데이터를 더 자주 받아온다고 생각하는데, &lt;strong&gt;틀렸다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;resync는 &lt;strong&gt;로컬 캐시에 이미 있는 객체들을 핸들러에 다시 흘려보낼 뿐&lt;/strong&gt;, 네트워크 호출이 0이다. &lt;code dir=&quot;auto&quot;&gt;old == new&lt;/code&gt;인 Update 이벤트가 다시 도는 것뿐이다. 목적은 “혹시 놓친 reconcile이 있으면 현재 상태 기준으로 한 번 더 맞춰라”는 안전장치다.&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;relist&lt;/th&gt;&lt;th&gt;resync&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;트리거&lt;/td&gt;&lt;td&gt;watch 끊김 / &lt;code dir=&quot;auto&quot;&gt;410 Gone&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code dir=&quot;auto&quot;&gt;resyncPeriod&lt;/code&gt; 주기&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;데이터 출처&lt;/td&gt;&lt;td&gt;&lt;strong&gt;API 서버 재조회&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;로컬 캐시&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;네트워크 비용&lt;/td&gt;&lt;td&gt;있음&lt;/td&gt;&lt;td&gt;없음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;목적&lt;/td&gt;&lt;td&gt;watch 갭 복구&lt;/td&gt;&lt;td&gt;주기적 재조정&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;그래서 resync를 짧게 잡으면 데이터가 신선해지는 게 아니라 &lt;strong&gt;그냥 CPU만 더 탄다.&lt;/strong&gt; 보통 0(off)이거나 수십 분 단위로 두는 게 좋다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;7-메모리를-의식하라&quot;&gt;7. 메모리를 의식하라&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Informer는 대상 타입의 &lt;strong&gt;객체를 전부 메모리에 캐싱&lt;/strong&gt;한다. Pod·Secret·ConfigMap을 클러스터 전역으로 watch하면 캐시가 수 GB까지 커질 수 있다. 대응 수단:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;범위를 좁혀라&lt;/strong&gt; — 네임스페이스 스코프 informer, label/field selector로 캐시 대상 자체를 줄인다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;TransformFunc&lt;/code&gt;로 잘라내라&lt;/strong&gt; — 저장 전에 &lt;code dir=&quot;auto&quot;&gt;managedFields&lt;/code&gt;처럼 안 쓰는 큰 필드를 떼어내 메모리를 줄인다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;초기 List 스파이크&lt;/strong&gt; — informer가 뜰 때 전체를 한 번에 받으면 메모리가 출렁인다. 최신 client-go의 streaming list(WatchList)는 이 초기 스파이크를 완화한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여러 컴포넌트가 같은 타입을 본다면 &lt;strong&gt;&lt;code dir=&quot;auto&quot;&gt;SharedInformerFactory&lt;/code&gt;로 informer를 공유&lt;/strong&gt;해 캐시 중복을 막는다. 단, &lt;strong&gt;selector를 건 필터링 된 informer는 별도 캐시&lt;/strong&gt;라 전체를 못 본다. “왜 내 Lister엔 이 객체가 없지?”의 단골 원인이 바로 어딘가에서 좁은 selector로 informer를 만든 경우다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;정리-informer는-공짜-캐시가-아니다&quot;&gt;정리: Informer는 “공짜 캐시”가 아니다&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;Informer는 List+Watch+재연결+캐시를 대신 해주는 강력한 추상화지만, 그 대가로 몇 가지 규칙을 지킬 것을 요구한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;이벤트는 정확하지 않다.&lt;/strong&gt; 끊김·relist로 중간 이벤트는 유실된다 → level-triggered로 수렴하게 짜라.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;읽은 객체는 내 것이 아니다.&lt;/strong&gt; 수정 전 &lt;code dir=&quot;auto&quot;&gt;DeepCopy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캐시는 조금 늦다.&lt;/strong&gt; read-after-write를 가정하지 말고 멱등하게.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시작 전 &lt;code dir=&quot;auto&quot;&gt;WaitForCacheSync&lt;/code&gt;.&lt;/strong&gt; 빈 캐시로 오판 금지.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핸들러는 가볍게, 일은 workqueue에서.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;resync ≠ 재조회.&lt;/strong&gt; 짧게 잡아도 신선해지지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;캐시는 메모리를 먹는다.&lt;/strong&gt; 범위·transform·공유로 관리하라.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 규칙들은 결국 하나로 모인다. &lt;strong&gt;Informer가 보여주는 건 “진실의 약간 늦은 사본”이지, 진실 그 자체가 아니다.&lt;/strong&gt; 그 사본을 어떻게 다뤄야 하는지를 아는 것이 Informer를 제대로 쓰는 일의 전부다.&lt;/p&gt;</content:encoded><category>Kubernetes</category><category>Informer</category><category>client-go</category><category>Controller</category></item><item><title>우리 팀이 Git 대충 쓰기에서 협업 시스템으로 넘어간 방법</title><link>https://ycra-dev.github.io/blog/git-team-collaboration/</link><guid isPermaLink="true">https://ycra-dev.github.io/blog/git-team-collaboration/</guid><description>1인 1과제로 규칙이 사라진 팀에서 다시 협업이 필요해졌을 때, Git 컨벤션·브랜치 전략·코드리뷰 문화를 어떻게 시스템으로 만들어갔는지 정리했다.

</description><pubDate>Sun, 08 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;div&gt;&lt;h2 id=&quot;git을-쓰는데도-협업이-안-되는-순간들&quot;&gt;Git을 쓰는데도 협업이 안 되는 순간들&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;개발자가 Git을 사용할 때는 생각보다 자주 같은 문제를 겪는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;브랜치 전략 수립&lt;/li&gt;
&lt;li&gt;merge 충돌 해결&lt;/li&gt;
&lt;li&gt;commit 메시지 기준 부재로 인한 히스토리 혼란&lt;/li&gt;
&lt;li&gt;PR 리뷰 참여 저조&lt;/li&gt;
&lt;li&gt;히스토리 관리 실패(되돌리기 어려움, 의사결정 과정 소실)&lt;/li&gt;
&lt;li&gt;권한/정책 운영 문제(force push, approve 기준 등)&lt;/li&gt;
&lt;li&gt;신규 인원 온보딩 실패(적응 어려움)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;우리 팀도 한동안은 이 문제들을 각자 알아서 버티는 방식으로 넘기고 있었다.
당장 기능 개발은 굴러가니까 지금은 바쁘니까 나중에 정하자로 미뤄지기 쉬웠고, 그 사이 규칙은 더 느슨해졌다.
그러다 협업이 본격적으로 필요해지는 순간, 작은 불편이 한꺼번에 터지면서 커뮤니케이션 비용이 급격히 늘어났다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;규칙이-증발한-팀에서-생긴-일&quot;&gt;규칙이 증발한 팀에서 생긴 일&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;지난 몇 년 동안 팀에서 인원에 비해 많은 과제를 수행하다 보니 1인 1과제로 흘러간 기간이 길었다.
그 결과 팀에서 이슈 관리, 코드 관리 규칙이 자연스럽게 사라졌고, 각자 편한 방식으로 일하는 “사일로”가 생겼다.&lt;/p&gt;
&lt;p&gt;문제는 다시 협업이 필요해졌을 때였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;규칙을 새로 도입하려면 &lt;strong&gt;설계하는 데 시간&lt;/strong&gt;이 들고&lt;/li&gt;
&lt;li&gt;규칙을 지키는 과정이 &lt;strong&gt;오히려 생산성을 깎는 느낌&lt;/strong&gt;이 들었다&lt;/li&gt;
&lt;li&gt;게다가 옆자리에서 바로 물어보는 습관이 생겨서, 그때그때 구두로 문제를 해결해버리곤 했다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;근데 이 방식은 오래 가지 못했다.
실시간 소통이 계속되면 &lt;strong&gt;집중 시간이 줄어들고&lt;/strong&gt;, 말로만 전달하면 &lt;strong&gt;기록이 남지 않아 기억에 의존하다가 증발&lt;/strong&gt;해버리고 말았다.&lt;/p&gt;
&lt;p&gt;그래서 결론적으로, 우리 팀 상황에 맞는 Git 협업 전략을 진짜로 만들기로 했다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;git-협업-성숙도-4단계&quot;&gt;Git 협업 성숙도 4단계&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;나는 Git을 사용하는 과정을 다음 4단계로 본다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;1단계-git이-무섭다&quot;&gt;1단계: Git이 무섭다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;commit, push는 하지만, 브랜치를 만들고 merge하다가 충돌 나는 상황이 두렵다.
다만, 요즘은 AI 도구 덕분에 충돌 해결 자체는 쉬워졌고, 그래서 &lt;strong&gt;기능을 외우기보다는 개념을 이해하는 쪽&lt;/strong&gt;이 더 중요해졌다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;2단계-코드는-합쳐지는데-히스토리는-망한다&quot;&gt;2단계: 코드는 합쳐지는데 히스토리는 망한다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;사람마다 브랜치 만들고, PR 올리고, merge하는 기준이 달라서 충돌은 안 나는데 &lt;strong&gt;히스토리가 전혀 관리되지 않는 상태&lt;/strong&gt;가 된다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;3단계-규칙은-있는데-너무-무겁다&quot;&gt;3단계: 규칙은 있는데 너무 무겁다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;규칙대로 굴러가긴 하는데 지키느라 오버헤드가 크다.
특히 PR에서 시간을 많이 뺏기면 &lt;strong&gt;규칙이 생산성을 떨어뜨린다&lt;/strong&gt;는 인식이 강해진다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;4단계-규칙이-익숙해져서-생산성이-오른다&quot;&gt;4단계: 규칙이 익숙해져서 생산성이 오른다&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;규칙이 자연스러워지고, PR을 통해 서로의 코드 이해도가 올라가면서
결과적으로 개발 속도, 품질, 리뷰 속도가 같이 개선된다.&lt;/p&gt;
&lt;p&gt;이 글에서는 &lt;strong&gt;2단계에 머물던 팀이 4단계로 올라가게 만든 방식&lt;/strong&gt;을 정리하려고 한다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;거버넌스-완벽한-정답은-없고-팀에-맞는-답이-있다&quot;&gt;거버넌스: 완벽한 정답은 없고 팀에 맞는 답이 있다&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;처음에 제일 고민했던 건 거버넌스를 어떻게 세울까였고, 그 결론은 완벽한 거버넌스는 없고, 팀에 맞는 거버넌스만 있다는 것이다.
팀의 업무 흐름(티켓 단위, 배포 주기, 리뷰 문화)이나 인력 구성(신규 인원 비중, 협업 빈도)이 다르면 같은 전략도 결과가 완전히 달라진다.
그래서 좋아 보이는 방식이 아니라, 지금 팀이 겪는 병목을 줄이는 방향으로 후보를 비교했다.&lt;/p&gt;
&lt;p&gt;대표적으로 이런 선택지들이 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;메인라인 개발(Mainline development)&lt;/li&gt;
&lt;li&gt;기능별 브랜치(Branch-per-feature)&lt;/li&gt;
&lt;li&gt;상태 브랜치(State branching)&lt;/li&gt;
&lt;li&gt;예정된 배포(Scheduled deployment)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;우리 팀은 &lt;strong&gt;기능별 브랜치 + 상태 브랜치&lt;/strong&gt;를 선택했다.
기능 단위로 티켓을 쪼개서 히스토리를 깔끔하게 만들고, 상태 브랜치로 배포,안정화 흐름을 분리하는 게 현재 팀 상황에 제일 잘 맞았다.
중요한 건 유행하는 전략을 그대로 사용하는 것보다, &lt;strong&gt;팀 상황을 기준으로 선택&lt;/strong&gt;하는 거였다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;규칙을-문서화하는-방식-cookbook으로-만들기&quot;&gt;규칙을 문서화하는 방식: Cookbook으로 만들기&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;우리는 Jira와 Bitbucket을 쓰고 있었고,
이 화면에서 이렇게 하면 된다를 그대로 따라할 수 있는 &lt;strong&gt;cookbook 형식 가이드&lt;/strong&gt;를 작성해서 공유했다.&lt;/p&gt;
&lt;p&gt;형식을 이렇게 잡은 이유는 원칙만 적으면 해석이 사람마다 달라지고, 결국 또 각자 방식으로 흩어지기 때문이다.
반대로 화면 단위로 “어디를 눌러서 무엇을 해야 하는지”가 정리되어 있으면, 신규 인원도 시행착오 없이 그대로 따라할 수 있었다.
그래서 가이드는 브랜치 생성 → PR 생성 → 리뷰 요청 → merge까지를 한 흐름으로 묶고, 예외 케이스(급한 hotfix, 롤백, 충돌 처리)도 최소한으로 포함했다.&lt;/p&gt;
&lt;p&gt;가이드를 운영하면서 “수정이 필요한 부분”은 팀원이 직접 보완해주게 만들어서 문서를 지침이 아니라 하나의 살아있는 팀 규칙으로 만들었다.
문서가 팀의 합의로 계속 업데이트되니까, 규칙이 누가 시켜서 하는 것이 아니라 우리 방식으로 굳어지기 시작했다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;핵심-규칙-변경이-둘-이상이면-티켓도-브랜치도-분리&quot;&gt;핵심 규칙: 변경이 둘 이상이면 티켓도 브랜치도 분리&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;우리 팀이 가장 중요하게 둔 규칙은 이거였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;변경사항 2개 이상 시 Jira 티켓 분리&lt;/li&gt;
&lt;li&gt;티켓 분리 시 브랜치 분리&lt;/li&gt;
&lt;li&gt;(버그 픽스 제외) 규칙 준수 원칙&lt;/li&gt;
&lt;li&gt;main 브랜치 직접 push 금지&lt;/li&gt;
&lt;li&gt;PR approve 이후 merge 허용(Bitbucket 권한/정책으로 제한)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 규칙 하나가 히스토리 관리 실패와 리뷰가 안 달리는 문제를 동시에 줄였다.
리뷰어 입장에선 변경 범위가 작아지니까 리뷰가 쉬워지고, 작성자 입장에선 흐름이 깔끔해졌다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;운영-루틴-주간회의--티켓--브랜치--commit--pr--리뷰--merge&quot;&gt;운영 루틴: 주간회의 → 티켓 → 브랜치 → commit → PR → 리뷰 → merge&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;우리는 다음 루틴을 팀의 기본 리듬으로 잡았다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;주 1회 주간회의: 주간 이슈 정리 및 담당 배정&lt;/li&gt;
&lt;li&gt;개인 티켓 생성(회의 내용 기반)&lt;/li&gt;
&lt;li&gt;티켓 기반 브랜치 생성&lt;/li&gt;
&lt;li&gt;작업 commit 및 PR 요청&lt;/li&gt;
&lt;li&gt;approve 후 merge&lt;/li&gt;
&lt;li&gt;반복&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 구조의 장점은, 이슈 관리 시스템만 봐도 누가 뭘 하고 있는지가 보인다는 것이다.
그래서 다음 주간회의 전까지 상황에 따라 티켓을 유동적으로 재분배할 수도 있었다.&lt;/p&gt;
&lt;div&gt;&lt;h3 id=&quot;주의한-점-티켓에-너무-힘주지-않기&quot;&gt;주의한 점: 티켓에 너무 힘주지 않기&lt;/h3&gt;&lt;/div&gt;
&lt;p&gt;티켓 자체를 완벽하게 쓰려다 보면 시간이 오래 걸릴 수 있다.
대신 우리는 이쪽에 힘을 줬다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;commit 메시지에 맥락을 남기기&lt;/li&gt;
&lt;li&gt;PR 요청 설명에 의사결정의 이유를 남기기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래야 PR이 빨라지고, 기록이 남아서, 말로만 소통하다 증발하는 문제도 줄었다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;피어리뷰-운영-고쳐주기보다-성장시키기&quot;&gt;피어리뷰 운영: 고쳐주기보다 성장시키기&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;피어리뷰는 보통 3종류로 나뉜다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;안 돌아가는 코드(버그/치명적 문제)&lt;/li&gt;
&lt;li&gt;모범사례 미준수(컨벤션, 중복 구현 등)&lt;/li&gt;
&lt;li&gt;리뷰어의 개인 선호(취향, 스타일)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이때 리뷰 방식은 크게 두 가지가 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;리뷰어가 직접 수정 commit을 한다&lt;/li&gt;
&lt;li&gt;리뷰 코멘트로 방향을 준다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;우리는 &lt;strong&gt;가능한 빨리 리뷰를 달아주는 방식&lt;/strong&gt;을 택했고,
직접 수정 commit은 되도록 지양했다.&lt;/p&gt;
&lt;p&gt;우리의 목표는 “빨리 합치기”가 아니라 “리뷰를 통해 성장하기”였기 때문이다.
리뷰어가 잘못된 점과 수정 방향을 남기면, 커미터가 자신의 결정 이유와 비교하면서
스스로 납득 가능한 해답을 다시 만들었다.&lt;/p&gt;
&lt;p&gt;초반에는 리뷰 시간이 오래 걸렸지만,
코드베이스 이해가 올라가고 서로의 개발 스타일/철학을 알게 되면서
결과적으로 &lt;strong&gt;개발 속도, 품질, 리뷰 속도가 같이 올라갔다.&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;ai-시대에도-git-협업-문화는-더-중요해진다&quot;&gt;AI 시대에도 Git 협업 문화는 더 중요해진다&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;AI 때문에 사람이 코드를 직접 작성할 이유는 점점 줄어들 수 있다.
그런데 아이러니하게도, AI가 만든 코드일수록 &lt;strong&gt;오너십은 더 강하게 필요&lt;/strong&gt;하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PR에 “부끄럽지 않은 코드”를 올리려고 사람이 한 번 더 검수해야 하고&lt;/li&gt;
&lt;li&gt;팀은 PR 리뷰로 2차 검수를 하면서&lt;/li&gt;
&lt;li&gt;AI로 인한 무분별한 코드 생성과 기술부채 누적을 막아야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 나는 오히려 앞으로는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jira 같은 티켓 시스템으로 태스크를 더 작게 쪼개고&lt;/li&gt;
&lt;li&gt;작은 단위로 commit/PR을 만들고&lt;/li&gt;
&lt;li&gt;AI로 빠르게 구현하되 사람이 책임지고 검수하는 흐름&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 방식이 &lt;strong&gt;생산성을 더 끌어올릴 거라고 생각한다.&lt;/strong&gt;
그리고 그 바탕에 Git 협업 문화가 있다.&lt;/p&gt;
&lt;div&gt;&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;&lt;/div&gt;
&lt;p&gt;이 글은 “나 혼자 편하게”가 아니라,
팀이 함께 좋은 문화를 만들고 공유하면서 생산성을 올리는 과정에 대한 기록이다.&lt;/p&gt;
&lt;p&gt;완벽한 규칙은 없지만, 우리 팀에 맞는 규칙을 만들고 계속 조정하면서 2단계에서 4단계로 올라가는 건 충분히 가능했다.
결국 핵심은 거창한 방법론이 아니라, &lt;strong&gt;작게 시작해서 팀이 실제로 지킬 수 있는 형태로 굳히는 것&lt;/strong&gt;이었다.&lt;/p&gt;</content:encoded><category>Git</category><category>협업</category><category>코드리뷰</category><category>팀문화</category></item></channel></rss>