<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>taeni.log</title>
        <link>https://velog.io/</link>
        <description>정태인의 블로그</description>
        <lastBuildDate>Thu, 19 Mar 2026 00:23:43 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>taeni.log</title>
            <url>https://velog.velcdn.com/images/taeni-develop/profile/9fb5dfe7-a1c1-43ee-b1fb-6ebe7e68b2bb/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. taeni.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/taeni-develop" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Redis Sentinel Failover Full Resync 현상 분석 및 대응 방안]]></title>
            <link>https://velog.io/@taeni-develop/Redis-Sentinel-Failover-Full-Resync-%ED%98%84%EC%83%81-%EB%B6%84%EC%84%9D-%EB%B0%8F-%EB%8C%80%EC%9D%91-%EB%B0%A9%EC%95%88</link>
            <guid>https://velog.io/@taeni-develop/Redis-Sentinel-Failover-Full-Resync-%ED%98%84%EC%83%81-%EB%B6%84%EC%84%9D-%EB%B0%8F-%EB%8C%80%EC%9D%91-%EB%B0%A9%EC%95%88</guid>
            <pubDate>Thu, 19 Mar 2026 00:23:43 GMT</pubDate>
            <description><![CDATA[<h1 id="redis-sentinel-failover-full-resync-현상-분석-및-대응-방안">Redis Sentinel Failover Full Resync 현상 분석 및 대응 방안</h1>
<p>환경: Redis 6.0 / Sentinel / 데이터 500GB 이상</p>
<hr>
<h2 id="1-현상">1. 현상</h2>
<p>Sentinel에서 <code>sentinel failover mymaster</code> 명령어로 수동 failover를 수행하면, 데이터가 거의 없고 backlog 설정이 충분한 환경에서도 partial resync가 아닌 full resync가 발생한다.</p>
<h3 id="발생-로그">발생 로그</h3>
<pre><code>Before turning into a replica, using my own master parameters to synthesize a cached master:
I may be able to synchronize with the new master with just a partial transfer
Discarding previously cached master state.
Full resync from master
connection with replica client id lost</code></pre><h3 id="환경-정보">환경 정보</h3>
<ul>
<li>Redis 버전: 6.0</li>
<li>데이터 사용량: 500GB 이상 (프로덕션)</li>
<li>client-output-buffer-limit (slave): 2GB 1GB 60</li>
<li>repl-timeout: 600초</li>
<li>repl-backlog-size: 충분히 큰 값으로 설정됨</li>
<li>master_replid2: 0000000000000000000000000000000000000000 (초기화됨)</li>
</ul>
<hr>
<h2 id="2-원인-분석">2. 원인 분석</h2>
<h3 id="21-근본-원인-sentinel-failover의-구조적-한계">2.1 근본 원인: sentinel failover의 구조적 한계</h3>
<p><code>sentinel failover</code>는 내부적으로 선택된 replica에게 <code>SLAVEOF NO ONE</code>을 보내는 비협조적(uncoordinated) 방식이다. master는 자신이 교체될 것을 모르기 때문에 내부 쓰기를 계속하고, 이로 인해 offset 불일치가 발생한다. 이것은 버전과 관계없이 동일하게 발생하는 구조적 문제이다.</p>
<h3 id="22-failover-시-내부-동작-순서">2.2 Failover 시 내부 동작 순서</h3>
<ol>
<li>Sentinel이 replica를 선택하여 <code>SLAVEOF NO ONE</code> 전송 → 새 master로 승격</li>
<li>새 master는 새로운 replid 생성, 이전 replid를 replid2에 저장, second_repl_offset 기록</li>
<li>이전 master는 아직 master로 동작 중 → 내부 쓰기(expire, Sentinel PING 등)로 offset 계속 증가</li>
<li>Sentinel이 이전 master에게 <code>SLAVEOF &lt;new-master&gt;</code> 전송 → replica로 전환</li>
<li>기존 replication 연결 끊김 → <code>connection with replica client id lost</code> 발생</li>
<li>이전 master가 새 master에게 PSYNC 요청 → 자신의 offset이 새 master의 second_repl_offset보다 큼</li>
<li>새 master가 partial resync 거부 → <strong>full resync 발생</strong></li>
</ol>
<h3 id="23-핵심-메커니즘-offset-불일치">2.3 핵심 메커니즘: Offset 불일치</h3>
<p>이전 master의 offset이 새 master의 second_repl_offset보다 크면, 새 master는 &quot;이 replica가 요청하는 offset은 내가 알고 있는 분기 시점보다 앞서 있다&quot;고 판단하여 partial resync를 거부한다. 데이터가 거의 없는 환경에서도 failover 과정의 시간 차이 동안 Sentinel의 PING/INFO 교환만으로 offset 차이가 발생한다.</p>
<h3 id="24-replid2-초기화는-결과이지-원인이-아님">2.4 replid2 초기화는 결과이지 원인이 아님</h3>
<p>master_replid2가 000...000으로 보이는 것은 full resync가 완료된 후 replica가 새 master의 replid로 갱신되면서 초기화된 결과이다.</p>
<h3 id="25-배제된-원인">2.5 배제된 원인</h3>
<ul>
<li>backlog 크기 부족 → 설정 충분 + 데이터 거의 없음</li>
<li>client-output-buffer-limit → 2GB로 충분</li>
<li>repl-timeout → 600초로 충분</li>
<li>backlog 덮어쓰기 → 쓰기 거의 없음</li>
</ul>
<hr>
<h2 id="3-500gb-환경에서-full-resync의-영향">3. 500GB 환경에서 Full Resync의 영향</h2>
<ul>
<li>RDB fork 시 copy-on-write로 메모리 사용량 최대 2배(~1TB) → OOM killer 위험</li>
<li>RDB 생성 중 수십 분간 CPU/IO 부하</li>
<li>500GB RDB 전송 중 네트워크 대역폭 점유</li>
<li>replica가 sync 완료할 때까지 수 시간 서비스 불안정</li>
<li>offset 차이만큼 데이터 유실 가능성</li>
</ul>
<hr>
<h2 id="4-대응-방안-client-pause--sentinel-failover">4. 대응 방안: CLIENT PAUSE + SENTINEL FAILOVER</h2>
<p>버전과 관계없이 Sentinel 환경에서는 <code>CLIENT PAUSE WRITE</code>로 쓰기를 먼저 중단하여 master의 offset 증가를 멈춘 후 <code>SENTINEL FAILOVER</code>를 수행해야 한다. 이렇게 하면 새 master의 second_repl_offset과 이전 master의 offset이 일치하여 partial resync가 가능해진다.</p>
<p>Sentinel이 failover 주체가 되므로 <code>+switch-master</code> 이벤트가 즉시 발행되고, 클라이언트 전환도 정상적으로 이루어진다.</p>
<h3 id="41-수동-수행-절차">4.1 수동 수행 절차</h3>
<p>사전 준비로 터미널 2개를 미리 준비한다.</p>
<ul>
<li>터미널 A: master 접속 (<code>redis-cli -h &lt;master-ip&gt; -p 6379</code>)</li>
<li>터미널 B: sentinel 접속 (<code>redis-cli -h &lt;sentinel-ip&gt; -p 26379</code>)</li>
</ul>
<table>
<thead>
<tr>
<th>순서</th>
<th>터미널</th>
<th>명령어</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>A (master)</td>
<td><code>INFO replication</code></td>
<td>slave0의 lag=0 확인 (0과 1 반복은 정상)</td>
</tr>
<tr>
<td>2</td>
<td>B (sentinel)</td>
<td><code>SENTINEL failover mymaster</code> 타이핑만 (엔터 치지 않음)</td>
<td>미리 준비</td>
</tr>
<tr>
<td>3</td>
<td>A (master)</td>
<td><code>CLIENT PAUSE 30000 WRITE</code></td>
<td>OK 확인 후 즉시 Step 4</td>
</tr>
<tr>
<td>4</td>
<td>B (sentinel)</td>
<td>엔터 (미리 타이핑한 명령 실행)</td>
<td>Step 3과 간격 최소화</td>
</tr>
<tr>
<td>5</td>
<td>A (master)</td>
<td><code>INFO replication</code></td>
<td>role:slave 변경 확인</td>
</tr>
<tr>
<td>6</td>
<td>A (master)</td>
<td><code>CLIENT UNPAUSE</code></td>
<td>안전하게 해제</td>
</tr>
</tbody></table>
<h3 id="42-핵심-포인트">4.2 핵심 포인트</h3>
<ul>
<li>Step 3과 Step 4 사이 간격을 최대한 짧게 하는 것이 핵심이다.</li>
<li>PAUSE timeout 30초는 안전장치이다. failover가 실패해도 30초 후 자동으로 쓰기가 재개된다.</li>
<li>lag이 0과 1을 오가는 것은 정상이다. lag은 replica가 1초마다 ACK를 보내는 타이밍 기반이므로, offset 차이가 수십~수백 이내면 바로 진행해도 된다.</li>
<li>CLIENT PAUSE를 안 해도 failover 중에는 어차피 쓰기가 안 되므로, CLIENT PAUSE가 추가하는 쓰기 중단 시간은 사실상 없다.</li>
</ul>
<h3 id="43-예상-클라이언트-영향-비교">4.3 예상 클라이언트 영향 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>CLIENT PAUSE + failover</th>
<th>그냥 sentinel failover</th>
</tr>
</thead>
<tbody><tr>
<td>쓰기 중단 시간</td>
<td>약 5~10초</td>
<td>약 5~10초 (비슷)</td>
</tr>
<tr>
<td>failover 후 상태</td>
<td>partial resync → 즉시 정상</td>
<td>full resync → 수 시간 부하</td>
</tr>
<tr>
<td>master 부하</td>
<td>없음</td>
<td>RDB fork로 메모리/CPU/IO 폭증</td>
</tr>
<tr>
<td>OOM 위험</td>
<td>없음</td>
<td>copy-on-write로 최대 ~1TB</td>
</tr>
<tr>
<td>replica 가용성</td>
<td>즉시 정상</td>
<td>sync 완료까지 불안정</td>
</tr>
<tr>
<td>데이터 정합성</td>
<td>보장</td>
<td>offset 차이만큼 유실 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-sentinel-failover-vs-cluster-failover">5. SENTINEL FAILOVER vs CLUSTER FAILOVER</h2>
<p>Redis Cluster 환경의 <code>CLUSTER FAILOVER</code>는 이 문제가 발생하지 않는다. replica가 master에게 failover를 요청하면 master가 <code>CLIENT PAUSE WRITE</code>를 실행하고, replica가 offset을 따라잡은 후 Cluster 투표를 통해 전환된다. 협조적(coordinated) 방식이므로 offset 불일치가 없고, Cluster 구성이 자동 전파되어 추가 조치도 불필요하다.</p>
<p>Sentinel 환경의 <code>SENTINEL FAILOVER</code>는 master에게 알리지 않고 replica에 <code>SLAVEOF NO ONE</code>을 보내는 비협조적 방식이므로 offset 불일치가 발생하며, 이를 방지하려면 수동으로 <code>CLIENT PAUSE WRITE</code>를 먼저 수행해야 한다. 이 문제는 Sentinel이 내부적으로 협조적 전환을 지원하지 않는 구조적 한계이며, GitHub issue #13118, #13917에서 개선이 제안된 상태이나 아직 공식 반영되지 않았다.</p>
<hr>
<h2 id="6-로그-확인-방법">6. 로그 확인 방법</h2>
<h3 id="61-failover-전후-replication-상태-확인">6.1 Failover 전후 replication 상태 확인</h3>
<pre><code>redis-cli -h &lt;노드-ip&gt; INFO replication</code></pre><p>확인할 값: <code>master_replid</code>, <code>master_replid2</code>, <code>master_repl_offset</code>, <code>second_repl_offset</code>, <code>repl_backlog_first_byte_offset</code></p>
<h3 id="62-새-master-로그에서-partial-resync-거부-사유-확인">6.2 새 master 로그에서 partial resync 거부 사유 확인</h3>
<pre><code>grep -iE &quot;psync|partial|full|resync|replid|offset|accept|reject&quot; &lt;new-master-log-path&gt;</code></pre><p>다음 중 하나가 출력된다:</p>
<ul>
<li><code>Partial resynchronization not accepted: Replication ID mismatch</code> → replid 불일치</li>
<li><code>Partial resynchronization not accepted: Requested offset for second ID was X, but I can reply up to Y</code> → offset 초과</li>
</ul>
<h3 id="63-이전-master강등된-노드-로그-확인">6.3 이전 master(강등된 노드) 로그 확인</h3>
<pre><code>grep -iE &quot;psync|partial|full|resync|cached|discard|turning|pause&quot; &lt;old-master-log-path&gt;</code></pre><h3 id="64-sentinel-로그에서-failover-진행-확인">6.4 Sentinel 로그에서 failover 진행 확인</h3>
<pre><code>grep -iE &quot;failover|switch-master|promoted|selected&quot; &lt;sentinel-log-path&gt;</code></pre><hr>
<h2 id="7-근거-문서">7. 근거 문서</h2>
<ul>
<li>Redis Replication 공식 문서 (replid2, second_repl_offset 동작 원리): <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/replication/">https://redis.io/docs/latest/operate/oss_and_stack/management/replication/</a></li>
<li>Redis Sentinel 공식 문서 (failover 시 REPLICAOF NO ONE 방식): <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/">https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/</a></li>
<li>CLUSTER FAILOVER 공식 문서 (협조적 전환 방식): <a href="https://redis.io/docs/latest/commands/cluster-failover/">https://redis.io/docs/latest/commands/cluster-failover/</a></li>
<li>GitHub issue #4819 — sentinel failover 후 offset 불일치로 full resync 발생 보고: <a href="https://github.com/redis/redis/issues/4819">https://github.com/redis/redis/issues/4819</a></li>
<li>GitHub issue #13483 — Redis 7.2.4에서도 동일 현상 발생 확인: <a href="https://github.com/redis/redis/issues/13483">https://github.com/redis/redis/issues/13483</a></li>
<li>GitHub issue #13118 — Sentinel에 협조적 failover 도입 제안 (미반영): <a href="https://github.com/redis/redis/issues/13118">https://github.com/redis/redis/issues/13118</a></li>
<li>GitHub issue #13917 — 동일 문제 개선 제안 (미반영): <a href="https://github.com/redis/redis/issues/13917">https://github.com/redis/redis/issues/13917</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[docker container run 명령어 확인]]></title>
            <link>https://velog.io/@taeni-develop/docker-container-run-%EB%AA%85%EB%A0%B9%EC%96%B4-%ED%99%95%EC%9D%B8</link>
            <guid>https://velog.io/@taeni-develop/docker-container-run-%EB%AA%85%EB%A0%B9%EC%96%B4-%ED%99%95%EC%9D%B8</guid>
            <pubDate>Thu, 30 Oct 2025 00:38:31 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-bash">docker inspect --format=&#39;docker run {{range .HostConfig.Binds}} -v {{.}} {{end}}{{range $p, $conf := .HostConfig.PortBindings}} -p {{$p}}:{{(index $conf 0).HostPort}} {{end}} {{.Config.Image}} {{range .Config.Cmd}} {{.}} {{end}}&#39; &lt;container_id&gt;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[MongoDB 업그레이드 시 데이터 무결성 체크]]></title>
            <link>https://velog.io/@taeni-develop/MongoDB-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AC%B4%EA%B2%B0%EC%84%B1-%EC%B2%B4%ED%81%AC</link>
            <guid>https://velog.io/@taeni-develop/MongoDB-%EC%97%85%EA%B7%B8%EB%A0%88%EC%9D%B4%EB%93%9C-%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AC%B4%EA%B2%B0%EC%84%B1-%EC%B2%B4%ED%81%AC</guid>
            <pubDate>Thu, 16 Oct 2025 01:44:20 GMT</pubDate>
            <description><![CDATA[<h2 id="데이터-입력">데이터 입력</h2>
<pre><code class="language-javascript">/* ===== 00_insert_verify_data.js ===== */
/* 목적: 업그레이드 전후 검증용 더미데이터 생성 (verify_* 네임스페이스) */

const DB_NAME = &quot;prd-sre-test-jti-create-test002&quot;;
const PREFIX = &quot;verify_&quot;;

const dbx = db.getSiblingDB(DB_NAME);
const colUsers = dbx.getCollection(PREFIX + &quot;users&quot;);
const colProducts = dbx.getCollection(PREFIX + &quot;products&quot;);
const colOrders = dbx.getCollection(PREFIX + &quot;orders&quot;);

const COUNTS = { users: 2000, products: 500, orders: 7000 };
const BATCH_SIZE = 500;

function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
function randomString(len) {
  const chars = &quot;abcdefghijklmnopqrstuvwxyz&quot;;
  let s = &quot;&quot;; for (let i = 0; i &lt; len; i++) s += chars.charAt(Math.floor(Math.random()*chars.length));
  return s;
}
function randomTags() {
  const tags = [&quot;mochi&quot;,&quot;fruit&quot;,&quot;gift&quot;,&quot;dessert&quot;,&quot;coffee&quot;,&quot;kids&quot;,&quot;premium&quot;];
  const n = randInt(1,3);
  return Array.from({length:n}, () =&gt; pick(tags));
}

// 인덱스
colUsers.createIndex({ email: 1 }, { unique: true });
colProducts.createIndex({ sku: 1 }, { unique: true });
colOrders.createIndex({ userId: 1, createdAt: -1 });

// Users
print(&quot;== inserting users ==&quot;);
let users = [];
for (let i = 0; i &lt; COUNTS.users; i++) {
  users.push({
    _id: new ObjectId(),
    email: `user${i}@test.com`,
    name: `User ${i}`,
    age: randInt(18,60),
    joinedAt: new Date(Date.now() - randInt(0,1000)*86400000),
    city: pick([&quot;Seoul&quot;,&quot;Suwon&quot;,&quot;Busan&quot;,&quot;Incheon&quot;]),
    tier: pick([&quot;basic&quot;,&quot;silver&quot;,&quot;gold&quot;,&quot;vip&quot;])
  });
  if (users.length &gt;= BATCH_SIZE) { colUsers.insertMany(users, { ordered:false }); users=[]; }
}
if (users.length) colUsers.insertMany(users, { ordered:false });

// Products
print(&quot;== inserting products ==&quot;);
let products = [];
for (let i = 0; i &lt; COUNTS.products; i++) {
  products.push({
    _id: new ObjectId(),
    sku: `SKU-${String(i).padStart(5,&quot;0&quot;)}`,
    name: `Product ${i} ${randomString(randInt(3,12))}`,
    price: Math.round((Math.random()*50000 + 5000))/100,
    tags: randomTags(),
    createdAt: new Date(Date.now() - randInt(0,365)*86400000)
  });
  if (products.length &gt;= BATCH_SIZE) { colProducts.insertMany(products, { ordered:false }); products=[]; }
}
if (products.length) colProducts.insertMany(products, { ordered:false });

// Orders
print(&quot;== inserting orders ==&quot;);
const userIds = colUsers.find({}, { _id:1 }).toArray().map(d=&gt;d._id);
const skus = colProducts.find({}, { sku:1 }).toArray().map(d=&gt;d.sku);

let orders = [];
for (let i = 0; i &lt; COUNTS.orders; i++) {
  const items = Array.from({length: randInt(1,3)}).map(()=&gt;({
    sku: pick(skus),
    qty: randInt(1,5),
    price: Math.round((Math.random()*10000 + 500))/100
  }));
  orders.push({
    _id: new ObjectId(),
    userId: pick(userIds),
    items,
    total: items.reduce((a,b)=&gt;a + b.qty*b.price, 0),
    status: pick([&quot;new&quot;,&quot;paid&quot;,&quot;shipped&quot;,&quot;cancelled&quot;]),
    createdAt: new Date(Date.now() - randInt(0,120)*86400000)
  });
  if (orders.length &gt;= BATCH_SIZE) { colOrders.insertMany(orders, { ordered:false }); orders=[]; }
}
if (orders.length) colOrders.insertMany(orders, { ordered:false });

print(&quot;✅ verify data inserted&quot;);
print(&quot;counts:&quot;, {
  users: colUsers.estimatedDocumentCount(),
  products: colProducts.estimatedDocumentCount(),
  orders: colOrders.estimatedDocumentCount()
});</code></pre>
<h2 id="1-업그레이드-전-스냅샷-같은-db-내-저장">1. 업그레이드 “전” 스냅샷 (같은 DB 내 저장)</h2>
<pre><code class="language-javascript">/* ===== 01_pre_snapshot_fixed.js ===== */

/* ===== 01_pre_snapshot_fixed.js ===== */

const DB_NAME = &quot;prd-sre-test-jti-create-test002&quot;;
const SNAP_COLL = &quot;__verify_snapshots&quot;;
const EXCLUDE_PREFIX = &quot;__verify_&quot;;
const ALLOWLIST = []; // 필요 시 [&quot;verify_users&quot;, ...] 처럼 제한

const dbx = db.getSiblingDB(DB_NAME);
const snaps = dbx.getCollection(SNAP_COLL);

function listCollections() {
  return dbx.runCommand({ listCollections: 1, nameOnly: false }).cursor.firstBatch;
}
function isIncluded(name) {
  if (name.startsWith(EXCLUDE_PREFIX)) return false;
  if (ALLOWLIST.length === 0) return true;
  return ALLOWLIST.includes(name);
}
function helloSafe() {
  try { return db.adminCommand({ hello: 1 }); }
  catch (e) { try { return db.isMaster(); } catch (e2) { return {}; } }
}
function isMongos(hello) {
  // mongos일 때 hello.msg === &quot;isdbgrid&quot; 가 통상적으로 존재
  return hello &amp;&amp; (hello.msg === &quot;isdbgrid&quot;);
}
function fcwSafe() { // FCV 안전 폴백
  try {
    const r = db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 });
    if (r &amp;&amp; r.featureCompatibilityVersion) return { source: &quot;getParameter&quot;, value: r.featureCompatibilityVersion };
  } catch (_) {}
  try {
    const doc = db.getSiblingDB(&quot;admin&quot;).system.version.findOne({ _id: &quot;featureCompatibilityVersion&quot; });
    if (doc) return { source: &quot;system.version&quot;, value: { version: doc.version, targetVersion: doc.targetVersion ?? null } };
  } catch (_) {}
  try { return { source: &quot;fallback&quot;, value: { serverVersion: db.version() } }; } catch (_) { return { source: &quot;fallback&quot;, value: null }; }
}
function collStatsSafe(n) { try { return dbx.runCommand({ collStats: n, scale: 1 }); } catch (e) { return { ok:0, errmsg: e.message }; } }
function indexesSafe(n) { try { return dbx.getCollection(n).getIndexes(); } catch (e) { return [{ _error: e.message }]; } }

// dbHash 가능 시 사용
function dbHashSafe(names) {
  try { return dbx.runCommand({ dbHash: 1, collections: names, full: true }); }
  catch (e) { return { ok: 0, errmsg: e.message }; }
}

// mongos 폴백: 결정적 샘플 체크섬
function sampleChecksum(collName, opts = { limitPerSide: 1000, stride: 100 }) {
  const { limitPerSide, stride } = opts;
  const col = dbx.getCollection(collName);

  function rollingHashOfCursor(cur) {
    let h = 0; let i = 0;
    cur.forEach(doc =&gt; {
      if ((i++ % stride) !== 0) return; // stride 간격 샘플링(결정적)
      const s = JSON.stringify(doc); // 기본은 전체 문서 직렬화(필요시 프로젝션으로 조정)
      for (let k = 0; k &lt; s.length; k++) h = ((h &lt;&lt; 5) - h + s.charCodeAt(k)) | 0;
    });
    return h|0;
  }

  // 앞쪽 일부(_id 오름차순) + 뒤쪽 일부(_id 내림차순)를 합쳐서 결정적 샘플
  const fwd = col.find({}).sort({ _id: 1 }).limit(limitPerSide);
  const rev = col.find({}).sort({ _id: -1 }).limit(limitPerSide);

  const hf = rollingHashOfCursor(fwd);
  const hr = rollingHashOfCursor(rev);
  const combined = (((hf &amp; 0xffff) &lt;&lt; 16) ^ (hr &amp; 0xffff)) | 0;

  return { hash32: combined, forwardSample: limitPerSide, backwardSample: limitPerSide, stride };
}

const snapshotId = new ObjectId();
print(`[PRE] start for ${DB_NAME}, snapshotId=${snapshotId}`);

const hello = helloSafe();
const atMongos = isMongos(hello);
const fcv = fcwSafe();

const colMetaAll = listCollections().map(c =&gt; ({
  name: c.name,
  type: c.type,
  options: {
    validator: c.options?.validator ?? null,
    timeseries: c.options?.timeseries ?? null,
    clusteredIndex: c.options?.clusteredIndex ?? null
  }
}));

const targetCols = colMetaAll.filter(c =&gt; c.type === &quot;collection&quot; &amp;&amp; isIncluded(c.name)).map(c =&gt; c.name);

// 공통 수집
const stats = {}, indexes = {};
targetCols.forEach(n =&gt; { stats[n] = collStatsSafe(n); indexes[n] = indexesSafe(n); });

// 해시 수집(분기)
let hashMode = &quot;&quot;;
let hashPayload = {};
if (!atMongos) {
  const dh = dbHashSafe(targetCols);
  if (dh &amp;&amp; dh.ok === 1) {
    hashMode = &quot;dbHash&quot;;
    hashPayload = dh; // { collections: {name:{md5:..}}, md5:.. }
  } else {
    hashMode = &quot;sample32&quot;;
    // mongod인데도 실패하면 폴백
    const samples = {};
    targetCols.forEach(n =&gt; { samples[n] = sampleChecksum(n); });
    hashPayload = { samples };
  }
} else {
  hashMode = &quot;sample32&quot;; // mongos → dbHash 불가
  const samples = {};
  targetCols.forEach(n =&gt; { samples[n] = sampleChecksum(n); });
  hashPayload = { samples };
}

snaps.insertOne({
  _id: snapshotId,
  role: &quot;pre&quot;,
  db: DB_NAME,
  createdAt: new Date(),
  hello,
  fcv,
  hashMode,         // &quot;dbHash&quot; | &quot;sample32&quot;
  hashPayload,      // dbHash 결과 or 샘플 해시 맵
  collections: colMetaAll,
  targetCols,
  stats,
  indexes
}, { writeConcern: { w: &quot;majority&quot; } });

print(`[PRE] saved (mode=${hashMode}).`);
</code></pre>
<h2 id="2업그레이드-후-비교-같은-db-내-저장">2.업그레이드 “후” 비교 (같은 DB 내 저장)</h2>
<pre><code class="language-javascript">/* ===== 02_post_compare_compass_fixed.js ===== */

(() =&gt; {
  const DB_NAME = &quot;prd-sre-test-jti-create-test002&quot;;
  const SNAP_COLL = &quot;__verify_snapshots&quot;;
  const EXCLUDE_PREFIX = &quot;__verify_&quot;;
  const ALLOWLIST = [];

  const dbx = db.getSiblingDB(DB_NAME);
  const snaps = dbx.getCollection(SNAP_COLL);

  function latestPre() { return snaps.find({ db: DB_NAME, role: &quot;pre&quot; }).sort({ createdAt: -1 }).limit(1).toArray()[0] || null; }
  function listCollections() { return dbx.runCommand({ listCollections: 1, nameOnly: false }).cursor.firstBatch; }
  function isIncluded(name) { if (name.startsWith(EXCLUDE_PREFIX)) return false; if (ALLOWLIST.length===0) return true; return ALLOWLIST.includes(name); }
  function collStatsSafe(n){ try { return dbx.runCommand({ collStats: n, scale: 1 }); } catch(e){ return { ok:0, errmsg: e.message }; } }
  function indexesSafe(n){ try { return dbx.getCollection(n).getIndexes(); } catch(e){ return [{ _error: e.message }]; } }
  function normalizeIndex(ix){
    const keep={}; [&quot;name&quot;,&quot;key&quot;,&quot;unique&quot;,&quot;partialFilterExpression&quot;,&quot;sparse&quot;,&quot;expireAfterSeconds&quot;,&quot;weights&quot;,&quot;default_language&quot;,&quot;language_override&quot;,&quot;textIndexVersion&quot;,&quot;wildcardProjection&quot;,&quot;collation&quot;]
      .forEach(k=&gt;{ if (ix[k] !== undefined) keep[k]=ix[k]; });
    return keep;
  }
  function helloSafe(){ try { return db.adminCommand({ hello: 1 }); } catch(e){ try{ return db.isMaster(); } catch(e2){ return {}; } } }
  function isMongos(hello){ return hello &amp;&amp; (hello.msg === &quot;isdbgrid&quot;); }
  function dbHashSafe(names){ try { return dbx.runCommand({ dbHash: 1, collections: names, full: true }); } catch(e){ return { ok:0, errmsg:e.message }; } }

  // 샘플 체크섬 (PRE와 동일 방식 유지해야 비교 가능)
  function sampleChecksum(collName, opts = { limitPerSide: 1000, stride: 100 }) {
    const { limitPerSide, stride } = opts;
    const col = dbx.getCollection(collName);
    function rollingHashOfCursor(cur){
      let h=0, i=0; cur.forEach(doc=&gt;{
        if ((i++ % stride) !== 0) return;
        const s = JSON.stringify(doc);
        for (let k=0;k&lt;s.length;k++) h = ((h&lt;&lt;5) - h + s.charCodeAt(k)) | 0;
      }); return h|0;
    }
    const fwd = col.find({}).sort({ _id: 1 }).limit(limitPerSide);
    const rev = col.find({}).sort({ _id: -1 }).limit(limitPerSide);
    const hf = rollingHashOfCursor(fwd);
    const hr = rollingHashOfCursor(rev);
    const combined = (((hf &amp; 0xffff) &lt;&lt; 16) ^ (hr &amp; 0xffff)) | 0;
    return { hash32: combined, forwardSample: limitPerSide, backwardSample: limitPerSide, stride };
  }

  const pre = latestPre();
  if (!pre) {
    print(&quot;ERROR: No PRE snapshot found. 먼저 01_pre_snapshot_fixed.js를 실행해 주세요.&quot;);
    return; // Compass 안전
  }

  print(`[POST] comparing with PRE ${pre._id} @ ${pre.createdAt}, mode=${pre.hashMode}`);

  const hello = helloSafe();
  const atMongos = isMongos(hello);

  const colMetaNowAll = listCollections().map(c =&gt; ({
    name: c.name, type: c.type,
    options: {
      validator: c.options?.validator ?? null,
      timeseries: c.options?.timeseries ?? null,
      clusteredIndex: c.options?.clusteredIndex ?? null
    }
  }));
  const targetColsNow = colMetaNowAll.filter(c =&gt; c.type===&quot;collection&quot; &amp;&amp; isIncluded(c.name)).map(c =&gt; c.name);

  const statsNow = {}, indexesNow = {};
  targetColsNow.forEach(n =&gt; { statsNow[n] = collStatsSafe(n); indexesNow[n] = indexesSafe(n).map(normalizeIndex); });

  // === 비교 섹션 ===
  function diffCounts(preStats, nowStats){
    const out=[]; const names = new Set([...Object.keys(preStats||{}), ...Object.keys(nowStats||{})]);
    names.forEach(n=&gt;{
      if (!isIncluded(n)) return;
      const a = preStats?.[n], b = nowStats?.[n];
      const ac = a?.count, bc = b?.count;
      if (ac !== bc) out.push({ collection:n, count: `${ac} → ${bc}` });
    }); return out;
  }
  function diffIndexes(preIx, nowIx){
    const out=[]; const names = new Set([...Object.keys(preIx||{}), ...Object.keys(nowIx||{})]);
    const sig = x =&gt; JSON.stringify(x);
    names.forEach(n=&gt;{
      if (!isIncluded(n)) return;
      const A = (preIx?.[n]||[]).map(normalizeIndex);
      const B = (nowIx?.[n]||[]).map(normalizeIndex);
      const SA = new Set(A.map(sig)), SB = new Set(B.map(sig));
      const missing = A.filter(x=&gt;!SB.has(sig(x)));
      const added = B.filter(x=&gt;!SA.has(sig(x)));
      if (missing.length || added.length) out.push({ collection:n, missingFromPost:missing, addedInPost:added });
    }); return out;
  }
  function diffOptions(preColsAll, nowColsAll){
    const out=[]; const preMap=Object.fromEntries((preColsAll||[]).map(x=&gt;[x.name,x])); const nowMap=Object.fromEntries((nowColsAll||[]).map(x=&gt;[x.name,x]));
    const names=new Set([...Object.keys(preMap), ...Object.keys(nowMap)]);
    names.forEach(n=&gt;{
      if (!isIncluded(n)) return;
      const a = preMap[n], b = nowMap[n];
      if (!a &amp;&amp; b) out.push({ collection:n, change:&quot;new collection&quot; });
      else if (a &amp;&amp; !b) out.push({ collection:n, change:&quot;collection removed&quot; });
      else if (a &amp;&amp; b) {
        const diffs=[]; [&quot;validator&quot;,&quot;timeseries&quot;,&quot;clusteredIndex&quot;].forEach(k=&gt;{
          if (JSON.stringify(a.options?.[k]) !== JSON.stringify(b.options?.[k])) diffs.push({ field:k, pre:a.options?.[k], post:b.options?.[k] });
        });
        if (diffs.length) out.push({ collection:n, diffs });
      }
    }); return out;
  }

  // 해시 비교 (모드에 따라)
  let hashDiff = [];
  if (pre.hashMode === &quot;dbHash&quot;) {
    // 가능하면 dbHash 재사용(현재 mongod에서 실행일 때만)
    const dh = dbHashSafe(targetColsNow);
    if (dh &amp;&amp; dh.ok === 1) {
      const preMap = pre.hashPayload?.collections || {};
      const nowMap = dh.collections || {};
      const names = new Set([...Object.keys(preMap), ...Object.keys(nowMap)]);
      names.forEach(n=&gt;{
        if (!isIncluded(n)) return;
        const a = preMap[n], b = nowMap[n];
        if (!a &amp;&amp; b) hashDiff.push({ collection:n, change:&quot;added(no PRE hash)&quot; });
        else if (a &amp;&amp; !b) hashDiff.push({ collection:n, change:&quot;removed(no POST hash)&quot; });
        else if (a.md5 !== b.md5) hashDiff.push({ collection:n, change:`hash changed ${a.md5} → ${b.md5}` });
      });
    } else {
      // PRE는 dbHash였지만 지금은 mongos이거나 실패 → sample32 폴백으로 재검증
      const samplesNow = {};
      targetColsNow.forEach(n =&gt; { samplesNow[n] = sampleChecksum(n); });
      hashDiff.push({ note: &quot;dbHash unavailable at POST; used sample32 fallback&quot;, samplesNow });
    }
  } else {
    // PRE가 sample32였음 → 같은 방식으로 비교
    const preSamples = pre.hashPayload?.samples || {};
    const diff = [];
    targetColsNow.forEach(n=&gt;{
      if (!isIncluded(n)) return;
      const nowS = sampleChecksum(n);
      const preS = preSamples[n];
      if (!preS) diff.push({ collection:n, change:&quot;no PRE sample&quot; });
      else if (preS.hash32 !== nowS.hash32) diff.push({ collection:n, change:`sample hash ${preS.hash32} → ${nowS.hash32}`, pre:preS, post:nowS });
    });
    // 새로 생긴/사라진 컬렉션도 기록
    const preNames = new Set(Object.keys(preSamples));
    const nowNames = new Set(targetColsNow);
    [...nowNames].forEach(n=&gt;{ if (!preNames.has(n)) diff.push({ collection:n, change:&quot;added in POST&quot; }); });
    [...preNames].forEach(n=&gt;{ if (!nowNames.has(n)) diff.push({ collection:n, change:&quot;removed in POST&quot; }); });
    hashDiff = diff;
  }

  const statsDiff = diffCounts(pre.stats, statsNow);
  const idxDiff = diffIndexes(pre.indexes, indexesNow);
  const optDiff = diffOptions(pre.collections, colMetaNowAll);

  print(&quot;\n=== HASH DIFF ===&quot;); printjson(hashDiff);
  print(&quot;\n=== COUNT/STATS DIFF ===&quot;); printjson(statsDiff);
  print(&quot;\n=== INDEX DIFF ===&quot;); printjson(idxDiff);
  print(&quot;\n=== COLLECTION OPTION DIFF ===&quot;); printjson(optDiff);

  snaps.insertOne({
    role: &quot;post&quot;,
    db: DB_NAME,
    createdAt: new Date(),
    compareWith: pre._id,
    postHello: hello,
    hashModePre: pre.hashMode,
    result: { hashDiff, statsDiff, idxDiff, optDiff },
    statsNow, indexesNow, colMetaNowAll
  }, { writeConcern: { w: &quot;majority&quot; } });

  print(&quot;\n[POST] saved &amp; compare finished (mongos/mongod auto).&quot;);
})();</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Redis Live Migration을 위한 K8S Redisshake Pod 생성]]></title>
            <link>https://velog.io/@taeni-develop/Redis-Live-Migration%EC%9D%84-%EC%9C%84%ED%95%9C-K8S-Redisshake-Pod-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@taeni-develop/Redis-Live-Migration%EC%9D%84-%EC%9C%84%ED%95%9C-K8S-Redisshake-Pod-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Thu, 17 Jul 2025 00:35:08 GMT</pubDate>
            <description><![CDATA[<h3 id="pod-생성-yaml">POD 생성 yaml</h3>
<pre><code class="language-yaml">
apiVersion: v1
kind: Pod
metadata:
  name: redis-shake
  namespace: redis-shake
  labels:
    app: redis-shake
spec:
  containers:
  - name: redis-shake
    image: rmhewedy/redis-shake:latest # 최신 버전을 사용하거나 특정 태그를 지정하세요.
    command: [&quot;/url/local/bin/redis-shake&quot;]
    args:
      - &quot;/etc/redis-shake/shake.toml&quot;
    volumeMounts:
    - name: redis-shake-config
      mountPath: /etc/redis-shake
  volumes:
  - name: redis-shake-config
    configMap:
      name: redis-shake-configmap
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-shake-configmap
  namespace: redis-shake
data:
  shake.toml: |

    [sync_reader] 
    cluster = false # 클러스터 여부
    address = &quot;IP:PORT&quot; # 원본 Redis IP:PORT
    username = &quot;&quot;
    password = &quot;패스워드&quot;
    tls = false
    sync_rdb = true
    sync_aof = true
    prefer_replica = true

    [redis_writer]
    cluster = false # 클러스터 여부
    address = &quot;IP:PORT&quot; # 대상 Redis IP:PORT
    username = &quot;&quot;
    password = &quot;패스워드&quot;
    tls = false
    sync_rdb = true
    sync_aof = true
    prefer_replica = true</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[mongodb wiredTiger 정보 조회]]></title>
            <link>https://velog.io/@taeni-develop/mongodb-wiredTiger-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C</link>
            <guid>https://velog.io/@taeni-develop/mongodb-wiredTiger-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C</guid>
            <pubDate>Mon, 21 Apr 2025 08:13:31 GMT</pubDate>
            <description><![CDATA[<pre><code>var wt = db.serverStatus().wiredTiger.cache;
printjson({
  &quot;bytes currently in the cache&quot;: wt[&quot;bytes currently in the cache&quot;],
  &quot;dirty bytes in cache&quot;: wt[&quot;tracked dirty bytes in the cache&quot;],
  &quot;pages read into cache&quot;: wt[&quot;pages read into cache&quot;],
  &quot;pages written from cache&quot;: wt[&quot;pages written from cache&quot;],
  &quot;unmodified pages evicted&quot;: wt[&quot;unmodified pages evicted&quot;],
  &quot;modified pages evicted&quot;: wt[&quot;internal pages evicted&quot;]
});</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[promql]]></title>
            <link>https://velog.io/@taeni-develop/promql</link>
            <guid>https://velog.io/@taeni-develop/promql</guid>
            <pubDate>Tue, 01 Apr 2025 02:04:16 GMT</pubDate>
            <description><![CDATA[<h4 id="7일간-최대-memory-사용률">7일간 최대 memory 사용률</h4>
<pre><code>max by (namespace, statefulset) (
  label_replace(
    max_over_time(container_memory_usage_bytes{container=&quot;backup-agent&quot;}[7d]),
    &quot;statefulset&quot;, &quot;$1&quot;, &quot;pod&quot;, &quot;^(.*)-[0-9]+$&quot;
  )
)</code></pre><h4 id="7일간-최대-cpu-사용률">7일간 최대 cpu 사용률</h4>
<pre><code>max by (namespace, statefulset) (
  label_replace(
    max_over_time(
      rate(container_cpu_usage_seconds_total{container=&quot;backup-agent&quot;}[5m])[7d:]
    ) * 1000,
    &quot;statefulset&quot;, &quot;$1&quot;, &quot;pod&quot;, &quot;^(.*)-[0-9]+$&quot;
  )
)</code></pre><h4 id="memory-sum">MEMORY SUM</h4>
<pre><code>{ &quot;size&quot;: 0, &quot;query&quot;: { &quot;bool&quot;: { &quot;must&quot;: [ { &quot;range&quot;: { &quot;@timestamp&quot;: { &quot;gte&quot;: &quot;now-1h&quot;, &quot;lte&quot;: &quot;now&quot; } } }, { &quot;prefix&quot;: { &quot;service.address&quot;: &quot;10.156.133.121&quot; } } ] } }, &quot;aggs&quot;: { &quot;max_memory_used_rss&quot;: { &quot;max&quot;: { &quot;field&quot;: &quot;redis.info.memory.used.rss&quot; } }, &quot;total_max_memory_used_rss&quot;: { &quot;sum&quot;: { &quot;field&quot;: &quot;redis.info.memory.used.rss&quot; } } } }
</code></pre><h3 id="memory-사용률-40-미만">memory 사용률 40% 미만</h3>
<pre><code>GET your_index/_search
{
  &quot;_source&quot;: [
    &quot;rscId&quot;,
    &quot;service.address&quot;
  ],
  &quot;script_fields&quot;: {
    &quot;used_memory_percent&quot;: {
      &quot;script&quot;: {
        &quot;source&quot;: &quot;doc[&#39;redis.info.memory.used.value&#39;].value * 100 / doc[&#39;redis.info.memory.max.value&#39;].value&quot;,
        &quot;lang&quot;: &quot;painless&quot;
      }
    }
  },
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        {
          &quot;range&quot;: {
            &quot;redis.info.memory.max.value&quot;: {
              &quot;gte&quot;: 4294967296
            }
          }
        },
        {
          &quot;script&quot;: {
            &quot;script&quot;: {
              &quot;source&quot;: &quot;doc[&#39;redis.info.memory.used.value&#39;].value &lt; doc[&#39;redis.info.memory.max.value&#39;].value * 0.4&quot;,
              &quot;lang&quot;: &quot;painless&quot;
            }
          }
        }
      ]
    }
  }
}</code></pre><h4 id="rscid로-group">rscId로 group</h4>
<pre><code>GET your_index/_search
{
  &quot;size&quot;: 0,
  &quot;aggs&quot;: {
    &quot;group_by_rscId&quot;: {
      &quot;terms&quot;: {
        &quot;field&quot;: &quot;rscId.keyword&quot;,
        &quot;size&quot;: 10000
      },
      &quot;aggs&quot;: {
        &quot;group_by_service_address&quot;: {
          &quot;terms&quot;: {
            &quot;field&quot;: &quot;service.address.keyword&quot;,
            &quot;size&quot;: 10000
          },
          &quot;aggs&quot;: {
            &quot;top_hits&quot;: {
              &quot;top_hits&quot;: {
                &quot;_source&quot;: [&quot;rscId&quot;, &quot;service.address&quot;, &quot;redis.info.memory.max.value&quot;],
                &quot;size&quot;: 1
              }
            },
            &quot;avg_used_memory_percent&quot;: {
              &quot;bucket_script&quot;: {
                &quot;buckets_path&quot;: {
                  &quot;used&quot;: &quot;avg_used_memory&quot;,
                  &quot;max&quot;: &quot;avg_max_memory&quot;
                },
                &quot;script&quot;: &quot;params.used * 100 / params.max&quot;
              }
            },
            &quot;avg_used_memory&quot;: {
              &quot;avg&quot;: {
                &quot;field&quot;: &quot;redis.info.memory.used.value&quot;
              }
            },
            &quot;avg_max_memory&quot;: {
              &quot;avg&quot;: {
                &quot;field&quot;: &quot;redis.info.memory.max.value&quot;
              }
            }
          }
        }
      }
    }
  },
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: [
        {
          &quot;range&quot;: {
            &quot;redis.info.memory.max.value&quot;: {
              &quot;gte&quot;: 4294967296
            }
          }
        },
        {
          &quot;script&quot;: {
            &quot;script&quot;: {
              &quot;source&quot;: &quot;doc[&#39;redis.info.memory.used.value&#39;].value &lt; doc[&#39;redis.info.memory.max.value&#39;].value * 0.4&quot;,
              &quot;lang&quot;: &quot;painless&quot;
            }
          }
        }
      ]
    }
  }
}</code></pre><pre><code class="language-javascript">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Elasticsearch 결과 변환&lt;/title&gt;
    &lt;style&gt;
        body { font-family: Arial, sans-serif; margin: 20px; }
        textarea { width: 100%; height: 200px; margin-top: 10px; }
        button { margin: 10px 0; padding: 10px; cursor: pointer; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #f4f4f4; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

    &lt;h2&gt;Elasticsearch 결과 변환&lt;/h2&gt;

    &lt;label for=&quot;jsonInput&quot;&gt;Elasticsearch JSON 응답 입력:&lt;/label&gt;
    &lt;textarea id=&quot;jsonInput&quot; placeholder=&quot;여기에 Elasticsearch JSON 데이터를 입력하세요...&quot;&gt;&lt;/textarea&gt;
    &lt;button onclick=&quot;processInput()&quot;&gt;변환하기&lt;/button&gt;

    &lt;table id=&quot;resultTable&quot;&gt;
        &lt;thead&gt;
            &lt;tr&gt;
                &lt;th&gt;rscId&lt;/th&gt;
                &lt;th&gt;Service Address&lt;/th&gt;
                &lt;th&gt;Max Memory&lt;/th&gt;
                &lt;th&gt;Max Used Memory Percent&lt;/th&gt;
            &lt;/tr&gt;
        &lt;/thead&gt;
        &lt;tbody&gt;
            &lt;!-- 결과가 여기에 추가됨 --&gt;
        &lt;/tbody&gt;
    &lt;/table&gt;

    &lt;script&gt;
        function formatElasticsearchData(response) {
            const result = [];

            try {
                response.aggregations.group_by_rscId.buckets.forEach(rscBucket =&gt; {
                    const rscId = rscBucket.key;

                    rscBucket.group_by_service_address.buckets.forEach(serviceBucket =&gt; {
                        const serviceAddress = serviceBucket.key;
                        const maxMemory = serviceBucket.top_hits.hits.hits[0]._source[&quot;redis.info.memory.max.value&quot;];
                        const maxUsedMemoryPercent = serviceBucket.avg_used_memory_percent.value.toFixed(2); // 소수점 2자리

                        result.push({ rscId, serviceAddress, maxMemory, maxUsedMemoryPercent });
                    });
                });
            } catch (error) {
                alert(&quot;잘못된 JSON 형식입니다. 올바른 데이터를 입력하세요.&quot;);
                console.error(&quot;Parsing error:&quot;, error);
                return [];
            }

            return result;
        }

        function processInput() {
            const inputText = document.getElementById(&quot;jsonInput&quot;).value.trim();
            if (!inputText) {
                alert(&quot;JSON 데이터를 입력하세요.&quot;);
                return;
            }

            try {
                const jsonData = JSON.parse(inputText);
                const results = formatElasticsearchData(jsonData);
                displayResults(results);
            } catch (error) {
                alert(&quot;JSON 파싱에 실패했습니다. 올바른 JSON을 입력하세요.&quot;);
                console.error(&quot;JSON Parse Error:&quot;, error);
            }
        }

        function displayResults(results) {
            const tableBody = document.querySelector(&quot;#resultTable tbody&quot;);
            tableBody.innerHTML = &quot;&quot;; // 기존 내용 초기화

            if (results.length === 0) {
                tableBody.innerHTML = &quot;&lt;tr&gt;&lt;td colspan=&#39;4&#39; style=&#39;text-align:center;&#39;&gt;데이터가 없습니다.&lt;/td&gt;&lt;/tr&gt;&quot;;
                return;
            }

            results.forEach(item =&gt; {
                const row = `&lt;tr&gt;
                    &lt;td&gt;${item.rscId}&lt;/td&gt;
                    &lt;td&gt;${item.serviceAddress}&lt;/td&gt;
                    &lt;td&gt;${item.maxMemory.toLocaleString()} Bytes&lt;/td&gt;
                    &lt;td&gt;${item.maxUsedMemoryPercent} %&lt;/td&gt;
                &lt;/tr&gt;`;
                tableBody.innerHTML += row;
            });
        }
    &lt;/script&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>
<pre><code>{
  &quot;query&quot;: {
    &quot;bool&quot;: {
      &quot;must&quot;: {
        &quot;script&quot;: {
          &quot;script&quot;: {
            &quot;source&quot;: &quot;&quot;&quot;
              if (!doc.containsKey(&#39;redis.info.memory.used.rss&#39;) || !doc.containsKey(&#39;redis.info.memory.max.value&#39;)) {
                return false;
              }
              if (doc[&#39;redis.info.memory.used.rss&#39;].size() == 0 || doc[&#39;redis.info.memory.max.value&#39;].size() == 0) {
                return false;
              }
              return doc[&#39;redis.info.memory.used.rss&#39;].value &gt; doc[&#39;redis.info.memory.max.value&#39;].value;
            &quot;&quot;&quot;,
            &quot;lang&quot;: &quot;painless&quot;
          }
        }
      },
      &quot;must_not&quot;: {
        &quot;prefix&quot;: {
          &quot;rscId&quot;: &quot;cube&quot;
        }
      }
    }
  }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[MongoDB Benchmark ]]></title>
            <link>https://velog.io/@taeni-develop/MongoDB-Benchmark</link>
            <guid>https://velog.io/@taeni-develop/MongoDB-Benchmark</guid>
            <pubDate>Wed, 26 Mar 2025 06:35:14 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-python">from pymongo import MongoClient
import time

# MongoDB 연결
DATABASE_NAME = &quot;benchmark_db&quot;  # 데이터베이스 이름을 변수로 처리
client = MongoClient(&quot;mongodb://localhost:27017/&quot;)
db = client[DATABASE_NAME]
collection = db[&quot;benchmark_collection&quot;]

# 테스트할 문서 개수 설정
NUM_DOCS = 10000  # 문서 수

def generate_document(doc_id):
    &quot;&quot;&quot;문서 생성 함수&quot;&quot;&quot;
    return {&quot;_id&quot;: doc_id, &quot;value&quot;: doc_id, &quot;index_value&quot;: doc_id}

def drop_database():
    &quot;&quot;&quot;테스트 시작 전에 데이터베이스 드롭&quot;&quot;&quot;
    client.drop_database(DATABASE_NAME)  # 변수로 설정한 데이터베이스 이름으로 삭제
    print(f&quot;[DATABASE] &#39;{DATABASE_NAME}&#39; 데이터베이스가 삭제되었습니다.&quot;)

def benchmark_insert():
    &quot;&quot;&quot;MongoDB Insert 성능 테스트&quot;&quot;&quot;
    collection.delete_many({})  # 기존 데이터 삭제

    total_time = 0
    for i in range(NUM_DOCS):
        start = time.time()
        doc = generate_document(i)
        collection.insert_one(doc)
        end = time.time()
        total_time += (end - start)

    avg_time = total_time / NUM_DOCS
    print(f&quot;[INSERT] {NUM_DOCS}개 문서 삽입 평균 시간: {avg_time:.4f}초&quot;)

def benchmark_find_without_index():
    &quot;&quot;&quot;MongoDB Collection Scan (인덱스 없이) 성능 테스트&quot;&quot;&quot;
    total_time = 0
    count = 0
    for i in range(NUM_DOCS):
        start = time.time()
        doc = collection.find_one({&quot;value&quot;: i})  # value로 조회
        end = time.time()
        total_time += (end - start)
        if doc:
            count += 1

    avg_time = total_time / NUM_DOCS
    print(f&quot;[FIND] 컬렉션 스캔 (인덱스 없이) 평균 조회 시간: {avg_time:.4f}초, 조회된 문서 수: {count}&quot;)

def benchmark_create_index():
    &quot;&quot;&quot;MongoDB 인덱스 생성&quot;&quot;&quot;
    start = time.time()
    collection.create_index([(&quot;index_value&quot;, 1)])  # &#39;index_value&#39; 필드에 인덱스 생성
    end = time.time()

    print(f&quot;[INDEX] &#39;index_value&#39; 필드에 인덱스 생성 시간: {end - start:.4f}초&quot;)

def benchmark_find_with_index():
    &quot;&quot;&quot;MongoDB Index Scan (인덱스 사용) 성능 테스트&quot;&quot;&quot;
    benchmark_create_index()  # 인덱스 생성 후 테스트

    total_time = 0
    count = 0
    for i in range(NUM_DOCS):
        start = time.time()
        doc = collection.find_one({&quot;index_value&quot;: i})  # index_value로 조회
        end = time.time()
        total_time += (end - start)
        if doc:
            count += 1

    avg_time = total_time / NUM_DOCS
    print(f&quot;[FIND] 인덱스 스캔 (인덱스 사용) 평균 조회 시간: {avg_time:.4f}초, 조회된 문서 수: {count}&quot;)

def benchmark_update_without_index():
    &quot;&quot;&quot;MongoDB Update (인덱스 없이) 성능 테스트&quot;&quot;&quot;
    total_time = 0
    for i in range(NUM_DOCS):
        start = time.time()
        collection.update_one({&quot;_id&quot;: i}, {&quot;$set&quot;: {&quot;value&quot;: 999999}})  # value 업데이트
        end = time.time()
        total_time += (end - start)

    avg_time = total_time / NUM_DOCS
    print(f&quot;[UPDATE] 컬렉션 스캔 (인덱스 없이) 평균 업데이트 시간: {avg_time:.4f}초&quot;)

def benchmark_update_with_index():
    &quot;&quot;&quot;MongoDB Update (인덱스 사용) 성능 테스트&quot;&quot;&quot;
    total_time = 0
    for i in range(NUM_DOCS):
        start = time.time()
        collection.update_one({&quot;index_value&quot;: i}, {&quot;$set&quot;: {&quot;index_value&quot;: 888888}})  # index_value 업데이트
        end = time.time()
        total_time += (end - start)

    avg_time = total_time / NUM_DOCS
    print(f&quot;[UPDATE] 인덱스 스캔 (인덱스 사용) 평균 업데이트 시간: {avg_time:.4f}초&quot;)

def benchmark_delete_without_index():
    &quot;&quot;&quot;MongoDB Delete (인덱스 없이) 성능 테스트&quot;&quot;&quot;
    total_time = 0
    for i in range(NUM_DOCS):
        start = time.time()
        collection.delete_one({&quot;_id&quot;: i})  # _id로 삭제
        end = time.time()
        total_time += (end - start)

    avg_time = total_time / NUM_DOCS
    print(f&quot;[DELETE] 컬렉션 스캔 (인덱스 없이) 평균 삭제 시간: {avg_time:.4f}초&quot;)

def benchmark_delete_with_index():
    &quot;&quot;&quot;MongoDB Delete (인덱스 사용) 성능 테스트&quot;&quot;&quot;
    total_time = 0
    for i in range(NUM_DOCS):
        start = time.time()
        collection.delete_one({&quot;index_value&quot;: i})  # index_value로 삭제
        end = time.time()
        total_time += (end - start)

    avg_time = total_time / NUM_DOCS
    print(f&quot;[DELETE] 인덱스 스캔 (인덱스 사용) 평균 삭제 시간: {avg_time:.4f}초&quot;)

if __name__ == &quot;__main__&quot;:
    drop_database()  # 테스트 시작 전에 데이터베이스 드롭
    benchmark_insert()  # 데이터 삽입
    benchmark_find_without_index()  # 인덱스 없이 조회
    benchmark_find_with_index()  # 인덱스 사용하여 조회
    benchmark_update_without_index()  # 인덱스 없이 업데이트
    benchmark_update_with_index()  # 인덱스를 사용한 업데이트
    benchmark_delete_without_index()  # 인덱스 없이 삭제
    benchmark_delete_with_index()  # 인덱스를 사용한 삭제</code></pre>
<p>모든 collection compact</p>
<pre><code>mongo your_database_name -u your_user -p your_pass --authenticationDatabase admin --quiet --eval &#39;db.getCollectionNames().forEach(function(c){ print(&quot;Compacting: &quot; + c); printjson(db.runCommand({compact: c})); })&#39;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] Factory Pattern을 활용한 Service 관리2]]></title>
            <link>https://velog.io/@taeni-develop/Spring-Strategy-Pattern%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-Service-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@taeni-develop/Spring-Strategy-Pattern%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-Service-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Tue, 21 Mar 2023 02:27:15 GMT</pubDate>
            <description><![CDATA[<h3 id="factory-pattern을-활용한-service-관리2">Factory Pattern을 활용한 Service 관리2</h3>
<p>아래 코드는 pattern을 설명하기 위한 예시이므로 로직상에 어색함이 있을 수 있다.</p>
<h4 id="abstract-class">Abstract class</h4>
<pre><code class="language-java">@RequiredArgsConstructor
abstract class OrderService {
    private final UserDao userDao;

    abstract ProductType getProductType;
    abstract int getPoint(String productCode);
    abstract int getProductName(String productCode);

    public void order(Order order) {
        orderDao.insertOrderTemp(order);

        int point = getPoint(order.getProductCode());
        String productName = getProductName(order.getProductCode());

        order.setPorint(point);
        order.setProductName(productName);

        requestOrder(order);
    }

    private void validOrder(Order order) throws OrderException {
        // 주문 정보 유효성 체크
    }

    private void requestOrder(Order order) {
        // 주문 실행
    }
}</code></pre>
<h4 id="옷-주문-service">옷 주문 Service</h4>
<pre><code class="language-java">@Service
public ClothesOrderService extends OrderService {
    private final ClothesProductDao clothesProductDao;
    public ClothesOrderService(UserDao userDao, ClothesProductDao clothesProductDao) {
        super(userDao);
        this.clothesProductDao = clothesProductDao;
    }

    @Override
    ProductType getProductType() {
        return ProductType.Clothes;
    }

    @Override
    int getPoint(String productCode) {
        // 포인트 계산 로직

        return 포인트;
    }

    @Override
    int getProductName(String productCode) {
        // 상품명 조합 로직

        return 상품명;
    }
}
</code></pre>
<h4 id="식품-주문-service">식품 주문 Service</h4>
<pre><code class="language-java">@Service
public FoodOrderService extends OrderService {
    private final FoodProductDao foodProductDao;
    public ClothesOrderService(UserDao userDao, FoodProductDao foodProductDao) {
        super(userDao);
        this.foodProductDao = foodProductDao;
    }

    @Override
    ProductType getProductType() {
        return ProductType.Food;
    }


    @Override
    int getPoint(String productCode) {
        // 포인트 계산 로직

        return 포인트;
    }

    @Override
    int getProductName(String productCode) {
        // 상품명 조합 로직

        return 상품명;
    }
}
</code></pre>
<h4 id="주문-service-factory">주문 Service Factory</h4>
<pre><code class="language-java">@Component
public class OrderServiceFactory {
    private final Map&lt;ProductType, OrderService&gt; orderServiceMap = new HashMap&lt;&gt;();

    public OrderServiceFactory(List&lt;OrderService&gt; orderServices) {
        orderServices.forEach(s -&gt; orderServiceMap.put(s.getOrderType(), s);
    }

    public OrderService getOrderService(ProductType productType) {
        return orderServiceMap.get(productType);
    }   
}</code></pre>
<h4 id="주문-service-factory-사용">주문 Service Factory 사용</h4>
<pre><code class="language-java">@RequiredArgsConstructor
@RestController
public class OrderController {
    private final ModelMapper modelMapper;
    private final OrderServiceFactory orderServiceFactory;

    @PostMapping(&quot;/order&quot;)
    public ResponseEntity order(@RequestBody OrderDto.Request requestDto) {
        Order order = modelMapper.map(requestDto, Order.class);
        orderServiceFactory.getOrderService(requestDto.getProductType()).order(order);

        return ResponseEntity.ok();
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Enum 공통 유틸 만들기]]></title>
            <link>https://velog.io/@taeni-develop/Enum%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%BD%94%EB%93%9C-%EA%B4%80%EB%A6%AC%EC%8B%9C-%EB%B0%98%EB%B3%B5%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%82%AC%EC%9A%A9%EB%90%98%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%9C%A0%ED%8B%B8-%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@taeni-develop/Enum%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%BD%94%EB%93%9C-%EA%B4%80%EB%A6%AC%EC%8B%9C-%EB%B0%98%EB%B3%B5%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%82%AC%EC%9A%A9%EB%90%98%EB%8A%94-%EB%B6%80%EB%B6%84-%EC%9C%A0%ED%8B%B8-%EC%83%9D%EC%84%B1</guid>
            <pubDate>Wed, 16 Nov 2022 06:41:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Enum을 활용한 코드 관리시에 반복적으로 사용되는 부분을 공통 유틸로 생성</p>
</blockquote>
<p>나는 평소에 Enum을 활용하여 코드를 관리하는 것을 선호한다. 
Enum으로 관리시에 자주 사용되는 부분을 공통 유틸로 분리하는 패턴을 적용하여 기록한다. 아래 패턴은 조금 더 좋은 아이디어가 있을 때마다 리팩토링하여 업데이트 할 예정이다</p>
<h3 id="일반적으로-사용되는-enum-반복-사용-패턴-1">일반적으로 사용되는 Enum 반복 사용 패턴 1</h3>
<pre><code class="language-java">@AllArgsConstructor
@Getter
public enum Friut {
    APPLE(&quot;AP&quot;),
    BANANA(&quot;BN&quot;),
    STRAWBERRY(&quot;ST&quot;),
    MANGO(&quot;MG&quot;),
    PINEAPPLE(&quot;PA&quot;);

    private String code;

    public static Friut findBycode(String code) {
        return Arrays.stream(Fruit.values())
            .filter(f -&gt; f.getCode().equals(value))
            .findAny()
            .orElse(Fruit.APPLE);
    }
}</code></pre>
<h3 id="interface를-활용하여-findbycode-부분-유틸화">Interface를 활용하여 findByCode 부분 유틸화</h3>
<ul>
<li><p>코드값 관련 인터페이스</p>
<pre><code class="language-java">public interface EnumCode {
  String getCode();
}</code></pre>
</li>
<li><p>EnumValue 인터페이스 구현 Enum</p>
<pre><code class="language-java">@AllArgsConstructor
@Getter
public enum Friut implements EnumCode {
  APPLE(&quot;AP&quot;),
  BANANA(&quot;BN&quot;),
  STRAWBERRY(&quot;ST&quot;),
  MANGO(&quot;MG&quot;),
  PINEAPPLE(&quot;PA&quot;);

  private String code;
}</code></pre>
</li>
<li><p>Enum code find util</p>
<pre><code class="language-java">public class EnumFindUtil{
  public static &lt;E extends Enum&lt;E&gt; &amp; EnumCode&gt; Optional&lt;E&gt; findByValue(Class&lt;E&gt; clazz, String value) {
      return Arrays.stream(clazz.getEnumConstants())
          .filter(e -&gt; e.getCode().equals(value)
          .findAny();
  }
}</code></pre>
</li>
<li><p>활용</p>
<pre><code class="language-java">Fruit favoriteFruit = EnumFindUtil.findByCode(Fruit.class, &quot;MG&quot;).orElse(Fruit.APPLE);</code></pre>
</li>
</ul>
<h3 id="일반적으로-사용되는-enum-반복-사용-패턴-2">일반적으로 사용되는 Enum 반복 사용 패턴 2</h3>
<pre><code class="language-java">@AllArgsConstructor
@Getter
public enum FruitTypeByColor implements EnumCode, EnumValueList&lt;Fruit&gt; {
    RED_FRUIT(&quot;RD&quot;, Arrays.asList(FRUIT.APPLE, FRUIT.STRAWBERRY)),
    YELLOW_FRUIT(&quot;YL&quot;, Arrays.asList(FRUIT.BANANA, FRUIT.MANGO, FRUIT.PINEAPPLE));

    private String code;
    private List&lt;Fruit&gt; valueList;

    public static FruitTypeByColor findByCode(String code) {
        return Arrays.stream(FruitTypeByColor.values())
            .filter(f -&gt; f.getCode().equals(value))
            .findAny()
            .orElseThrow();
    }

    public FruitTypeByColor findByValue(Fruit value){
        return Arrays.stream(FruitTypeByColor.values())
            .filter(e -&gt; hasByValue(e.getValueList(), value))
            .findAny()
            .orElseThrow();
    }

    private boolean hasByValue(List&lt;Fruit&gt; values, Fruit value) {
        return values.stream()
            .anyMatch(e -&gt; e == value);
    }
}</code></pre>
<h3 id="interface를-활용하여-getvaluelist-부분-유틸화">Interface를 활용하여 getValueList 부분 유틸화</h3>
<ul>
<li><p>코드리스트 관련 인터페이스</p>
<pre><code class="language-java">public interface EnumValueList&lt;T&gt; {
  List&lt;T&gt; getValueList();
}</code></pre>
</li>
<li><p>EnumCodeList 인터페이스 구현 Enum</p>
<pre><code class="language-java">@AllArgsConstructor
@Getter
public enum FruitTypeByColor implements EnumCode, EnumValueList&lt;Fruit&gt; {
  RED_FRUIT(&quot;RD&quot;, Arrays.asList(FRUIT.APPLE, FRUIT.STRAWBERRY)),
  YELLOW_FRUIT(&quot;YL&quot;, Arrays.asList(FRUIT.BANANA, FRUIT.MANGO, FRUIT.PINEAPPLE));

  private String code;
  private List&lt;Fruit&gt; valueList;
}</code></pre>
</li>
<li><p>Enum code find util</p>
<pre><code class="language-java">public class EnumFindUtil{
  public static &lt;E extends Enum&lt;E&gt; &amp; EnumCode&gt; Optional&lt;E&gt; findByCode(Class&lt;E&gt; clazz, String value) {
      return Arrays.stream(clazz.getEnumConstants())
          .filter(e -&gt; e.getCode().equals(value)
          .findAny();
  }

  public static &lt;E extends Enum&lt;E&gt; &amp; EnumValueList&lt;C&gt;, C extends Enum&lt;C&gt;&gt; Optional&lt;E&gt; findByValue(Class&lt;E&gt; clazz, C value){
      return Arrays.stream(clazz.getEnumConstants())
          .filter(e -&gt; hasByValue(e.getValueList(), code))
          .findAny();
  }

  private static &lt;E extends Enum&lt;E&gt;, C extends Enum&lt;C&gt;&gt; boolean hasByValue(List&lt;E&gt; values, C value) {
      return values.stream()
          .anyMatch(e -&gt; e == value);
  }
}</code></pre>
</li>
<li><p>활용</p>
<pre><code class="language-java">FruitTypeByColor favoriteFruit = EnumFindUtil.findByCode(Fruit.class, &quot;RD&quot;).orElseThrow();
FruitTypeByColor fruitType = EnumFindUtil.findByValue(FruitTypeByValue.class, Fruit.APPLE).orElseThrow();</code></pre>
<blockquote>
<p>위 패턴을 활용하여 다양한 적용이 가능할 것으로 보인다</p>
</blockquote>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java static factory method naming 방식]]></title>
            <link>https://velog.io/@taeni-develop/Java-static-factory-method-naming-%EB%B0%A9%EC%8B%9D</link>
            <guid>https://velog.io/@taeni-develop/Java-static-factory-method-naming-%EB%B0%A9%EC%8B%9D</guid>
            <pubDate>Wed, 01 Jun 2022 05:49:16 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드</p>
<pre><code class="language-java">Date d = Date.from(instant);</code></pre>
</li>
<li><p>of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드</p>
<pre><code class="language-java">Set&lt;Rank&gt; faceCards = EnumSet.of(JACK, QUEEN, KING)</code></pre>
</li>
<li><p>valueOf: _from_과 _of_의 더 자세한 버전</p>
<pre><code class="language-java">BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);</code></pre>
</li>
<li><p>instance 혹은 getInstance: (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.</p>
<pre><code class="language-java">StackWalker luke = StackWalker.getInstance(options);</code></pre>
</li>
<li><p>create 혹은 newInstance: <em>instance</em> 혹은 _getInstance_와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장한다.</p>
<pre><code class="language-java">Object newArray = Array.newInstance(classObject, arrayLen);</code></pre>
</li>
<li><p>getType: _getInstance_와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. _Type_은 팩터리 메서드가 반환할 객체의 타입이다.</p>
<pre><code class="language-java">FileStore fs = Files.getFileStore(path);</code></pre>
</li>
<li><p>newType: _newInstance_와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. _Type_은 팩터리 메서드가 반환할 객체의 타입이다.</p>
<pre><code class="language-java">BufferedReader br = Files.newBufferdReader(path);</code></pre>
</li>
<li><p>type: _getType_과 _newType_의 간결한 버전</p>
<pre><code class="language-java">List&lt;Complaint&gt; litany = Collections.list(legacyLitany);</code></pre>
</li>
</ul>
<p><em>출처: Effective JAVA</em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[K8S Cluster kubeadm 토큰 재생성]]></title>
            <link>https://velog.io/@taeni-develop/K8S-Cluster-%ED%86%A0%ED%81%B0-%EC%9E%AC%EC%83%9D%EC%84%B1</link>
            <guid>https://velog.io/@taeni-develop/K8S-Cluster-%ED%86%A0%ED%81%B0-%EC%9E%AC%EC%83%9D%EC%84%B1</guid>
            <pubDate>Tue, 08 Mar 2022 05:53:03 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>K8S kubeadm 토큰의 유효기간은 24시간까지이다. 토큰 생성시점이 24시간이 지날 경우 유효기간이 지나기에 토큰의 재생성이 필요하다</p>
</blockquote>
<h4 id="master-node-접속">Master node 접속</h4>
<pre><code class="language-bash">$ kubeadm token create --print-join-command
kubeadm join 172.0.0.0:6443 --token rirkqc.j4ir52qccxnyxxlx --discovery-token-ca-cert-hash sha256:09377352f6c32357e21278fc23c3e4c78cbfbdddd4e6d15df7d91b5501c</code></pre>
<h4 id="worker-node-접속">Worker node 접속</h4>
<pre><code class="language-bash">$ kubeadm join 172.0.0.0:6443 --token rirkqc.j4ir52qccxnyxxlx --discovery-token-ca-cert-hash sha256:09377352f6c32357e21278fc23c3e4c78cbfbdddd4e6d15df7d91b5501c</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2(ubuntu) 환경에서 kubeadm을 이용한 K8S Cluster 구축]]></title>
            <link>https://velog.io/@taeni-develop/AWS-EC2-K8S-Cluster-%EA%B5%AC%EC%B6%95</link>
            <guid>https://velog.io/@taeni-develop/AWS-EC2-K8S-Cluster-%EA%B5%AC%EC%B6%95</guid>
            <pubDate>Tue, 08 Mar 2022 02:19:29 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>IP는 172.0.0.0으로 표기함.
실제 사용되는 IP와 SUBNET을 적용바람.</p>
</blockquote>
<h2 id="🛠-k8s-설치-각-노드에-모두-동일하게-설치">🛠 K8S 설치 (각 노드에 모두 동일하게 설치)</h2>
<h4 id="k8s-저장소-추가">k8s 저장소 추가</h4>
<pre><code class="language-bash">$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
$ sudo chmod -R 777 /etc/apt
$ cat &lt;&lt;EOF&gt; /etc/apt/sources.list.d/kubernetes.list
&gt; deb http://apt.kubernetes.io/ kubernetes-xenial main
&gt; EOF</code></pre>
<h4 id="docker-설치">docker 설치</h4>
<pre><code class="language-bash">$ wget -qO- get.docker.com | sh</code></pre>
<h4 id="k8s-package-설치">k8s package 설치</h4>
<pre><code class="language-bash">$ sudo apt-get update
$ sudo apt-get install -y kubelet kubeadm kubectl kubernetes-cni</code></pre>
<h4 id="docker-환경설정-변경">docker 환경설정 변경</h4>
<p><code>ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock</code>구문 뒤에 <code>--exec-opt native.cgroupdriver=systemd</code>를 추가</p>
<pre><code class="language-bash">$ sudo vi /lib/systemd/system/docker.service
[Service]
Type=notify                                                                    
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker                                                
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --exec-opt native.cgroupdriver=systemd
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always</code></pre>
<pre><code class="language-bash">$ sudo systemctl daemon-reload
$ sudo systemctl restart docker</code></pre>
<h2 id="🗒-master-node-설정">🗒 MASTER NODE 설정</h2>
<h4 id="cluster-초기화">cluster 초기화</h4>
<pre><code class="language-bash">$ sudo kubeadm init --apiserver-advertise-address 0.0.0.0 --pod-network-cidr=172.0.0.0/16 --ignore-preflight-errors=ALL

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run &quot;kubectl apply -f [podnetwork].yaml&quot; with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 172.0.0.0:6443 --token gs5zbb.lwpwcaghege7a6cs \
        --discovery-token-ca-cert-hash sha256:0885f23cdbf64b621c53c0c230bd938adf0da057e8039331ae4b6490343bb0ca</code></pre>
<ul>
<li>mkdir 시작되는 구문부터 3줄 복사하여 터미널에 입력</li>
<li>`kubeadm join 172.0.0.0:6443 --token gs5zbb.lwpwcaghege7a6cs \<pre><code>  --discovery-token-ca-cert-hash sha256:0885f23cdbf64b621c53c0c230bd938adf0da057e8039331ae4b6490343bb0ca`은 worker node에서 master node에 연결하기 위한 명령어</code></pre><h4 id="설치확인">설치확인</h4>
<pre><code class="language-bash">$ kubectl get nodes
</code></pre>
</li>
</ul>
<p>NAME               STATUS     ROLES                  AGE     VERSION
ip-172-0-0-0       NotReady   control-plane,master   7m13s   v1.23.4</p>
<pre><code>
## 🗒 WORKER NODE 설정
#### 초기화
```bash
$ sudo kubeadm reset</code></pre><h4 id="master-node-join-master-node-cluster-초기화시에-생성된-토큰으로">master node join (master node cluster 초기화시에 생성된 토큰으로)</h4>
<pre><code class="language-bash">$ sudo kubeadm join 172.0.0.0:6443 --token p46o53.yf40pnp8mbsd4efe \
        --discovery-token-ca-cert-hash sha256:8464611730f9c9ad87d814993643085146acfbbddddd6bdf28a2ed63a59897eb --ignore-preflight-errors=ALL</code></pre>
<h4 id="master-node에서-cluster-구성-확인-status-notready">master node에서 cluster 구성 확인 (STATUS NotReady)</h4>
<pre><code class="language-bash">$ kubectl get nodes
NAME               STATUS     ROLES                  AGE     VERSION
ip-172-0-0-1       NotReady   &lt;none&gt;                 6m34s   v1.23.4
ip-172-0-0-0       NotReady   control-plane,master   28m     v1.23.4
ip-172-0-0-2       NotReady   &lt;none&gt;                 12m     v1.23.4</code></pre>
<h2 id="🗒-master-node-컨테이너-네트워크-애드온-설정">🗒 MASTER NODE 컨테이너 네트워크 애드온 설정</h2>
<h4 id="ip-대역-변경">IP 대역 변경</h4>
<pre><code class="language-bash">$ wget https://docs.projectcalico.org/v3.22/manifests/calico.yaml
$ sed -i -e &#39;s?192.168.0.0/16?172.0.0.0/16?g&#39; calico.yaml
$ kubectl apply -f calico.yaml</code></pre>
<h4 id="node-확인-status-ready-확인">node 확인 (STATUS Ready 확인)</h4>
<pre><code class="language-bash">$ kubectl get nodes
NAME               STATUS     ROLES                  AGE    VERSION
ip-172-0-0-1       Ready      &lt;none&gt;                 33m    v1.23.4
ip-172-0-0-0       Ready      control-plane,master   115m   v1.23.4
ip-172-0-0-2       Ready      &lt;none&gt;                 99m    v1.23.4</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[AWS EC2 간 통신 보안 그룹 설정]]></title>
            <link>https://velog.io/@taeni-develop/EC2-%EA%B0%84-%ED%86%B5%EC%8B%A0-%EB%B3%B4%EC%95%88-%EA%B7%B8%EB%A3%B9-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@taeni-develop/EC2-%EA%B0%84-%ED%86%B5%EC%8B%A0-%EB%B3%B4%EC%95%88-%EA%B7%B8%EB%A3%B9-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Mon, 28 Feb 2022 07:02:55 GMT</pubDate>
            <description><![CDATA[<ol>
<li>해당 EC2 인스턴스 보안 그룹 설정 페이지</li>
<li>인바운드 규칙에 해당 보안 그룹의 ID 추가 (용도에 따른 프로토콜/포트 설정)</li>
<li>아웃바운드 설정도 유효한지 확인</li>
</ol>
<p><img src="https://images.velog.io/images/taeni-develop/post/9baf5683-0cff-4f66-9177-1db03b61ddf3/Screenshot_20220228-160511_Whale.jpg" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Configuration] Eclipse + gradle 환경에서의 debug mode (+Spring-loaded) 설정 ]]></title>
            <link>https://velog.io/@taeni-develop/Eclipse-gradle-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-debug-mode-Spring-loaded-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@taeni-develop/Eclipse-gradle-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-debug-mode-Spring-loaded-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Mon, 08 Nov 2021 04:10:11 GMT</pubDate>
            <description><![CDATA[<p>Eclipse + gradle + Spring Boot 환경에서의 debug mode (+Spring-loaded) 설정 </p>
<h3 id="1-buildgradle에-내용-추가">1. build.gradle에 내용 추가</h3>
<pre><code>bootRun {
   jvmArgs &#39;-Xdebug&#39;, &#39;-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005&#39;
}</code></pre><pre><code>dependencies {
   implementation &#39;org.springframework.boot:spring-boot-devtools&#39;
}</code></pre><h3 id="2-gradle-bootrun-실행">2. Gradle BootRun 실행</h3>
<h3 id="3-eclipse에서-아래-순서대로-실행">3. Eclipse에서 아래 순서대로 실행</h3>
<ul>
<li>Debug Configuration</li>
<li>Remote Java Application</li>
<li>Connect tab에서 Project 설정 및 port (5005번) 설정</li>
</ul>
<h3 id="4-debug-실행">4. Debug 실행</h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Configuration] 개발환경 Proxy 설정]]></title>
            <link>https://velog.io/@taeni-develop/IDE-proxy-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@taeni-develop/IDE-proxy-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Thu, 04 Nov 2021 04:08:34 GMT</pubDate>
            <description><![CDATA[<h2 id="windows-기준">Windows 기준</h2>
<h3 id="eclipse">Eclipse</h3>
<h4 id="경로-이클립스경로eclipseini">경로: 이클립스경로.Eclipse.ini</h4>
<pre><code>-Djavax.net.ssl.trustStore=NUL
-Djavax.net.ssl.trustStoreType=Windows-ROOT </code></pre><h3 id="intellij">IntelliJ</h3>
<h4 id="경로-intellij경로ideaproperties">경로: IntelliJ경로\idea.properties</h4>
<pre><code>java.net.useSystemProxies=true</code></pre><h3 id="npm">NPM</h3>
<h4 id="경로-cusers사용자계정npmrc">경로: C:\Users\사용자계정.npmrc</h4>
<pre><code>set=proxy
http://ID:PW@IP:PORT=
proxy=http://ID:PW@IP:PORT/
https-proxy=http://ID:PW@IP:PORT/
strict-ssl=false
registry=http://registry.npmjs.org/</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] Optional 활용]]></title>
            <link>https://velog.io/@taeni-develop/Optional-%ED%99%9C%EC%9A%A9</link>
            <guid>https://velog.io/@taeni-develop/Optional-%ED%99%9C%EC%9A%A9</guid>
            <pubDate>Thu, 30 Sep 2021 11:00:41 GMT</pubDate>
            <description><![CDATA[<h3 id="java-optional이란">Java Optional이란</h3>
<p><code>Java8부터 지원한 NPE 방지 wrapper class</code></p>
<pre><code class="language-java">// 기존 방식
String name = userRepository.getName();

if (name != null) {
   logic(name);
}

// Optional 사용
Optional.ofNullable(userRepository.getName())
   .ifPresent((n) -&gt; {
      logic(n);
   });</code></pre>
<h3 id="optional-제대로-활용하기">Optional 제대로 활용하기</h3>
<p><code>isPresent()-get() 대신 orElse()/orElseGet() 등 사용</code></p>
<pre><code class="language-java">// not good
Optional&lt;String&gt; name = Optional.ofNullable(userRepository.getName());
if (name.isPresent()) {
   return name.get();
} else {
   return null;
}

// good
return Optional.ofNullable(userRepository.getName())
   .orElse(null);</code></pre>
<p><code>null 대신 isEmpty() 사용</code></p>
<pre><code class="language-java">// not good
Optional&lt;String&gt; name = null;

// good
Optional&lt;String&gt; name = Optional.empty();
</code></pre>
<p><code>Optional&lt;Integer&gt; 대신 OptionalInt 사용(int, long, double)</code></p>
<pre><code class="language-java">// not good
Optional&lt;Integer&gt; age = Optional.of(userRepository.getAge());

// good
OptionalInt age = OptionalInt.of(userRepository.getAge());</code></pre>
<p><code>연산이 필요하거나 객체생성이 수행되는 orElse는 orElseGet 사용</code></p>
<pre><code class="language-java">// not bad
int userCnt = Optional.ofNullable(userRepository.getUserList())
   .map(List::size)
   .orElse(0);

// not good
Optional&lt;User&gt; user = Optional.ofNullable(userRepository.getUser())
   .orElse(new User());

// good
Optional&lt;User&gt; user = Optional.ofNullable(userRepository.getUser())
   .orElseGet(User::new);</code></pre>
<p><code>null이 예상되는 경우 of() 대신 ofNullable() 사용</code></p>
<pre><code class="language-java">// not good
Optional&lt;User&gt; user = Optional.of(userRepository.getUser()); // null이 반환될 수 있음. NPE 발생.

// good
Optional&lt;User&gt; user = Optional.ofNullable(userRepository.getUser());

</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 팩토리패턴을 활용한 서비스 관리]]></title>
            <link>https://velog.io/@taeni-develop/Spring-%ED%8C%A9%ED%86%A0%EB%A6%AC%ED%8C%A8%ED%84%B4%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B4%80%EB%A6%AC-8mvd0mqy</link>
            <guid>https://velog.io/@taeni-develop/Spring-%ED%8C%A9%ED%86%A0%EB%A6%AC%ED%8C%A8%ED%84%B4%EC%9D%84-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B4%80%EB%A6%AC-8mvd0mqy</guid>
            <pubDate>Tue, 10 Aug 2021 11:15:48 GMT</pubDate>
            <description><![CDATA[<p>비즈니스 로직을 구현할 때 입력받은 값에 따라 일부 로직을 각각 다르게 구현해야 하는 경험을 많이 하게 된다.</p>
<p><del>필자는 요즘 Clean code에 대해 관심이 많아 위 같은 경우에 대해 어떤 식으로 구현을 하면 좋을지 고민한 끝에 여러 가지 자료를 참조한 끝에 아래와 같이 구현한 정보를 기록하려 한다.</del></p>
<p>Spring framework 개발환경에서 if문으로 장황스럽게 구현된 코드를 Factory pattern을 활용하여 관리가 용이하도록 변경해보려한다.</p>
<p>아래 코드를 예로 들어보자.
구매한 상품 타입에 따라 포인트 계산식이 달라지는 로직이다.</p>
<p><strong>의류포인트계산 Service</strong></p>
<pre><code class="language-java">@Service
public class ClothesPointCalculateService {
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.05);
    }
}</code></pre>
<p><strong>식품포인트계산 Service</strong></p>
<pre><code class="language-java">@Service
public class FoodPointCalculateService {
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.03);
    }
}</code></pre>
<p><strong>전자제품포인트계산 Service</strong></p>
<pre><code class="language-java">@Service
public class ElectronicsPointCalculateService {
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.10);
    }
}</code></pre>
<p><strong>포인트계산 Test case</strong></p>
<pre><code class="language-java">@SpringBootTest
class PointCalculateTest {

    private final int PRICE = 10000;

    @Autowired
    private ClothesPointCalculateService clothesPointCalculateService;

    @Autowired
    private FoodPointCalculateService foodPointCalculateService;

    @Autowired
    private ElectronicsPointCalculateService electronicsPointCalculateService;

    @Test
    public void clothesPointCalculate_if_success() {
        assert(calculatePoint(ProductType.CLOTHES) == 500);
    }

    @Test
    public void foodPointCalculate_if_success() {
        assert(calculatePoint(ProductType.FOOD) == 300);
    }

    @Test
    public void electronicsPointCalculate_if_success() {
        assert(calculatePoint(ProductType.ELECTRONICS) == 1000);
    }

    private int calculatePoint(ProductType productType) {
        PointCalculate pointCalculate = PointCalculate.builder()
                .productType(productType)
                .price(PRICE)
                .build();

        if (productType == ProductType.CLOTHES) {
            return clothesPointCalculateService.calculatePoint(pointCalculate);
        } else if (productType == ProductType.FOOD) {
            return foodPointCalculateService.calculatePoint(pointCalculate);
        } else if (productType == ProductType.ELECTRONICS) {
            return electronicsPointCalculateService.calculatePoint(pointCalculate);
        }

        return 0;
    }
}</code></pre>
<p>포인트계산을 사용하는 곳이 당장은 한 군데 밖에 없어 크게 문제될 것이 없다하더라도, 추후 주문/정산/상품상세 등 다양한 곳에서 사용하게 될 수 있고 상품타입도 추가될 수 있다.
상품타입이 추가되면 아래 if문을 사용하는 곳을 모두 찾아 수정하여야한다.</p>
<pre><code class="language-java">if (productType == ProductType.CLOTHES) {
    return clothesPointCalculateService.calculatePoint(pointCalculate);
} else if (productType == ProductType.FOOD) {
    return foodPointCalculateService.calculatePoint(pointCalculate);
} else if (productType == ProductType.ELECTRONICS) {
    return electronicsPointCalculateService.calculatePoint(pointCalculate);
}</code></pre>
<p>시스템을 운영하는 데 굉장히 비효율적이므로 위에 예시를 든 코드를 Factory Pattern을 활용하여 개선해보자</p>
<p>우선 포인트계산 Service를 인터페이스로 분리하자.
(기존에 생성해둔 Service와의 이름이 중복되어 
class명 앞에 Factory를 붙였다)</p>
<pre><code class="language-java">public interface PointCalculateService {
    int calculatePoint(PointCalculate pointCalculate);
    ProductType getProductType();
}</code></pre>
<pre><code class="language-java">
@Service
public class FactoryClothesPointCalculateService implements PointCalculateService {

    @Override
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.05);
    }

    @Override
    public ProductType getProductType() {
        return ProductType.CLOTHES;
    }
}</code></pre>
<pre><code class="language-java">@Service
public class FactoryElectronicsPointCalculateService implements PointCalculateService {

    @Override
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.10);
    }

    @Override
    public ProductType getProductType() {
        return ProductType.ELECTRONICS;
    }
}</code></pre>
<pre><code class="language-java">@Service
public class FactoryFoodPointCalculateService implements PointCalculateService {

    @Override
    public int calculatePoint(PointCalculate pointCalculate) {
        return (int) Math.ceil(pointCalculate.getPrice() * 0.03);
    }

    @Override
    public ProductType getProductType() {
        return ProductType.FOOD;
    }
}</code></pre>
<p>이제 포인트계산 Service를 담아사용할 Factory component를 생성하자</p>
<h4 id="포인트계산-service-factory-class">포인트계산 Service Factory class</h4>
<pre><code class="language-java">@Component
public class PointCalculateServiceFactory {

    private final Map&lt;ProductType, PointCalculateService&gt; pointCalculateServiceMap = new HashMap&lt;&gt;();

    /**
     * 생성자주입
     * @param pointCalculateServices
     */
    public PointCalculateServiceFactory(List&lt;PointCalculateService&gt; pointCalculateServices) {
        pointCalculateServices.forEach(s -&gt; pointCalculateServiceMap.put(s.getProductType(), s));
    }

    public PointCalculateService getPointCalculateService(ProductType productType) {
        return pointCalculateServiceMap.get(productType);
    }
}</code></pre>
<h4 id="포인트계산-test-cass">포인트계산 Test cass</h4>
<pre><code class="language-java">@SpringBootTest
class PointCalculateTest {

    private final int PRICE = 10000;

    @Autowired
    private PointCalculateServiceFactory pointCalculateServiceFactory;

    @Test
    public void clothesPointCalculate_if_success() {
        assert(calculatePoint(ProductType.CLOTHES) == 500);
    }

    @Test
    public void foodPointCalculate_if_success() {
        assert(calculatePoint(ProductType.FOOD) == 300);
    }

    @Test
    public void electronicsPointCalculate_if_success() {
        assert(calculatePoint(ProductType.ELECTRONICS) == 1000);
    }

    private int calculatePoint(ProductType productType) {
        PointCalculate pointCalculate = PointCalculate.builder()
                .price(PRICE)
                .productType(productType)
                .build();
        return pointCalculateServiceFactory.getPointCalculateService(productType).calculatePoint(pointCalculate);
    }
}</code></pre>
<p>장황스러운 if문이 아래와 같이 이해하기도 쉽고 유지보수도 용이하도록 변경되었다.</p>
<pre><code class="language-java">pointCalculateServiceFactory.getPointCalculateService(productType).calculatePoint(pointCalculate);</code></pre>
<p>앞으로는 새로운 포인트계산서비스가 필요할 경우 CalculateService interface를 상속받는 Service bean만 생성해주면 된다.</p>
<p>위 코드는 <a href="https://github.com/taeni-develop/factoryPatternExam">github</a>에서 확인 할 수 있다.</p>
]]></description>
        </item>
    </channel>
</rss>