<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>oldserver 님의 블로그</title>
    <link>https://oldserver.tistory.com/</link>
    <description>oldserver 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Mon, 2 Nov 2026 06:18:44 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>oldserver</managingEditor>
    <image>
      <title>oldserver 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/9058471/attach/df0e2ae3c498476f947d1460ce4b382a</url>
      <link>https://oldserver.tistory.com</link>
    </image>
    <item>
      <title>AWS 다운사이즈 후 모니터링 &amp;mdash; t3 버스터블 CPU 크레딧&amp;middot;Redis Evictions&amp;middot;RDS FreeableMemory</title>
      <link>https://oldserver.tistory.com/21</link>
      <description>&lt;div style=&quot;max-width:720px;margin:0 auto;padding:0 8px;color:#2b2f36;line-height:2.0;font-size:17px;word-break:keep-all;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot;&gt;

  &lt;div style=&quot;font-size:13px;font-weight:700;color:#3182f6;letter-spacing:.5px;margin:18px 0 14px;&quot;&gt;INFRA · 모니터링&lt;/div&gt;
  &lt;div style=&quot;font-size:32px;font-weight:800;line-height:1.4;color:#191f28;letter-spacing:-.3px;&quot;&gt;다운사이즈한 뒤가 진짜다&lt;/div&gt;
  &lt;div style=&quot;font-size:17px;color:#6b7684;margin:16px 0 8px;line-height:1.8;&quot;&gt;버스터블은 CPU%가 아니라 &quot;CPU 크레딧&quot;을 본다. 크레딧·Evictions·여유메모리 지켜보는 법.&lt;/div&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:28px 0;&quot;&gt;&lt;/div&gt;

  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;14일차에서 AWS 인스턴스를 다운사이즈해 비용을 약 1/3 줄였다. 그런데 &lt;strong&gt;다운사이즈는 끝이 아니라 시작&lt;/strong&gt;이다. 스펙을 내렸으면 &quot;정말 그 스펙으로 버티는지&quot;를 며칠 지켜봐야 한다. 특히 &lt;strong&gt;버스터블(t3/t4g)로 내린 경우&lt;/strong&gt;, 평소 CPU% 그래프만 봐서는 위험 신호를 놓친다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;이 글은 다운사이즈 후 무엇을, 왜, 어떻게 보는지다. 핵심은 세 가지 — 버스터블의 &lt;strong&gt;CPU 크레딧&lt;/strong&gt;, Redis의 &lt;strong&gt;Evictions&lt;/strong&gt;, RDS의 &lt;strong&gt;FreeableMemory&lt;/strong&gt;. (수치는 예시다.)&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;왜 &quot;CPU%&quot;가 아니라 &quot;크레딧&quot;인가&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;연산최적(c계열) 같은 비버스터블은 CPU%만 보면 된다. 그런데 &lt;strong&gt;버스터블(t3/t4g)은 다르다.&lt;/strong&gt; &quot;평소엔 적게 쓰고, 가끔 치솟을 때 크레딧을 꺼내 쓰는&quot; 모델이다.&lt;/p&gt;
  &lt;!-- 크레딧 개념 SVG --&gt;
  &lt;div style=&quot;margin:24px 0;&quot;&gt;
    &lt;div style=&quot;border:1px solid #eceef1;border-radius:12px;background:#fbfcfd;padding:14px;&quot;&gt;
      &lt;svg viewBox=&quot;0 0 760 210&quot; style=&quot;width:100%;height:auto;display:block;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;CPU가 baseline 이하면 크레딧 적립, 초과하면 소진되는 개념도&quot;&gt;
        &lt;!-- 기준선 --&gt;
        &lt;line x1=&quot;60&quot; y1=&quot;120&quot; x2=&quot;700&quot; y2=&quot;120&quot; stroke=&quot;#94a3b8&quot; stroke-width=&quot;1.5&quot; stroke-dasharray=&quot;6 4&quot;/&gt;
        &lt;text x=&quot;60&quot; y=&quot;112&quot; font-size=&quot;12&quot; fill=&quot;#64748b&quot; font-weight=&quot;700&quot;&gt;baseline (예: 20%)&lt;/text&gt;
        &lt;!-- 아래(적립) 영역 --&gt;
        &lt;rect x=&quot;60&quot; y=&quot;120&quot; width=&quot;300&quot; height=&quot;60&quot; fill=&quot;#f0fdf4&quot;/&gt;
        &lt;text x=&quot;210&quot; y=&quot;155&quot; text-anchor=&quot;middle&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; fill=&quot;#15803d&quot;&gt;CPU &amp;lt; baseline&lt;/text&gt;
        &lt;text x=&quot;210&quot; y=&quot;173&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#5aa17a&quot;&gt;→ 크레딧 적립 ▲ (안전)&lt;/text&gt;
        &lt;!-- 위(소진) 영역 --&gt;
        &lt;rect x=&quot;400&quot; y=&quot;40&quot; width=&quot;300&quot; height=&quot;80&quot; fill=&quot;#fef2f2&quot;/&gt;
        &lt;text x=&quot;550&quot; y=&quot;72&quot; text-anchor=&quot;middle&quot; font-size=&quot;13&quot; font-weight=&quot;700&quot; fill=&quot;#b91c1c&quot;&gt;CPU &amp;gt; baseline&lt;/text&gt;
        &lt;text x=&quot;550&quot; y=&quot;90&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#c87171&quot;&gt;→ 크레딧 소진 ▼&lt;/text&gt;
        &lt;text x=&quot;550&quot; y=&quot;108&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#c87171&quot;&gt;0 되면 성능 제한!&lt;/text&gt;
        &lt;!-- CPU 곡선 느낌 --&gt;
        &lt;path d=&quot;M60 150 Q 160 145 260 150 T 420 95 Q 500 60 560 90 T 700 150&quot; fill=&quot;none&quot; stroke=&quot;#3182f6&quot; stroke-width=&quot;2.5&quot;/&gt;
        &lt;text x=&quot;60&quot; y=&quot;200&quot; font-size=&quot;11&quot; fill=&quot;#8b95a1&quot;&gt;※ 위험 신호는 &quot;CPU 80%&quot;가 아니라 &quot;크레딧이 0에 가까워지는 것&quot;&lt;/text&gt;
      &lt;/svg&gt;
    &lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#8b95a1;margin-top:10px;text-align:center;&quot;&gt;그림 1. 버스터블은 baseline 기준으로 크레딧이 적립/소진된다. 크레딧이 0이면 성능이 제한된다.&lt;/div&gt;
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;즉 버스터블에서 진짜 위험 신호는 &quot;CPU 80%&quot;가 아니라 &lt;strong&gt;&quot;CPU 크레딧이 계속 줄어 0에 가까워지는 것&quot;&lt;/strong&gt;이다. CPU%만 보면 &quot;아직 80%인데 왜 느리지?&quot;에서 막힌다. 그래서 다운사이즈 후엔 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;CPUCreditBalance&lt;/code&gt;를 1순위로 본다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;① EC2 버스터블 — CPU 크레딧 (가장 중요)&lt;/h2&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;# 대상 인스턴스들의 최근 6시간 크레딧 추이
for id in &amp;lt;INSTANCE_ID_1&amp;gt; &amp;lt;INSTANCE_ID_2&amp;gt;; do
  echo &quot;=== $id ===&quot;
  aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUCreditBalance \
    --dimensions Name=InstanceId,Value=$id \
    --start-time $(date -u -d '6 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
    --end-time   $(date -u +%Y-%m-%dT%H:%M:%SZ) \
    --period 300 --statistics Average \
    --query &quot;Datapoints[-5:].{T:Timestamp,Credit:Average}&quot; --output table
done&lt;/code&gt;&lt;/pre&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;strong&gt;판정&lt;/strong&gt; — 값이 유지되거나 증가 → ✅ 안전(CPU가 baseline 이하). 계속 감소하거나 0 근접 → ⚠ 피크에 CPU 초과 중, 지속되면 상향 검토. 실제로 다운사이즈 당일 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;72 → 86 → 97&lt;/code&gt;로 증가했고, 피크 후엔 상한에 도달해 있었다 — 이 워크로드엔 t3.medium이 충분했다는 뜻.
  &lt;/div&gt;
  &lt;h3 style=&quot;font-size:19px;font-weight:800;color:#191f28;margin:28px 0 10px;&quot;&gt;unlimited 모드 — 크레딧 0이어도 안 멈춘다&lt;/h3&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;버스터블에는 &lt;strong&gt;unlimited 모드&lt;/strong&gt;가 있다. &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;CpuCredits=unlimited&lt;/code&gt;면 크레딧이 0이어도 성능을 유지하고 초과분만 소액 과금한다(서비스 중단 없음). 다운사이즈하면서 켜두면 &quot;크레딧 소진 → 갑자기 느려짐&quot;이라는 최악은 피한다. 대신 꾸준히 초과하면 과금이 쌓이니, 그럴 땐 unlimited로 버티지 말고 인스턴스를 상향하는 게 답이다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;② Redis — Evictions와 메모리&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;Redis를 작은 노드로 내렸다면 &lt;strong&gt;메모리&lt;/strong&gt;가 관건이다. CPU는 거의 안 쓰니 볼 필요 없고, 메모리 사용률과 Evictions를 본다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;redis-cli -h &amp;lt;REDIS_ENDPOINT&amp;gt; -p 6379 info memory | grep -iE &quot;used_memory_human|maxmemory_human&quot;
redis-cli -h &amp;lt;REDIS_ENDPOINT&amp;gt; -p 6379 info stats  | grep evicted&lt;/code&gt;&lt;/pre&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;strong&gt;판정&lt;/strong&gt; — &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;used_memory&lt;/code&gt;가 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;maxmemory&lt;/code&gt;의 80%를 넘으면 주의. &lt;strong&gt;&lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;evicted_keys&lt;/code&gt;가 0이 아니면&lt;/strong&gt; 메모리 부족으로 키가 쫓겨나는 중 → 상위 노드로.
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;Evictions가 핵심이다. 메모리가 꽉 차면 Redis는 오래된 키를 강제로 버리는데(eviction), 이게 캐시 미스·세션 유실로 이어진다. 다운사이즈 후 Evictions가 0을 유지하면 그 노드로 충분한 것이다. redis-cli가 없으면 CloudWatch의 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;DatabaseMemoryUsagePercentage&lt;/code&gt;로도 본다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;③ Aurora RDS — FreeableMemory&lt;/h2&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;for id in &amp;lt;DB_INSTANCE_1&amp;gt; &amp;lt;DB_INSTANCE_2&amp;gt;; do
  echo &quot;=== $id ===&quot;
  aws cloudwatch get-metric-statistics --namespace AWS/RDS --metric-name FreeableMemory \
    --dimensions Name=DBInstanceIdentifier,Value=$id \
    --start-time $(date -u -d '6 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
    --end-time   $(date -u +%Y-%m-%dT%H:%M:%SZ) \
    --period 300 --statistics Minimum \
    --query &quot;Datapoints[-5:].{T:Timestamp,FreeBytes:Minimum}&quot; --output table
done&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;&lt;strong&gt;판정 (바이트 → GB = ÷ 1073741824):&lt;/strong&gt; FreeableMemory가 1GB 밑으로 떨어지면 주의. 기준선 예시: share 약 6GB, log 약 3.5GB 여유 → 안전. RDS도 버스터블(t4g)로 내렸다면 EC2처럼 크레딧이 추가로 중요하지만, CPU가 한 자릿수로 꾸준하면 크레딧이 계속 적립돼 소진 걱정은 없다. 비버스터블(r계열)은 크레딧 개념이 없으니 CPU%와 FreeableMemory만 본다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;기준선을 기록해두는 게 핵심&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;모니터링에서 제일 중요한 건 &lt;strong&gt;&quot;패치 당일 기준선&quot;을 숫자로 적어두는 것&lt;/strong&gt;이다. 나중에 &quot;이게 정상인가?&quot;를 판단하려면 비교 대상이 있어야 한다.&lt;/p&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;항목&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;패치 당일(예시)&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;피크 후(예시)&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;상태&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;EC2 크레딧&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;72 → 97 (증가)&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;상한 도달&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#15803d;&quot;&gt;✅&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;Redis 메모리&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;15% / Evictions 0&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;유지&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#15803d;&quot;&gt;✅&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;RDS share 여유&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;~6GB&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;~5.7GB&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#15803d;&quot;&gt;✅&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;RDS/EC2 CPU&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;한 자릿수&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;한 자릿수&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#15803d;&quot;&gt;✅&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;당일과 피크 후(트래픽 많은 시간대 지난 뒤)를 &lt;strong&gt;같은 명령으로 두 번 찍어&lt;/strong&gt; 비교하면, &quot;다운사이즈가 안전했다&quot;를 데이터로 확인할 수 있다. 감이 아니라 숫자로.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;이상 징후 시 조치 — 전부 즉시 롤백 가능&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;좋은 점은, 위험 신호가 보여도 &lt;strong&gt;타입 변경은 상위 타입으로 즉시 되돌릴 수 있다&lt;/strong&gt;(데이터 손실 없음). 그래서 과감하게 내려보고, 지켜보다 아니면 올리면 된다.&lt;/p&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;신호&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;조치&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;EC2 크레딧 0 근접/지속 감소&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;인스턴스 유형 상향(t3.medium → t3.large)&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;Redis Evictions 증가 / 메모리 80%+&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;상위 캐시 노드로&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;RDS FreeableMemory 1GB 미만&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;해당 인스턴스 상향(즉시 롤백, 데이터 유지)&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;마치며&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;다운사이즈는 &quot;줄이기&quot;가 절반, &quot;지켜보기&quot;가 나머지 절반이다. 그리고 지켜볼 때 &lt;strong&gt;버스터블은 CPU%가 아니라 CPU 크레딧&lt;/strong&gt;을 봐야 한다는 게 이 글의 핵심이다. CPU%만 보면 &quot;80%인데 왜 느리지&quot;에서 막히지만, 크레딧을 보면 &quot;크레딧이 0이라 baseline으로 제한됐구나&quot;가 바로 보인다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;세 지표 — &lt;strong&gt;EC2 CPU 크레딧, Redis Evictions, RDS FreeableMemory&lt;/strong&gt; — 를 패치 당일과 피크 후로 두 번 찍어 기준선과 비교하면, 비용 절감이 안전했는지 숫자로 확인된다. 전부 즉시 롤백 가능하니 신호가 나쁘면 되돌리면 그만이다. 과감하게 줄이되, 며칠은 꼭 지켜보자.&lt;/p&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:36px 0 20px;&quot;&gt;&lt;/div&gt;
  &lt;div style=&quot;background:#f8f9fa;border:1px solid #eceef1;border-radius:12px;padding:20px;text-align:center;margin-bottom:12px;&quot;&gt;
    &lt;div style=&quot;font-weight:700;color:#191f28;font-size:15px;&quot;&gt;AWS 비용 최적화 · 버스터블 운영 · 모니터링 문의&lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#6b7684;margin-top:6px;&quot;&gt;댓글로 편하게 남겨주세요.&lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;</description>
      <category>게임 서버구축</category>
      <category>aurora</category>
      <category>AWS</category>
      <category>cloudwatch</category>
      <category>CPUCredit</category>
      <category>redis</category>
      <category>t3</category>
      <category>게임서버</category>
      <category>다운사이징</category>
      <category>모니터링</category>
      <category>버스터블</category>
      <author>oldserver</author>
      <guid isPermaLink="true">https://oldserver.tistory.com/21</guid>
      <comments>https://oldserver.tistory.com/21#entry21comment</comments>
      <pubDate>Sun, 1 Nov 2026 15:02:31 +0900</pubDate>
    </item>
    <item>
      <title>Adjust 어트리뷰션 S3 BI 파이프라인</title>
      <link>https://oldserver.tistory.com/20</link>
      <description>&lt;div style=&quot;max-width:720px;margin:0 auto;padding:0 8px;color:#2b2f36;line-height:2.0;font-size:17px;word-break:keep-all;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot;&gt;

  &lt;div style=&quot;font-size:13px;font-weight:700;color:#3182f6;letter-spacing:.5px;margin:18px 0 14px;&quot;&gt;DATA · 어트리뷰션 BI&lt;/div&gt;
  &lt;div style=&quot;font-size:32px;font-weight:800;line-height:1.4;color:#191f28;letter-spacing:-.3px;&quot;&gt;Adjust 데이터를 S3로 받아 사내 BI 대시보드 만들기&lt;/div&gt;
  &lt;div style=&quot;font-size:17px;color:#6b7684;margin:16px 0 8px;line-height:1.8;&quot;&gt;Adjust는 pull이 아니라 S3 push. IAM 키를 업로더/리더로 나누고, gzip·헤더·키 함정을 넘은 기록.&lt;/div&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:28px 0;&quot;&gt;&lt;/div&gt;

  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;10일차에서 AppsFlyer를 Pull API로 받는 배치를 만들었다면, 이번엔 &lt;strong&gt;Adjust&lt;/strong&gt;를 붙였다. 그런데 접근 방식이 다르다. 우리가 API로 당겨오는(pull) 게 아니라, &lt;strong&gt;Adjust가 매시 정각 우리 S3 버킷에 CSV를 올려주고(push)&lt;/strong&gt;, 우리는 그걸 받아 DB에 쌓는다. S3가 중간에 끼는 구조다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;이 글은 Adjust 어트리뷰션 raw 데이터를 S3 경유로 받아 사내 BI 대시보드까지 잇는 파이프라인의 구축 기록이다. 전체 흐름과 함께, 실제로 시간을 잡아먹은 함정들 — &lt;strong&gt;IAM 키 역할 분리, &quot;.csv.gz인데 평문&quot;, 헤더 중괄호, 업로더 키 AccessDenied&lt;/strong&gt; — 과 멱등·소급 설계를 담았다. (식별자·수치는 예시다.)&lt;/p&gt;

  &lt;!-- 파이프라인 SVG --&gt;
  &lt;div style=&quot;margin:24px 0;&quot;&gt;
    &lt;div style=&quot;border:1px solid #eceef1;border-radius:12px;background:#fbfcfd;padding:14px;&quot;&gt;
      &lt;svg viewBox=&quot;0 0 760 250&quot; style=&quot;width:100%;height:auto;display:block;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;Adjust가 S3에 push, 배치가 pull해서 DB 적재 후 대시보드. IAM 업로더/리더 분리&quot;&gt;
        &lt;defs&gt;
          &lt;marker id=&quot;ah20&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#879196&quot;/&gt;&lt;/marker&gt;
          &lt;linearGradient id=&quot;a20green&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;&lt;stop offset=&quot;0&quot; stop-color=&quot;#34B37E&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#1C8F5F&quot;/&gt;&lt;/linearGradient&gt;
          &lt;linearGradient id=&quot;a20orange&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;&lt;stop offset=&quot;0&quot; stop-color=&quot;#F58536&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#C8511B&quot;/&gt;&lt;/linearGradient&gt;
          &lt;linearGradient id=&quot;a20blue&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;&lt;stop offset=&quot;0&quot; stop-color=&quot;#4D9BF5&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#2160C4&quot;/&gt;&lt;/linearGradient&gt;
          &lt;linearGradient id=&quot;a20purple&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;&lt;stop offset=&quot;0&quot; stop-color=&quot;#A166FF&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#6B2FD6&quot;/&gt;&lt;/linearGradient&gt;
        &lt;/defs&gt;

        &lt;!-- Adjust --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;20&quot; y=&quot;44&quot; width=&quot;52&quot; height=&quot;52&quot; rx=&quot;10&quot; fill=&quot;url(#a20green)&quot;/&gt;
          &lt;path d=&quot;M34 78 L44 58 L52 68 L62 52&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.6&quot; fill=&quot;none&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;/&gt;
          &lt;text x=&quot;46&quot; y=&quot;112&quot; text-anchor=&quot;middle&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;Adjust&lt;/text&gt;
          &lt;text x=&quot;46&quot; y=&quot;127&quot; text-anchor=&quot;middle&quot; font-size=&quot;9.5&quot; fill=&quot;#687078&quot;&gt;어트리뷰션&lt;/text&gt;
        &lt;/g&gt;
        &lt;!-- S3 --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;188&quot; y=&quot;44&quot; width=&quot;52&quot; height=&quot;52&quot; rx=&quot;10&quot; fill=&quot;url(#a20green)&quot;/&gt;
          &lt;path d=&quot;M202 62 q12 -8 24 0 v16 q-12 8 -24 0 z&quot; fill=&quot;#fff&quot;/&gt;
          &lt;text x=&quot;214&quot; y=&quot;112&quot; text-anchor=&quot;middle&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;S3 버킷&lt;/text&gt;
          &lt;text x=&quot;214&quot; y=&quot;127&quot; text-anchor=&quot;middle&quot; font-size=&quot;9.5&quot; fill=&quot;#687078&quot;&gt;전용·퍼블릭 차단&lt;/text&gt;
        &lt;/g&gt;
        &lt;!-- 배치 --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;356&quot; y=&quot;44&quot; width=&quot;52&quot; height=&quot;52&quot; rx=&quot;10&quot; fill=&quot;url(#a20orange)&quot;/&gt;
          &lt;path d=&quot;M370 58 h8 l14 28 h-8 l-5-11 -7 14 h-8 z&quot; fill=&quot;#fff&quot;/&gt;
          &lt;text x=&quot;382&quot; y=&quot;112&quot; text-anchor=&quot;middle&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;배치(cron)&lt;/text&gt;
          &lt;text x=&quot;382&quot; y=&quot;127&quot; text-anchor=&quot;middle&quot; font-size=&quot;9.5&quot; fill=&quot;#687078&quot;&gt;gzip판별·파싱&lt;/text&gt;
        &lt;/g&gt;
        &lt;!-- DB --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;524&quot; y=&quot;44&quot; width=&quot;52&quot; height=&quot;52&quot; rx=&quot;10&quot; fill=&quot;url(#a20blue)&quot;/&gt;
          &lt;ellipse cx=&quot;550&quot; cy=&quot;60&quot; rx=&quot;14&quot; ry=&quot;5&quot; fill=&quot;none&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.2&quot;/&gt;
          &lt;path d=&quot;M536 60 v20 q0 5 14 5 t14 -5 v-20&quot; fill=&quot;none&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.2&quot;/&gt;
          &lt;text x=&quot;550&quot; y=&quot;112&quot; text-anchor=&quot;middle&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;MySQL&lt;/text&gt;
          &lt;text x=&quot;550&quot; y=&quot;127&quot; text-anchor=&quot;middle&quot; font-size=&quot;9.5&quot; fill=&quot;#687078&quot;&gt;raw·집계·log·map&lt;/text&gt;
        &lt;/g&gt;
        &lt;!-- 대시보드 --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;692&quot; y=&quot;44&quot; width=&quot;52&quot; height=&quot;52&quot; rx=&quot;10&quot; fill=&quot;url(#a20purple)&quot;/&gt;
          &lt;rect x=&quot;705&quot; y=&quot;58&quot; width=&quot;26&quot; height=&quot;24&quot; rx=&quot;2&quot; fill=&quot;none&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2&quot;/&gt;
          &lt;path d=&quot;M710 76 v-6 M718 76 v-12 M726 76 v-9&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.2&quot; stroke-linecap=&quot;round&quot;/&gt;
          &lt;text x=&quot;718&quot; y=&quot;112&quot; text-anchor=&quot;middle&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;대시보드&lt;/text&gt;
          &lt;text x=&quot;718&quot; y=&quot;127&quot; text-anchor=&quot;middle&quot; font-size=&quot;9.5&quot; fill=&quot;#687078&quot;&gt;사내 IP+auth&lt;/text&gt;
        &lt;/g&gt;

        &lt;line x1=&quot;72&quot; y1=&quot;70&quot; x2=&quot;184&quot; y2=&quot;70&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah20)&quot;/&gt;
        &lt;text x=&quot;128&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#1C8F5F&quot; font-weight=&quot;700&quot;&gt;push(업로더)&lt;/text&gt;
        &lt;line x1=&quot;240&quot; y1=&quot;70&quot; x2=&quot;352&quot; y2=&quot;70&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah20)&quot;/&gt;
        &lt;text x=&quot;296&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#C8511B&quot; font-weight=&quot;700&quot;&gt;pull(리더)&lt;/text&gt;
        &lt;line x1=&quot;408&quot; y1=&quot;70&quot; x2=&quot;520&quot; y2=&quot;70&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah20)&quot;/&gt;
        &lt;line x1=&quot;576&quot; y1=&quot;70&quot; x2=&quot;688&quot; y2=&quot;70&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah20)&quot;/&gt;

        &lt;text x=&quot;380&quot; y=&quot;165&quot; text-anchor=&quot;middle&quot; font-size=&quot;12&quot; fill=&quot;#687078&quot;&gt;IAM 키 분리: Adjust=쓰기(PutObject) / 배치=읽기(GetObject)&lt;/text&gt;
      &lt;/svg&gt;
    &lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#8b95a1;margin-top:10px;text-align:center;&quot;&gt;그림 1. Adjust가 S3에 push, 배치가 pull. 업로더 키는 쓰기만, 리더 키는 읽기만.&lt;/div&gt;
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;핵심 설계 결정 하나 — &lt;strong&gt;Adjust 관련 전체(배치 + 대시보드)를 게임 서버와 분리한 독립 프로젝트&lt;/strong&gt;로 만들었다. 왜 중요했는지는 아래에서 드러난다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;왜 pull이 아니라 &quot;S3 push&quot;인가&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;Adjust는 raw 데이터를 &lt;strong&gt;Cloud storage upload&lt;/strong&gt; 기능으로 내보낸다. 우리가 API로 당기는 게 아니라, Adjust가 매시 정각 지정한 S3 버킷에 CSV를 던져넣는다. 파일명은 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;{app_token}_{날짜}T{시각}_{해시}_{seq}.csv.gz&lt;/code&gt; 형태. 이 push 모델의 장점은 &lt;strong&gt;Adjust API rate limit을 신경 쓸 필요가 없다&lt;/strong&gt;는 것, 단점은 &quot;S3에 뭐가 왔는지&quot;를 우리가 관리해야 한다는 것(그래서 file_log 테이블이 필요하다).&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;함정 ① IAM 키를 &quot;업로더&quot;와 &quot;리더&quot;로 분리&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;S3를 사이에 둔 두 주체는 권한이 정반대다. Adjust는 &lt;strong&gt;쓰기만&lt;/strong&gt;, 우리 배치는 &lt;strong&gt;읽기만&lt;/strong&gt; 하면 된다. 그래서 IAM User를 둘로 나눴다 — &lt;strong&gt;최소 권한 원칙&lt;/strong&gt;.&lt;/p&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;IAM User&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;용도&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;핵심 권한&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;업로더&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;Adjust가 업로드&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;s3:PutObject&lt;/code&gt; (버킷 한정)&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;리더&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;우리 배치가 다운로드&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;s3:GetObject&lt;/code&gt; (버킷 한정)&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background:#fff8f3;border-left:3px solid #e8833a;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#8a5420;font-size:16px;&quot;&gt;
    &lt;strong&gt;겪은 함정&lt;/strong&gt; — 배치 config에 &lt;strong&gt;업로더 키&lt;/strong&gt;를 넣었더니 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;GetObject AccessDenied&lt;/code&gt;. 업로더는 쓰기 전용이라 읽기가 안 된다. 배치에는 반드시 &lt;strong&gt;리더 키&lt;/strong&gt;를 넣어야 한다. 외부에 넘기는 키가 새도 남의 파일을 못 읽고, 우리 배치 키가 새도 버킷에 못 쓴다 — 분리의 이점이다.
  &lt;/div&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;함정 ② &quot;.csv.gz&quot;인데 내용이 평문이다&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;배치가 gzip 해제를 하는데 어느 날 &lt;strong&gt;전량 SKIP&lt;/strong&gt;됐다. 확장자는 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;.csv.gz&lt;/code&gt;인데 내용이 &lt;strong&gt;평문 CSV&lt;/strong&gt;였다(데이터가 적을 때 압축 안 하고 올리는 경우). 해결은 확장자를 믿지 말고 &lt;strong&gt;gzip 매직넘버로 실제 압축 여부를 판별&lt;/strong&gt;하는 것.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;// 확장자(.gz)를 믿지 말고 파일 앞 2바이트로 gzip 여부 판별
$head = substr($content, 0, 2);
if ($head === &quot;\x1f\x8b&quot;) {        // gzip 매직넘버 1f 8b
    $csv = gzdecode($content);     // 압축이면 해제
} else {
    $csv = $content;               // 평문이면 그대로
}&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;교훈은 분명하다. &lt;strong&gt;파일 확장자는 &quot;약속&quot;일 뿐 &quot;보장&quot;이 아니다.&lt;/strong&gt; 외부가 주는 파일은 확장자가 아니라 실제 바이트로 판별한다. 2바이트 체크 한 줄이 에러 폭탄을 막았다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;함정 ③ 헤더가 중괄호로 감싸져 있다&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;gzip을 풀고 파싱했더니 이번엔 &lt;strong&gt;모든 컬럼이 빈 값&lt;/strong&gt;. 헤더가 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;{activity_kind},{created_at},...&lt;/code&gt;처럼 컬럼명이 중괄호로 감싸져 있었다(값엔 없음). &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;activity_kind&lt;/code&gt; 키로 찾으면 실제 헤더 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;{activity_kind}&lt;/code&gt;와 안 맞아 전부 빈 값이 된 것.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;// 헤더만 중괄호가 있으므로 헤더 파싱 시 제거
$header = array_map(function($h) {
    return trim($h, &quot;{} \t\r\n&quot;);   // 양끝 중괄호+공백 제거
}, str_getcsv($headerLine));&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 14px;&quot;&gt;추가로 반영한 Adjust CSV 특성:&lt;/p&gt;
  &lt;ul style=&quot;margin:0 0 20px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;시각이 유닉스 타임스탬프(초)&lt;/strong&gt; — &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;created_at=1792461704&lt;/code&gt; 형태. UTC로 변환해 저장.&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;environment&lt;/strong&gt; — sandbox/production/빈값이 섞임. 실매출 집계는 production 기준만.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;매체 전용 컬럼은 대부분 빈 값&lt;/strong&gt; — 매체 파트너(Google/Meta/TikTok) 연동 전이라 안 채워진다. 연동 후부터 채워지고 &lt;strong&gt;과거는 소급 불가&lt;/strong&gt;(그래서 매체 연동은 빠를수록 좋다).&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;DB 설계 — raw + 집계 + file_log + app_map&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;10일차의 &quot;raw + 집계&quot; 패턴에 &lt;strong&gt;file_log(멱등)&lt;/strong&gt;와 &lt;strong&gt;app_map(게임 구분)&lt;/strong&gt;을 더했다.&lt;/p&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;테이블&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;역할&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;raw&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;CSV 1행=1레코드. 정규 컬럼 + &lt;strong&gt;원본 JSON&lt;/strong&gt;(소급용)&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;dashboard&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;일자×플랫폼×매체×국가×환경 집계(조회 전용)&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;file_log&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;처리한 S3 key + status. &lt;strong&gt;멱등·중복방지&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;app_map&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;app_token→게임/플랫폼. &lt;strong&gt;신규 게임은 2줄 추가&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;
  &lt;ul style=&quot;margin:0 0 20px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;멱등 적재&lt;/strong&gt; — 같은 키를 DELETE 후 INSERT. 과거 파일을 재처리해도 중복이 안 쌓인다. file_log status='done'으로 미처리 파일만 집어간다.&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;소급 가능성&lt;/strong&gt; — S3 원본은 안 지운다(source of truth). raw에 원본 JSON을 보관해 나중에 &quot;이 컬럼도 정규화&quot;가 생기면 다시 뽑는다.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;게임 자동 등장&lt;/strong&gt; — app_map에 app_token 2줄(AOS/iOS)만 넣으면 대시보드 필터에 자동으로 나타난다. 신규 게임 추가가 &quot;코드 수정&quot;이 아니라 &quot;데이터 한 줄&quot;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;strong&gt;집계 정확도&lt;/strong&gt; — 가산 지표(설치·매출·결제 수)는 집계 테이블 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;SUM&lt;/code&gt;. 하지만 distinct 지표(DAU·결제자 수)는 집계를 SUM하면 &lt;strong&gt;차원 중복으로 과대 계산&lt;/strong&gt;된다. 이건 raw에서 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;COUNT(DISTINCT adid)&lt;/code&gt;로 재집계해야 정확하다.
  &lt;/div&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;배치 &amp; 대시보드&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;&lt;strong&gt;배치&lt;/strong&gt;는 독립 PHP를 cron으로. UTC 기준(gmdate), DB NOW() 미사용. 미처리 파일만 처리하고 실패 파일은 error로 기록돼 다음에 재시도된다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;# UTC 21:00 = KST 06:00, 전날 올라온 파일 적재
0 21 * * * /path/to/php /path/to/adjust_daily.php &amp;gt;&amp;gt; /path/to/logs/adjust_daily.log 2&amp;gt;&amp;amp;1&lt;/code&gt;&lt;/pre&gt;
  &lt;div style=&quot;background:#fff8f3;border-left:3px solid #e8833a;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#8a5420;font-size:16px;&quot;&gt;
    &lt;strong&gt;겪은 함정&lt;/strong&gt; — cron은 돌았는데 데이터가 안 쌓였다. 로그 리다이렉트 경로의 &lt;strong&gt;logs 디렉토리가 없어서&lt;/strong&gt; cron이 공회전하고 있었다. 디렉토리를 먼저 만들어야 한다. &quot;cron이 도는데 왜 결과가 없지?&quot;로 한참 헤매게 만드는 전형적 함정.
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;&lt;strong&gt;대시보드&lt;/strong&gt;는 순수 PHP + ECharts(CDN). 빌드 없이 기존 php-fpm 재사용. 접근은 &lt;strong&gt;사내 IP만 허용 + basic auth&lt;/strong&gt;. 필터는 날짜 범위 / 게임(app_map 동적) / 플랫폼 / 환경(기본 production) / 국가 TopN, 지표는 설치·DAU·매출·플랫폼·국가 요약과 KPI 카드.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;핵심 설계 — 독립 프로젝트로 분리한 이유&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;처음엔 Adjust 배치를 게임 서버 코드베이스 안에 뒀다. 그런데 &lt;strong&gt;게임 서버를 재배포할 때마다 Adjust용 config(S3 키 등)가 플레이스홀더로 덮여버리는&lt;/strong&gt; 일이 생겼다. 배포 파이프라인이 config를 덮어쓴 것. 그래서 &lt;strong&gt;Adjust 전체를 독립 프로젝트로 분리&lt;/strong&gt;했다.&lt;/p&gt;
  &lt;ul style=&quot;margin:0 0 20px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;게임 서버 재배포가 Adjust 쪽에 영향을 안 준다(config가 안 덮인다)&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;config는 gitignore로 서버에만, 샘플에는 플레이스홀더만&lt;/li&gt;
    &lt;li&gt;의존성(AWS SDK)도 이 프로젝트 composer로만 관리 → 게임 서버와 무관&lt;/li&gt;
  &lt;/ul&gt;
  &lt;div style=&quot;background:#fff8f3;border-left:3px solid #e8833a;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#8a5420;font-size:16px;&quot;&gt;
    한 번은 샘플 config에 실제 비밀번호가 치환돼 커밋될 뻔했다. HEAD로 복구하고 &lt;strong&gt;샘플엔 플레이스홀더만&lt;/strong&gt; 유지하는 규칙을 세웠다. BI 파이프라인은 자격증명을 많이 다루므로 &quot;실제 키는 gitignore config에만, 샘플엔 플레이스홀더만&quot;을 처음부터 못 박는 게 안전하다.
  &lt;/div&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;겪은 함정 총정리&lt;/h2&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;증상&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;원인&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;해결&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;gzip 전량 SKIP&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;.csv.gz인데 평문&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;매직넘버(1f8b) 판별&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;raw 전 컬럼 빈 값&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;헤더 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:12px;&quot;&gt;{컬럼}&lt;/code&gt; 중괄호&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;헤더 파싱 시 중괄호 제거&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;cron 돌았는데 데이터 없음&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;로그 디렉토리 없음&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;logs 디렉토리 선생성&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;재배포 후 키가 덮임&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;게임 배포가 config 덮음&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;독립 프로젝트 + config gitignore&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;GetObject AccessDenied&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;배치에 업로더 키&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;리더 키로 교체&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;마치며&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;Adjust BI의 큰 틀은 10일차(AppsFlyer)와 같다 — 외부 어트리뷰션 데이터를 raw + 집계로 쌓고 대시보드로 본다. 다른 건 &lt;strong&gt;S3가 중간에 끼는 push 모델&lt;/strong&gt;, 그래서 &lt;strong&gt;IAM 키를 업로더/리더로 나눠야&lt;/strong&gt; 한다는 점이다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;실제로 발목을 잡은 건 큰 설계가 아니라 &lt;strong&gt;자잘한 형식 함정&lt;/strong&gt;이었다. 확장자는 gz인데 평문, 헤더는 중괄호, 배치에 엉뚱한 키, 없는 로그 디렉토리. 외부가 주는 데이터는 &lt;strong&gt;&quot;약속된 형식&quot;을 믿지 말고 실제 바이트·실제 헤더로 확인&lt;/strong&gt;해야 한다. 그리고 데이터 파이프라인은 자격증명을 많이 다루니, 처음부터 &lt;strong&gt;최소 권한 IAM 분리 + config gitignore + 독립 배포&lt;/strong&gt;를 깔아두면 재배포·키 유출 사고에서 피해가 작다.&lt;/p&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:36px 0 20px;&quot;&gt;&lt;/div&gt;
  &lt;div style=&quot;background:#f8f9fa;border:1px solid #eceef1;border-radius:12px;padding:20px;text-align:center;margin-bottom:12px;&quot;&gt;
    &lt;div style=&quot;font-weight:700;color:#191f28;font-size:15px;&quot;&gt;어트리뷰션 BI · 데이터 파이프라인 · S3/IAM 설계 문의&lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#6b7684;margin-top:6px;&quot;&gt;댓글로 편하게 남겨주세요.&lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI  개발</category>
      <category>Adjust</category>
      <category>BI</category>
      <category>ECharts</category>
      <category>Iam</category>
      <category>MySQL</category>
      <category>s3</category>
      <category>게임서버</category>
      <category>데이터파이프라인</category>
      <category>마케팅분석</category>
      <category>어트리뷰션</category>
      <author>oldserver</author>
      <guid isPermaLink="true">https://oldserver.tistory.com/20</guid>
      <comments>https://oldserver.tistory.com/20#entry20comment</comments>
      <pubDate>Mon, 26 Oct 2026 10:15:46 +0900</pubDate>
    </item>
    <item>
      <title>EUR 결제 금액이 100배로 저장됐다 &amp;mdash; 로케일 콤마(,) 소수점 함정과 데이터 보정</title>
      <link>https://oldserver.tistory.com/18</link>
      <description>&lt;div style=&quot;max-width:720px;margin:0 auto;padding:0 8px;color:#2b2f36;line-height:2.0;font-size:17px;word-break:keep-all;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot;&gt;

  &lt;div style=&quot;font-size:13px;font-weight:700;color:#3182f6;letter-spacing:.5px;margin:18px 0 14px;&quot;&gt;TROUBLESHOOTING · 결제&lt;/div&gt;
  &lt;div style=&quot;font-size:32px;font-weight:800;line-height:1.4;color:#191f28;letter-spacing:-.3px;&quot;&gt;EUR 결제 금액이 100배로 저장됐다&lt;/div&gt;
  &lt;div style=&quot;font-size:17px;color:#6b7684;margin:16px 0 8px;line-height:1.8;&quot;&gt;&quot;22,99&quot;가 2299로. 범인은 str_replace 한 줄과, 콤마를 소수점으로 쓰는 로케일이었다.&lt;/div&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:28px 0;&quot;&gt;&lt;/div&gt;

  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;어느 날 매출 지표가 이상하게 튀었다. 특정 패키지 몇 건이 수천 달러로 집계된 것이다. 실제로 그렇게 팔렸을 리 없는 숫자. 추적해보니 &lt;strong&gt;EUR 결제 일부 건의 가격이 서버에 100배로 저장&lt;/strong&gt;되고 있었다. &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;22.99&lt;/code&gt;가 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;2299&lt;/code&gt;로, &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;5.99&lt;/code&gt;가 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;599&lt;/code&gt;로.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;이 글은 그 &quot;가격 100배&quot; 버그의 원인과 데이터 보정 과정이다. 범인은 결제 코드의 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;str_replace&lt;/code&gt; 한 줄이었고, 더 정확히는 &lt;strong&gt;소수점 구분자로 콤마를 쓰는 로케일&lt;/strong&gt;이라는, 유럽·러시아 결제를 붙이면 반드시 만나는 함정이었다. (금액·건수는 예시다.)&lt;/p&gt;

  &lt;!-- 변환 다이어그램 --&gt;
  &lt;div style=&quot;margin:24px 0;&quot;&gt;
    &lt;div style=&quot;border:1px solid #eceef1;border-radius:12px;background:#fbfcfd;padding:14px;&quot;&gt;
      &lt;svg viewBox=&quot;0 0 760 200&quot; style=&quot;width:100%;height:auto;display:block;font-family:ui-monospace,monospace;&quot; role=&quot;img&quot; aria-label=&quot;점으로 온 값은 정상, 콤마로 온 값은 콤마 제거로 100배가 되는 과정&quot;&gt;
        &lt;defs&gt;
          &lt;marker id=&quot;ah17&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#879196&quot;/&gt;&lt;/marker&gt;
        &lt;/defs&gt;
        &lt;!-- 정상 경로 --&gt;
        &lt;rect x=&quot;20&quot; y=&quot;30&quot; width=&quot;150&quot; height=&quot;44&quot; rx=&quot;9&quot; fill=&quot;#f0fdf4&quot; stroke=&quot;#bbf7d0&quot;/&gt;
        &lt;text x=&quot;95&quot; y=&quot;51&quot; text-anchor=&quot;middle&quot; font-size=&quot;15&quot; font-weight=&quot;700&quot; fill=&quot;#15803d&quot;&gt;&quot;22.99&quot;&lt;/text&gt;
        &lt;text x=&quot;95&quot; y=&quot;66&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#5aa17a&quot; font-family=&quot;sans-serif&quot;&gt;점(.) 로케일&lt;/text&gt;
        &lt;text x=&quot;230&quot; y=&quot;46&quot; font-size=&quot;11&quot; fill=&quot;#687078&quot; font-family=&quot;sans-serif&quot;&gt;콤마 제거&lt;/text&gt;
        &lt;text x=&quot;230&quot; y=&quot;60&quot; font-size=&quot;11&quot; fill=&quot;#687078&quot; font-family=&quot;sans-serif&quot;&gt;(콤마 없음)&lt;/text&gt;
        &lt;line x1=&quot;170&quot; y1=&quot;52&quot; x2=&quot;330&quot; y2=&quot;52&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.5&quot; marker-end=&quot;url(#ah17)&quot;/&gt;
        &lt;rect x=&quot;340&quot; y=&quot;30&quot; width=&quot;150&quot; height=&quot;44&quot; rx=&quot;9&quot; fill=&quot;#f0fdf4&quot; stroke=&quot;#bbf7d0&quot;/&gt;
        &lt;text x=&quot;415&quot; y=&quot;51&quot; text-anchor=&quot;middle&quot; font-size=&quot;15&quot; font-weight=&quot;700&quot; fill=&quot;#15803d&quot;&gt;22.99&lt;/text&gt;
        &lt;text x=&quot;415&quot; y=&quot;66&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#5aa17a&quot; font-family=&quot;sans-serif&quot;&gt;정상&lt;/text&gt;
        &lt;text x=&quot;560&quot; y=&quot;56&quot; font-size=&quot;13&quot; fill=&quot;#15803d&quot; font-family=&quot;sans-serif&quot;&gt;✓ 영향 없음&lt;/text&gt;

        &lt;!-- 버그 경로 --&gt;
        &lt;rect x=&quot;20&quot; y=&quot;120&quot; width=&quot;150&quot; height=&quot;44&quot; rx=&quot;9&quot; fill=&quot;#fef2f2&quot; stroke=&quot;#fecaca&quot;/&gt;
        &lt;text x=&quot;95&quot; y=&quot;141&quot; text-anchor=&quot;middle&quot; font-size=&quot;15&quot; font-weight=&quot;700&quot; fill=&quot;#b91c1c&quot;&gt;&quot;22,99&quot;&lt;/text&gt;
        &lt;text x=&quot;95&quot; y=&quot;156&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#c87171&quot; font-family=&quot;sans-serif&quot;&gt;콤마(,) 로케일&lt;/text&gt;
        &lt;text x=&quot;230&quot; y=&quot;136&quot; font-size=&quot;11&quot; fill=&quot;#b91c1c&quot; font-family=&quot;sans-serif&quot;&gt;콤마 제거&lt;/text&gt;
        &lt;text x=&quot;230&quot; y=&quot;150&quot; font-size=&quot;11&quot; fill=&quot;#b91c1c&quot; font-family=&quot;sans-serif&quot;&gt;(소수점 사라짐!)&lt;/text&gt;
        &lt;line x1=&quot;170&quot; y1=&quot;142&quot; x2=&quot;330&quot; y2=&quot;142&quot; stroke=&quot;#c0392b&quot; stroke-width=&quot;1.5&quot; marker-end=&quot;url(#ah17)&quot;/&gt;
        &lt;rect x=&quot;340&quot; y=&quot;120&quot; width=&quot;150&quot; height=&quot;44&quot; rx=&quot;9&quot; fill=&quot;#fef2f2&quot; stroke=&quot;#fecaca&quot;/&gt;
        &lt;text x=&quot;415&quot; y=&quot;141&quot; text-anchor=&quot;middle&quot; font-size=&quot;15&quot; font-weight=&quot;700&quot; fill=&quot;#b91c1c&quot;&gt;2299&lt;/text&gt;
        &lt;text x=&quot;415&quot; y=&quot;156&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#c87171&quot; font-family=&quot;sans-serif&quot;&gt;100배!&lt;/text&gt;
        &lt;text x=&quot;560&quot; y=&quot;146&quot; font-size=&quot;13&quot; fill=&quot;#b91c1c&quot; font-family=&quot;sans-serif&quot;&gt;✗ 가격 왜곡&lt;/text&gt;
      &lt;/svg&gt;
    &lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#8b95a1;margin-top:10px;text-align:center;&quot;&gt;그림 1. 같은 &quot;콤마 제거&quot; 코드가 점 로케일엔 무해하지만 콤마 로케일엔 100배 버그를 만든다.&lt;/div&gt;
  &lt;/div&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;현상 — 전부가 아니라 &quot;일부만&quot; 깨졌다&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;빌링 업데이트 이후부터 증상이 나타났다. 특징이 중요하다.&lt;/p&gt;
  &lt;ul style=&quot;margin:0 0 20px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;EUR 결제 &lt;strong&gt;일부&lt;/strong&gt; 건만 100배: &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;22.99→2299&lt;/code&gt;, &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;5.99→599&lt;/code&gt;&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;전부가 아니라 일부라는 게 단서였다. &lt;strong&gt;점(.)으로 온 건은 정상, 콤마(,)로 온 건만&lt;/strong&gt; 깨졌다.&lt;/li&gt;
    &lt;li&gt;실제 결제·아이템 지급은 멀쩡. &lt;strong&gt;DB의 price 값과 매출 통계만&lt;/strong&gt; 왜곡됐다.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;&quot;일부만 깨진다&quot;는 건 거의 항상 &lt;strong&gt;입력 데이터의 형태가 둘 이상&lt;/strong&gt;이라는 신호다. 여기선 그게 소수점 구분자(점 vs 콤마)였다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;원인 — 콤마를 &quot;변환&quot;이 아니라 &quot;제거&quot;&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;소수점 구분자로 콤마를 쓰는 로케일(독일·프랑스 등 유럽 다수)에서는 마켓이 가격을 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;&quot;22,99&quot;&lt;/code&gt;처럼 콤마로 내려준다. 영어권은 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;&quot;22.99&quot;&lt;/code&gt;로 점을 쓰고. 서버 코드는 가격에서 콤마를 &lt;strong&gt;제거&lt;/strong&gt;하고 있었다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;// 콤마를 제거만 함 → &quot;22,99&quot; 가 &quot;2299&quot; 로 바뀐다
$price = !empty($row['price_str']) ? str_replace(',', '', $row['price_str']) : 0;
// 이후: UPDATE payment SET price = {$price} ...&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;str_replace(',', '')&lt;/code&gt;는 &quot;천 단위 콤마 제거&quot;를 의도한 코드였을 것이다(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;1,299 → 1299&lt;/code&gt;). 그런데 &lt;strong&gt;콤마가 천 단위 구분자가 아니라 소수점 구분자로도 쓰인다&lt;/strong&gt;는 걸 놓쳤다. 같은 콤마 기호가 로케일에 따라 정반대 의미를 가진다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;수정 — 콤마를 소수점으로 정규화&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;핵심은 &quot;콤마 무조건 제거&quot;가 아니라 &lt;strong&gt;&quot;이 콤마가 소수점인지 천 단위인지 판단&quot;&lt;/strong&gt;하는 것이다. 가장 단순한 규칙은 &quot;문자열의 &lt;strong&gt;마지막&lt;/strong&gt; 구분자(점이든 콤마든)를 소수점으로 본다&quot;이다. 그 뒤 자릿수는 소수, 앞의 구분자는 전부 천 단위로 제거.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;// &quot;22,99&quot;-&gt;22.99, &quot;1,299.00&quot;-&gt;1299.00, &quot;1.299,00&quot;-&gt;1299.00
$price = normalize_price_locale(trim($row['price_str']));
// 규칙: 마지막 구분자 = 소수점, 그 앞 구분자 = 천 단위(제거)&lt;/code&gt;&lt;/pre&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;데이터 보정 — 조회 → 백업 → UPDATE&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;코드를 고쳐도 &lt;strong&gt;이미 100배로 저장된 과거 데이터&lt;/strong&gt;는 그대로다. 실데이터 보정은 순서가 생명이다. &lt;strong&gt;① 대상 조회 → ② 백업 → ③ UPDATE&lt;/strong&gt;, 세 쿼리의 WHERE를 완전히 동일하게.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 10px;font-weight:700;color:#191f28;&quot;&gt;① 대상 조회 (UPDATE 전 반드시 확인)&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:0 0 20px;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;SELECT idx, user_id, product_id, price, price/100 AS fixed_price, currency_code, reg_dttm
FROM payment
WHERE currency_code = 'EUR'
  AND status = 5
  AND reg_dttm &amp;gt;= '2026-10-22 00:00:00'
  AND price = FLOOR(price)      -- 소수점 없는 정수 = 콤마 제거로 깨진 건
ORDER BY reg_dttm DESC;&lt;/code&gt;&lt;/pre&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:0 0 20px;color:#4e5968;font-size:16px;&quot;&gt;
    여기서 &lt;strong&gt;&lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;fixed_price&lt;/code&gt;가 전부 x.99 / x.49 같은 정상 가격으로 떨어지는지&lt;/strong&gt; 눈으로 확인한다. 이게 보정의 안전장치다. 정상적으로 정수 가격인 EUR 상품이 섞여 있으면 그것도 1/100 되니, 조회 결과를 반드시 검토한다.
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 10px;font-weight:700;color:#191f28;&quot;&gt;② 백업 (되돌릴 수 있게)&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:0 0 20px;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;CREATE TABLE payment_bak_20261023 AS
SELECT * FROM payment
WHERE currency_code = 'EUR' AND status = 5
  AND reg_dttm &amp;gt;= '2026-10-22 00:00:00'
  AND price = FLOOR(price);&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 10px;font-weight:700;color:#191f28;&quot;&gt;③ 보정 UPDATE (조회와 동일 조건)&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:0 0 20px;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;UPDATE payment
SET price = price / 100
WHERE currency_code = 'EUR' AND status = 5
  AND reg_dttm &amp;gt;= '2026-10-22 00:00:00'
  AND price = FLOOR(price);&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;조회·백업·UPDATE의 &lt;strong&gt;WHERE 조건을 완전히 동일하게&lt;/strong&gt; 두는 게 중요하다. 조회로 확인한 대상 = 백업한 대상 = 보정한 대상이 1:1로 맞아야 &quot;본 것만 고쳤다&quot;가 보장된다. 보정 후 매출 지표를 재집계해 숫자가 정상으로 돌아왔는지 확인한다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;짚고 갈 지점&lt;/h2&gt;
  &lt;ul style=&quot;margin:0 0 20px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;price = FLOOR(price)&lt;/code&gt; 판별의 전제&lt;/strong&gt; — &quot;정수면 깨진 것&quot;이라는 가정이다. 정상 정수 가격 상품이 있으면 오판될 수 있으니 ①조회에서 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;fixed_price&lt;/code&gt;를 꼭 확인한다. 가정에 기대는 조건일수록 사람이 눈으로 검증한다.&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;시간 경계&lt;/strong&gt; — &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;reg_dttm&lt;/code&gt;이 UTC/KST 무엇이든 그날 전체가 포함되게 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;00:00:00&lt;/code&gt;부터로 넉넉히. 경계에서 놓치는 것보다 범위를 넓게 잡고 조회로 거르는 게 안전.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;영향 범위를 좁혔다&lt;/strong&gt; — 이 버그는 price와 통계만 왜곡했을 뿐 결제 승인·지급은 멀쩡했다. &quot;price만, 통계만&quot;이라고 선을 그어야 과잉 롤백을 안 한다.&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;마치며&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;교훈은 하나다. &lt;strong&gt;같은 기호(콤마)가 로케일에 따라 정반대 의미를 가진다.&lt;/strong&gt; 영어권에선 천 단위 구분자, 유럽 다수에선 소수점 구분자. &quot;콤마 제거&quot;라는 한 줄이 어떤 로케일에선 맞고 어떤 로케일에선 100배 버그가 된다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;통화·가격을 다룰 때는 문자열을 직접 자르지 말고 &lt;strong&gt;로케일을 의식해 정규화&lt;/strong&gt;하거나, 처음부터 &lt;strong&gt;최소 단위 정수(센트)&lt;/strong&gt;로 다루는 게 안전하다. 그리고 운영 데이터 보정은 언제나 &lt;strong&gt;조회 → 백업 → (동일 조건) UPDATE&lt;/strong&gt;. 이 순서만 지키면 &quot;본 것만 고친다&quot;가 보장된다.&lt;/p&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:36px 0 20px;&quot;&gt;&lt;/div&gt;
  &lt;div style=&quot;background:#f8f9fa;border:1px solid #eceef1;border-radius:12px;padding:20px;text-align:center;margin-bottom:12px;&quot;&gt;
    &lt;div style=&quot;font-weight:700;color:#191f28;font-size:15px;&quot;&gt;결제 연동 · 다국가 통화 · 운영 데이터 보정 문의&lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#6b7684;margin-top:6px;&quot;&gt;댓글로 편하게 남겨주세요.&lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;</description>
      <category>클라우드 운영</category>
      <category>EUR</category>
      <category>MySQL</category>
      <category>php</category>
      <category>게임서버</category>
      <category>결제</category>
      <category>데이터보정</category>
      <category>로케일</category>
      <category>버그</category>
      <category>소수점</category>
      <category>통화</category>
      <author>oldserver</author>
      <guid isPermaLink="true">https://oldserver.tistory.com/18</guid>
      <comments>https://oldserver.tistory.com/18#entry18comment</comments>
      <pubDate>Sun, 25 Oct 2026 10:24:27 +0900</pubDate>
    </item>
    <item>
      <title>PHP 긴급 취약점(PHP2Shell, CVE-2026-109618/109619) 운영 서버 대응기</title>
      <link>https://oldserver.tistory.com/19</link>
      <description>&lt;div style=&quot;max-width:720px;margin:0 auto;padding:0 8px;color:#2b2f36;line-height:2.0;font-size:17px;word-break:keep-all;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot;&gt;

  &lt;div style=&quot;font-size:13px;font-weight:700;color:#3182f6;letter-spacing:.5px;margin:18px 0 14px;&quot;&gt;SECURITY · 긴급 대응&lt;/div&gt;
  &lt;div style=&quot;font-size:32px;font-weight:800;line-height:1.4;color:#191f28;letter-spacing:-.3px;&quot;&gt;PHP 긴급 취약점(PHP2Shell) 운영 서버 대응기&lt;/div&gt;
  &lt;div style=&quot;font-size:17px;color:#6b7684;margin:16px 0 8px;line-height:1.8;&quot;&gt;어려웠던 건 취약점이 아니라 &quot;서버마다 php.ini 경로와 FPM 서비스명이 제각각&quot;이라는 운영 현실이었다.&lt;/div&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:28px 0;&quot;&gt;&lt;/div&gt;

  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;PHP에서 치명적 보안 취약점 2건이 공개됐다. 통칭 &lt;strong&gt;PHP2Shell&lt;/strong&gt;(CVE-2026-109618, CVE-2026-109619). 둘 다 &lt;strong&gt;인증받지 않은 외부 공격자의 원격 코드 실행(RCE)&lt;/strong&gt;으로 이어질 수 있어 전 세계가 긴급 패치에 들어갔다. PHP로 웹 요청을 처리하는 거의 모든 구성(PHP-FPM, php-cgi, FrankenPHP, LiteSpeed)이 영향권이고, 특히 Apache mod_php가 위험하다고 알려졌다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;이 글은 운영 중인 여러 PHP 서버에 이 취약점을 대응한 실무 기록이다. 공격 방법이 아니라 &lt;strong&gt;우리 환경을 어떻게 점검하고 완화하고 패치했는지&lt;/strong&gt;만 다룬다.&lt;/p&gt;
  &lt;div style=&quot;background:#fff8f3;border-left:3px solid #e8833a;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#8a5420;font-size:15px;&quot;&gt;
    ⚠️ 취약점 원인은 공개된 기술 분석·공식 공지 기반이며, 각 CVE의 정확한 조건은 공식 보안 업데이트 확인이 필요하다. 이 글은 공격 방법이 아니라 방어/대응 설정만 다룬다.
  &lt;/div&gt;

  &lt;!-- 대응 흐름 SVG --&gt;
  &lt;div style=&quot;margin:24px 0;&quot;&gt;
    &lt;div style=&quot;border:1px solid #eceef1;border-radius:12px;background:#fbfcfd;padding:14px;&quot;&gt;
      &lt;svg viewBox=&quot;0 0 760 120&quot; style=&quot;width:100%;height:auto;display:block;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;영향도 확인 → 경로·서비스 확인 → 완화 → 패치 순서의 대응 흐름&quot;&gt;
        &lt;defs&gt;&lt;marker id=&quot;ah19&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#879196&quot;/&gt;&lt;/marker&gt;&lt;/defs&gt;
        &lt;rect x=&quot;16&quot; y=&quot;40&quot; width=&quot;150&quot; height=&quot;46&quot; rx=&quot;10&quot; fill=&quot;#eff6ff&quot; stroke=&quot;#bfdbfe&quot;/&gt;
        &lt;text x=&quot;91&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;700&quot; fill=&quot;#1d4ed8&quot;&gt;① 영향도&lt;/text&gt;
        &lt;text x=&quot;91&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#6b85c0&quot;&gt;의존·업로드 점검&lt;/text&gt;
        &lt;rect x=&quot;206&quot; y=&quot;40&quot; width=&quot;150&quot; height=&quot;46&quot; rx=&quot;10&quot; fill=&quot;#fff7e6&quot; stroke=&quot;#f1d58a&quot;/&gt;
        &lt;text x=&quot;281&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;700&quot; fill=&quot;#b7791f&quot;&gt;② 경로·서비스&lt;/text&gt;
        &lt;text x=&quot;281&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#a07d2a&quot;&gt;php --ini / fpm&lt;/text&gt;
        &lt;rect x=&quot;396&quot; y=&quot;40&quot; width=&quot;150&quot; height=&quot;46&quot; rx=&quot;10&quot; fill=&quot;#f0fdf4&quot; stroke=&quot;#bbf7d0&quot;/&gt;
        &lt;text x=&quot;471&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;700&quot; fill=&quot;#15803d&quot;&gt;③ 완화&lt;/text&gt;
        &lt;text x=&quot;471&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#5aa17a&quot;&gt;upload_progress Off&lt;/text&gt;
        &lt;rect x=&quot;586&quot; y=&quot;40&quot; width=&quot;158&quot; height=&quot;46&quot; rx=&quot;10&quot; fill=&quot;#f5f3ff&quot; stroke=&quot;#ddd6fe&quot;/&gt;
        &lt;text x=&quot;665&quot; y=&quot;60&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;700&quot; fill=&quot;#6d28d9&quot;&gt;④ 패치(근본)&lt;/text&gt;
        &lt;text x=&quot;665&quot; y=&quot;76&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#9178c7&quot;&gt;버전 업 + 재시작&lt;/text&gt;
        &lt;line x1=&quot;166&quot; y1=&quot;63&quot; x2=&quot;202&quot; y2=&quot;63&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.5&quot; marker-end=&quot;url(#ah19)&quot;/&gt;
        &lt;line x1=&quot;356&quot; y1=&quot;63&quot; x2=&quot;392&quot; y2=&quot;63&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.5&quot; marker-end=&quot;url(#ah19)&quot;/&gt;
        &lt;line x1=&quot;546&quot; y1=&quot;63&quot; x2=&quot;582&quot; y2=&quot;63&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.5&quot; marker-end=&quot;url(#ah19)&quot;/&gt;
      &lt;/svg&gt;
    &lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#8b95a1;margin-top:10px;text-align:center;&quot;&gt;그림 1. 영향도 확인 → 경로·서비스 확인 → 완화(설정) → 패치(근본). 완화는 시간 벌기, 해결은 패치.&lt;/div&gt;
  &lt;/div&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;1. 공식 대응책 — 패치가 근본, 설정은 완화&lt;/h2&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;조치&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;대상&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;성격&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;PHP 패치 버전으로 업데이트&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;둘 다&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#6d28d9;font-weight:700;&quot;&gt;근본 해결&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;session.upload_progress.enabled=Off&lt;/code&gt; + 재시작&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;109619&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;임시 완화&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;post_max_size&lt;/code&gt; 축소 등&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;109618&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;임시 완화&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;패치 포함 버전: &lt;strong&gt;8.5.12 / 8.4.27 / 8.3.36 / 8.2.35&lt;/strong&gt;. (그 이하 EOL 버전은 공식 패치 없음) PHP 관리팀도 못 박았다 — &lt;strong&gt;설정 변경만으로는 일부 공격 경로만 제한&lt;/strong&gt;할 뿐 전체 안전을 보장하지 못한다. &quot;완화책 = 시간 벌기, 해결 = 패치&quot;다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;2. 영향도부터 — 끄면 뭐가 깨지나&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;설정을 끄기 전에 &lt;strong&gt;우리 코드가 그 기능에 의존하는지&lt;/strong&gt;부터 확인했다.&lt;/p&gt;
  &lt;ul style=&quot;margin:0 0 20px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;코드가 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;session.upload_progress&lt;/code&gt;를 쓰는가?&lt;/strong&gt; → 전 프로젝트에서 참조 0건. 꺼도 코드가 안 깨진다.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;파일 업로드(multipart) 엔드포인트가 있는가?&lt;/strong&gt; → 게임 클라이언트 API는 업로드 없음(공격자가 보낼 대상이 없음). 업로드는 사내 전용 어드민에만 있고 외부 비공개.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;결론: 외부 공개 API는 공격 표면이 사실상 없고, 업로드받는 어드민은 사내 전용이라 외부 요청 자체가 안 온다. 어느 쪽도 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;session.upload_progress&lt;/code&gt;에 의존하지 않으니 꺼도 서비스 영향 없음 → 권고대로 비활성화.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;3. 헷갈리기 쉬운 지점 — upload_progress는 &quot;업로드 기능&quot;이 아니다&lt;/h2&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;설정&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;역할&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;끄면?&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;file_uploads&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;파일 업로드 허용&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#b91c1c;&quot;&gt;끄면 업로드 자체 안 됨&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;session.upload_progress.enabled&lt;/code&gt;&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;업로드 진행률을 세션에 기록&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#15803d;&quot;&gt;업로드 정상, 진행률 바만 사라짐&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;session.upload_progress&lt;/code&gt;는 &lt;strong&gt;PHP 기본값이 On&lt;/strong&gt;이다(&quot;켠 적 없어도&quot; 대부분 켜져 있음). 파일 업로드 기능과는 무관하고, 업로드 중 진행률을 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;$_SESSION&lt;/code&gt;에 기록해 진행률 바를 그리게 하는 부가 기능일 뿐이다. 그래서 Off 해도 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;$_FILES&lt;/code&gt;·&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;move_uploaded_file()&lt;/code&gt; 같은 업로드 처리는 정상 동작한다.&lt;/p&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;4. 진짜 어려웠던 것 — 서버마다 환경이 다르다&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;작업 대상 서버들이 &lt;strong&gt;php.ini 경로·FPM 서비스명·PHP 버전이 제각각&lt;/strong&gt;이었다. 추측하면 반드시 틀린다. 각 서버에서 먼저 확인하고 들어가야 한다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;# 실제 로드되는 php.ini 경로
php --ini | grep &quot;Loaded Configuration&quot;
# 멀티 PHP(Remi)면 버전 바이너리로
/opt/remi/phpXX/root/usr/bin/php --ini | grep &quot;Loaded Configuration&quot;

# FPM 서비스 실제 이름
systemctl list-units --type=service | grep -Ei 'fpm'

# PHP가 어떻게 도는지(fpm / mod_php / cgi)
ps aux | grep -E 'php-fpm|httpd|apache|nginx' | grep -v grep&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 14px;&quot;&gt;실제로 겪은 함정 네 가지:&lt;/p&gt;
  &lt;div style=&quot;background:#fff8f3;border-left:3px solid #e8833a;border-radius:0 6px 6px 0;padding:16px 20px;margin:0 0 20px;color:#8a5420;font-size:15.5px;&quot;&gt;
    &lt;strong&gt;① 엉뚱한 php.ini를 고침&lt;/strong&gt; — &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;/etc/php.ini&lt;/code&gt;를 고쳤는데 Remi PHP라 실제 로드 파일은 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;/etc/opt/remi/phpXX/php.ini&lt;/code&gt;. 파일은 바뀌었지만 런타임은 그대로 On.&lt;br&gt;&lt;br&gt;
    &lt;strong&gt;② FPM 서비스명이 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;php-fpm&lt;/code&gt;이 아님&lt;/strong&gt; — &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;Unit not found&lt;/code&gt;. 실제로는 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;php82-php-fpm&lt;/code&gt;, &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;php72-php-fpm&lt;/code&gt;처럼 버전이 붙음.&lt;br&gt;&lt;br&gt;
    &lt;strong&gt;③ &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;php&lt;/code&gt; 명령이 PATH에 없음&lt;/strong&gt; — 멀티 PHP라 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;php82&lt;/code&gt;/전체 경로로 호출해야 함.&lt;br&gt;&lt;br&gt;
    &lt;strong&gt;④ CLI와 FPM의 ini가 분리&lt;/strong&gt; — &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;php -i&lt;/code&gt;가 On이어도 FPM은 Off일 수 있음(반대도). 웹이 실제 쓰는 값은 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 5px;font-size:13px;&quot;&gt;phpinfo()&lt;/code&gt;나 FPM 바이너리로 확인.
  &lt;/div&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;5. 적용 절차 — reload vs restart&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;session.upload_progress&lt;/code&gt;는 php.ini 값이라 &lt;strong&gt;FPM이 설정만 다시 읽으면(reload) 반영&lt;/strong&gt;된다(무중단). 하지만 &lt;strong&gt;버전 업데이트(바이너리 교체)는 restart/재배포&lt;/strong&gt;가 필요하다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;# 0) 경로·서비스·현재값 확인
php --ini | grep &quot;Loaded Configuration&quot;
systemctl list-units --type=service | grep -Ei 'fpm'
php -i | grep upload_progress.enabled        # 멀티PHP면 phpXX -i ...

# 1) 백업
sudo cp -a &amp;lt;ini경로&amp;gt; &amp;lt;ini경로&amp;gt;.bak.$(date +%Y%m%d)

# 2) On/주석 어느 쪽이든 Off로 치환 (그 한 줄만)
sudo sed -ri 's/^[[:space:]]*;?[[:space:]]*(session\.upload_progress\.enabled)[[:space:]]*=.*/\1 = Off/' &amp;lt;ini경로&amp;gt;

# 3) 파일 반영 확인
grep -n 'session.upload_progress.enabled' &amp;lt;ini경로&amp;gt;

# 4) 반영 (무중단 reload, 안 바뀌면 restart)
sudo systemctl reload &amp;lt;fpm-서비스명&amp;gt;

# 5) 런타임 최종값 확인 — Off =&gt; Off 면 완료
php -i | grep upload_progress.enabled&lt;/code&gt;&lt;/pre&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;strong&gt;drop-in 대안&lt;/strong&gt; — php.ini 본문을 안 건드리고 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;/etc/php.d/99-disable-upload-progress.ini&lt;/code&gt;에 한 줄만 넣는 방법. 패키지 업데이트에도 보존되고 원복이 &quot;파일 삭제&quot;라 깔끔하다. 런타임 결과는 동일(나중에 로드되는 쪽이 이김).
  &lt;/div&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;6. 109618(요청 크기) 완화 — 서비스 특성을 보고&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;upload_progress&lt;/code&gt; Off는 &lt;strong&gt;109619만&lt;/strong&gt; 막는다. 109618은 요청 크기 완화(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;post_max_size&lt;/code&gt; 축소) 또는 패치로 따로 대응해야 반쪽이 안 된다. 다만 서비스 특성을 봐야 한다.&lt;/p&gt;
  &lt;ul style=&quot;margin:0 0 20px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;어드민은 업로드가 실제 기능&lt;/strong&gt;이라 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;post_max_size&lt;/code&gt;를 함부로 줄이면 업로드가 깨진다. 규칙은 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;post_max_size &amp;gt;= upload_max_filesize&lt;/code&gt;. 이미 작게 잡혀 있으면 유지하는 게 안전.&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;게임 API 서버&lt;/strong&gt;는 큰 POST(배틀 결과, 직렬화 페이로드)가 있어, 줄이면 API가 깨질 수 있다 → 실제 최대 요청 크기 확인 후에만 조정.&lt;/li&gt;
    &lt;li&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;memory_limit=-1&lt;/code&gt;(무제한)은 공식 완화책이지만 &lt;strong&gt;OOM 위험&lt;/strong&gt;이 있어 운영/어드민엔 비권장. 임시·모니터링 동반 시에만.&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;7. 서버별로 갈린 결말 — 패치 가능 / EOL&lt;/h2&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;서버(역할)&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;PHP&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:center;color:#191f28;&quot;&gt;완화&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;패치&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;운영 API 서버 A&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;8.2.x&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;text-align:center;color:#15803d;&quot;&gt;✅&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;8.2.35+로 패치 가능&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;개발 서버&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;8.2.x&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;text-align:center;color:#15803d;&quot;&gt;✅&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;패치 가능(먼저 검증)&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;운영 API 서버 B(해외)&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;7.2&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;text-align:center;color:#15803d;&quot;&gt;✅&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#b91c1c;&quot;&gt;EOL — 공식 패치 없음&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;운영 어드민(해외)&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;7.2&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;text-align:center;color:#15803d;&quot;&gt;✅&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#b91c1c;&quot;&gt;EOL — 공식 패치 없음&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;
  &lt;ul style=&quot;margin:0 0 20px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;8.2 서버&lt;/strong&gt;: &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;8.2.20 → 8.2.35&lt;/code&gt;는 같은 마이너 안의 패치 업이라 호환성 깨짐(BC break) 걱정이 없다. 개발 서버에서 먼저 검증 후 운영 적용 → 두 CVE 근본 해결.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;7.2(EOL) 서버&lt;/strong&gt;: 공식 CVE 패치가 없다. 완화책 + &lt;strong&gt;접근 통제(보안그룹/WAF)&lt;/strong&gt;가 실질 방어. 중기적으로 버전 업 필수. &quot;EOL 버전을 끌고 있으면 긴급 상황에서 선택지가 없다&quot;를 다시 확인했다.&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;마치며&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;보안 공지는 &quot;최신 버전으로 패치하세요&quot; 한 줄이지만, 운영에서 시간을 잡아먹은 건 &lt;strong&gt;환경의 제각각임&lt;/strong&gt;이었다. php.ini 경로가 서버마다 다르고(Remi면 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/etc/opt/remi/phpXX/&lt;/code&gt;), FPM 서비스명에 버전이 붙고(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;php82-php-fpm&lt;/code&gt;), CLI와 FPM이 서로 다른 ini를 보기도 한다. &lt;strong&gt;추측하지 말고 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;php --ini&lt;/code&gt;·&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;systemctl list-units&lt;/code&gt;로 매번 확인하고 들어가는 것&lt;/strong&gt;이 이번 대응의 핵심 교훈이었다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;그리고 둘을 다시 새겼다. &lt;strong&gt;완화책은 시간 벌기, 해결은 패치&lt;/strong&gt;(설정만 끄고 끝냈다 생각하면 109618과 EOL 서버가 그대로 남는다). 그리고 &lt;strong&gt;EOL 버전을 끌고 있으면 긴급 상황에서 선택지가 없다&lt;/strong&gt;. 평소에 &quot;우리가 PHP를 어디서 몇 버전으로 쓰는지&quot; 목록을 유지하고, EOL 전에 올려두는 게 결국 가장 싼 보험이다.&lt;/p&gt;
  &lt;div style=&quot;background:#fff8f3;border-left:3px solid #e8833a;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#8a5420;font-size:15px;&quot;&gt;
    참고: 이 글은 공식 보안 공지의 대응 수칙을 운영 관점에서 정리한 것이다. 실제 적용 시 서비스 호환성·안정성을 사전에 테스트하고, 최신 공식 공지의 안전 버전·권고를 따른다.
  &lt;/div&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:36px 0 20px;&quot;&gt;&lt;/div&gt;
  &lt;div style=&quot;background:#f8f9fa;border:1px solid #eceef1;border-radius:12px;padding:20px;text-align:center;margin-bottom:12px;&quot;&gt;
    &lt;div style=&quot;font-weight:700;color:#191f28;font-size:15px;&quot;&gt;서버 보안 · PHP 운영 · 긴급 패치 대응 문의&lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#6b7684;margin-top:6px;&quot;&gt;댓글로 편하게 남겨주세요.&lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;</description>
      <category>클라우드 운영</category>
      <category>CVE</category>
      <category>php</category>
      <category>php-fpm</category>
      <category>php.ini</category>
      <category>remi</category>
      <category>session.upload_progress</category>
      <category>게임서버</category>
      <category>보안취약점</category>
      <category>보안패치</category>
      <category>서버보안</category>
      <author>oldserver</author>
      <guid isPermaLink="true">https://oldserver.tistory.com/19</guid>
      <comments>https://oldserver.tistory.com/19#entry19comment</comments>
      <pubDate>Sat, 24 Oct 2026 16:51:14 +0900</pubDate>
    </item>
    <item>
      <title>Unity 6 + AI(MCP)로 모바일 슈팅 게임 만들기 &amp;mdash; 콤보&amp;middot;보스&amp;middot;오브젝트 풀링</title>
      <link>https://oldserver.tistory.com/17</link>
      <description>&lt;div style=&quot;max-width: 720px; margin: 0 auto; padding: 0 8px; color: #2b2f36; line-height: 2.0; font-size: 17px; word-break: keep-all; font-family: -apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot;&gt;&lt;!-- 카테고리 + 제목 --&gt;
&lt;div style=&quot;font-size: 13px; font-weight: bold; color: #3182f6; letter-spacing: .5px; margin: 18px 0 14px;&quot;&gt;GAMEDEV &amp;middot; Unity + AI&lt;/div&gt;
&lt;div style=&quot;font-size: 32px; font-weight: 800; line-height: 1.4; color: #191f28; letter-spacing: -.3px;&quot;&gt;Unity 6 + AI(MCP)로 슈팅 게임 한 편 만들기&lt;/div&gt;
&lt;div style=&quot;font-size: 17px; color: #6b7684; margin: 16px 0 8px; line-height: 1.8;&quot;&gt;콤보 배율, 봄, 단계별 보스, 오브젝트 풀링까지. AI에게 맡길 것과 사람이 잡아야 할 것의 선.&lt;/div&gt;
&lt;div style=&quot;height: 1px; background: #eceef1; margin: 28px 0;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;!-- 도입 --&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;15일차에서 Unity 6 + AI(MCP) 환경을 붙이고 APK까지 뽑았다. 이번 16일차는 그 환경으로 &lt;b&gt;실제로 게임 한 편을 만든&lt;/b&gt; 이야기다. 세로형 슈팅 게임인데, 단순 &quot;적 쏘기&quot;에서 멈추지 않고 &lt;b&gt;콤보 배율, 봄(긴급회피), 단계별 보스, 아이템, 오브젝트 풀링&lt;/b&gt;까지 넣었다. 상당 부분을 AI 어시스턴트와 대화하며 만들었고, 그 과정에서 &quot;AI에게 맡길 것 / 사람이 잡아야 할 것&quot;의 선이 꽤 분명해졌다.&lt;/p&gt;
&lt;!-- 게임 소개 + 스크린샷 자리 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;완성한 게임 &amp;mdash; 세로형 슈팅&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;플레이어 기체가 아래에서 위로 적을 쏘는 고전적인 세로 슈팅이다. 여기에 핵심 재미 요소를 몇 개 얹었다.&lt;/p&gt;
&lt;!-- 게임 스크린샷: 티스토리 에디터에서 이 자리에 플레이 화면 이미지를 업로드하세요 --&gt;
&lt;div style=&quot;text-align: center; margin: 24px 0;&quot;&gt;
&lt;div style=&quot;font-size: 14px; color: #8b95a1; margin-top: 10px;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;16_2.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/caDDvh/dJMcah7tdSo/8J9sDHPxuZWF8jDRloPRl0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/caDDvh/dJMcah7tdSo/8J9sDHPxuZWF8jDRloPRl0/img.jpg&quot; data-alt=&quot;플레이 화면 1&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/caDDvh/dJMcah7tdSo/8J9sDHPxuZWF8jDRloPRl0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcaDDvh%2FdJMcah7tdSo%2F8J9sDHPxuZWF8jDRloPRl0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; alt=&quot;플레이 화면&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2340&quot; data-filename=&quot;16_2.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;플레이 화면 1&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;16_1.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cPoCUs/dJMcadRke7A/EH70NL7UXKOUks5CWKhHV0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cPoCUs/dJMcadRke7A/EH70NL7UXKOUks5CWKhHV0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cPoCUs/dJMcadRke7A/EH70NL7UXKOUks5CWKhHV0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcPoCUs%2FdJMcadRke7A%2FEH70NL7UXKOUks5CWKhHV0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2340&quot; data-filename=&quot;16_1.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2340&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
플레이 화면 보스&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;margin: 0 0 20px; padding-left: 20px; color: #2b2f36;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;&lt;b&gt;콤보 배율&lt;/b&gt; &amp;mdash; 적을 연속으로 잡으면 콤보가 쌓이고, 일정 콤보마다 점수 배율이 &amp;times;2 &amp;rarr; &amp;times;3 &amp;rarr; &amp;times;4 &amp;rarr; &amp;times;5로 오른다. 약 2.2초 안에 다음 적을 못 잡으면 콤보가 끊긴다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;&lt;b&gt;봄(긴급회피)&lt;/b&gt; &amp;mdash; 위기 때 화면의 적 탄을 전부 지우고 모든 적에게 큰 데미지. 개수 제한이 있어 아껴 써야 한다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;&lt;b&gt;단계별 보스&lt;/b&gt; &amp;mdash; 일정 점수마다 경고(WARNING) 후 보스 등장. 격파하면 다음 보스는 더 강해진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아이템 / 베스트 스코어&lt;/b&gt; &amp;mdash; 파워업&amp;middot;봄 아이템, 최고 점수는 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;PlayerPrefs&lt;/code&gt;로 저장돼 다음 판에도 남는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- 구조 SVG --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;코드 구조 &amp;mdash; 역할별로 쪼갠 스크립트&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;한 파일에 다 넣지 않고 역할별로 나눴다. AI와 작업할 때 이 분리가 특히 중요했다 &amp;mdash; &quot;GameController만 보고 콤보 로직 고쳐줘&quot;처럼 &lt;b&gt;범위를 좁혀 지시&lt;/b&gt;할 수 있기 때문이다. 중심은 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;GameController&lt;/code&gt;이고, 다른 스크립트가 static 메서드로 상태를 보고한다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0;&quot;&gt;
&lt;div style=&quot;border: 1px solid #eceef1; border-radius: 12px; background: #fbfcfd; padding: 14px;&quot;&gt;&lt;!-- 중심: GameController --&gt; GameController 점수&amp;middot;콤보&amp;middot;봄&amp;middot;보스 상태 &lt;!-- 주변 모듈 (6개) --&gt; &lt;!-- 상단: Player --&gt; PlayerController 이동&amp;middot;발사 (New Input) &lt;!-- 좌상: Enemy --&gt; Enemy / Spawner 적 생성&amp;middot;이동 &lt;!-- 우상: Boss --&gt; Boss / EnemyBullet 단계별 보스&amp;middot;탄 &lt;!-- 좌하: ObjectPool --&gt; ObjectPool 총알&amp;middot;폭발 재사용 &lt;!-- 우하: 연출 --&gt; 연출 FX Explosion&amp;middot;Shake&amp;middot;Popup &lt;!-- 하단: UI --&gt; UIManager HUD (점수&amp;middot;콤보&amp;middot;목숨) &lt;!-- 연결선 (중심 ↔ 주변) --&gt;&lt;/div&gt;
&lt;div style=&quot;font-size: 14px; color: #8b95a1; margin-top: 10px; text-align: center;&quot;&gt;그림 2. GameController를 중심으로 한 모듈 구조 (총 20여 개 스크립트)&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;작은 게임에선 이렇게 중앙 상태(&lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;GameController&lt;/code&gt;의 static 필드)를 두고 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;GameController.EnemyKilled(pos)&lt;/code&gt; 식으로 보고하는 게 단순하고 빠르다.&lt;/p&gt;
&lt;!-- 핵심 1 콤보 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;핵심 ① 콤보 배율 &amp;mdash; &quot;재미&quot;를 숫자로 설계&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;콤보는 카운트만 올리는 게 아니라 &lt;b&gt;점수 배율로 연결&lt;/b&gt;해야 손맛이 난다. 처치할 때마다 콤보를 올리고, 구간별로 배율을 바꾼다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background: #1e2430; color: #e6edf3; border-radius: 10px; padding: 16px; white-space: pre-wrap; word-break: break-all; font-size: 13.5px; line-height: 1.6; margin: 24px 0;&quot;&gt;&lt;code&gt;void BumpCombo()
{
    Combo++;
    comboUntil = Time.time + comboWindow;   // 2.2초 안에 다음 처치해야 유지
    if (Combo &amp;gt;= 35) Multiplier = 5;
    else if (Combo &amp;gt;= 20) Multiplier = 4;
    else if (Combo &amp;gt;= 10) Multiplier = 3;
    else if (Combo &amp;gt;= 5)  Multiplier = 2;
    else Multiplier = 1;
}

public static void EnemyKilled(Vector3 pos)
{
    if (GameOver || Instance == null) return;
    Instance.BumpCombo();
    int gain = 1 * Multiplier;              // 배율 적용 점수
    Score += gain;
    ScorePopup.Spawn(pos, Multiplier &amp;gt; 1 ? (&quot;+&quot; + gain + &quot; x&quot; + Multiplier) : &quot;+1&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;설계 포인트는 &lt;b&gt;피격 시 콤보 리셋&lt;/b&gt;이다. &quot;안 맞고 연속으로 잡을수록 점수가 폭발한다&quot;는 긴장감이 생긴다. AI에게 &quot;콤보 유지 시간 지나면 리셋, 피격해도 리셋&quot;을 말로 명확히 정의해주니 로직이 정확히 나왔다.&lt;/p&gt;
&lt;!-- 핵심 2 풀링 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;핵심 ② 오브젝트 풀링 &amp;mdash; 모바일 성능의 핵심&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;슈팅 게임은 총알&amp;middot;폭발&amp;middot;적 탄이 초당 수십 개씩 생성&amp;middot;소멸한다. 매번 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;Instantiate&lt;/code&gt;/&lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;Destroy&lt;/code&gt;하면 &lt;b&gt;GC가 몰려 폰에서 끊긴다.&lt;/b&gt; 그래서 미리 만들어두고 재사용하는 오브젝트 풀링이 사실상 필수다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background: #1e2430; color: #e6edf3; border-radius: 10px; padding: 16px; white-space: pre-wrap; word-break: break-all; font-size: 13.5px; line-height: 1.6; margin: 24px 0;&quot;&gt;&lt;code&gt;// 봄 발동 시: 적 탄을 &quot;제거&quot;가 아니라 &quot;풀로 반환&quot;
foreach (var eb in Object.FindObjectsByType&amp;lt;EnemyBullet&amp;gt;(FindObjectsSortMode.None))
    ObjectPool.ReturnObj(eb.gameObject);
// 폭발도 Destroy 대신 풀에서 꺼내 쓴다
ObjectPool.SpawnObj(explosionPrefab, pos, Quaternion.identity);&lt;/code&gt;&lt;/pre&gt;
&lt;div style=&quot;background: #f2f4f6; border-left: 3px solid #3182f6; border-radius: 0 6px 6px 0; padding: 16px 20px; margin: 24px 0; color: #4e5968; font-size: 16px;&quot;&gt;AI는 풀링 패턴 자체는 잘 짜준다. 다만 &lt;b&gt;&quot;이건 풀링으로, 저건 그냥 Destroy로&quot;의 경계&lt;/b&gt;는 사람이 정해줘야 했다. 자주 생성되는 것(총알&amp;middot;폭발&amp;middot;적 탄)만 풀링하고, 가끔 나오는 보스&amp;middot;아이템은 일반 생성으로 둔 게 적절한 균형이었다.&lt;/div&gt;
&lt;!-- 핵심 3 입력 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;핵심 ③ 입력 &amp;mdash; 키보드 + 터치 동시 지원&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;15일차에서 데인 New Input System을 처음부터 적용했다. 그리고 &lt;b&gt;키보드와 터치를 둘 다&lt;/b&gt; 받게 했다 &amp;mdash; 에디터에선 키보드로 빠르게 테스트, 폰에선 터치로 플레이.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot; style=&quot;background: #1e2430; color: #e6edf3; border-radius: 10px; padding: 16px; white-space: pre-wrap; word-break: break-all; font-size: 13.5px; line-height: 1.6; margin: 24px 0;&quot;&gt;&lt;code&gt;// 봄: B키(데스크톱) 또는 두 손가락 터치(모바일)
bool bombKey = Keyboard.current != null &amp;amp;&amp;amp; Keyboard.current.bKey.wasPressedThisFrame;
bool bombTouch = Touchscreen.current != null &amp;amp;&amp;amp; Touchscreen.current.touches.Count &amp;gt; 1
                 &amp;amp;&amp;amp; Touchscreen.current.touches[1].press.wasPressedThisFrame;
if (!GameOver &amp;amp;&amp;amp; (bombKey || bombTouch)) UseBomb();&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;Keyboard.current&lt;/code&gt;&amp;middot;&lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;Touchscreen.current&lt;/code&gt;가 null일 수 있으니 &lt;b&gt;항상 null 체크&lt;/b&gt;를 넣는다. 그 입력 장치가 없는 환경에서 예외를 막기 위해서다. 이 체크를 빼먹으면 폰에서 키보드 참조로 죽거나 그 반대가 된다.&lt;/p&gt;
&lt;!-- AI 분담 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;AI에게 뭘 맡기고, 뭘 직접 할까&lt;/h2&gt;
&lt;div style=&quot;display: flex; flex-wrap: wrap; gap: 14px; margin: 20px 0;&quot;&gt;
&lt;div style=&quot;flex: 1 1 300px; background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 12px; padding: 18px;&quot;&gt;
&lt;div style=&quot;font-weight: 800; color: #15803d; margin-bottom: 8px;&quot;&gt;AI가 잘하는 것&lt;/div&gt;
&lt;ul style=&quot;margin: 0; padding-left: 18px; font-size: 15px; color: #334155; line-height: 1.9;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패턴이 분명한 코드 &amp;mdash; 풀링, 콤보 구간 분기, static 상태&lt;/li&gt;
&lt;li&gt;반복 작업 &amp;mdash; 비슷한 적/탄 스크립트 여러 개&lt;/li&gt;
&lt;li&gt;&quot;이 함수 이렇게 바꿔줘&quot; 식의 국소 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div style=&quot;flex: 1 1 300px; background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 12px; padding: 18px;&quot;&gt;
&lt;div style=&quot;font-weight: 800; color: #1d4ed8; margin-bottom: 8px;&quot;&gt;사람이 잡아야 할 것&lt;/div&gt;
&lt;ul style=&quot;margin: 0; padding-left: 18px; font-size: 15px; color: #334155; line-height: 1.9;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재미 설계 &amp;mdash; 콤보 유지 2.2초, 배율 구간(5/10/20/35)&lt;/li&gt;
&lt;li&gt;경계 결정 &amp;mdash; 뭘 풀링하고 뭘 static으로 둘지&lt;/li&gt;
&lt;li&gt;최종 빌드&amp;middot;실기 테스트 (폰 플레이 느낌)&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 AI는 &lt;b&gt;&quot;정해진 규칙을 코드로 옮기는 속도&quot;&lt;/b&gt;를 압도적으로 높여줬고, 사람은 &lt;b&gt;&quot;무엇을 만들지, 어떤 느낌일지&quot;&lt;/b&gt;를 정하는 데 집중했다. 콤보 배율 하나도 &quot;35콤보에 &amp;times;5&quot;라는 규칙만 주면 코드는 순식간에 나온다 &amp;mdash; 그 규칙(=재미)을 정하는 게 사람 몫이었다.&lt;/p&gt;
&lt;!-- 마치며 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;AI(MCP)로 게임을 만든다고 하면 &quot;AI가 다 만들어주겠네&quot;로 들리지만, 실제로는 &lt;b&gt;&quot;사람이 재미를 설계하고, AI가 그걸 빠르게 코드로 구현&quot;&lt;/b&gt;하는 협업에 가깝다. 콤보&amp;middot;봄&amp;middot;보스&amp;middot;풀링을 하루 만에 붙일 수 있었던 건 AI 덕이지만, &quot;그 콤보가 2.2초인지 3초인지&quot;는 결국 플레이해보며 사람이 판단했다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;15일차(환경&amp;middot;APK)와 16일차(게임 제작)를 묶으면, Unity 6 + AI로 &lt;b&gt;아이디어에서 폰에 설치되는 게임까지&lt;/b&gt; 가는 한 사이클이 된다. 그 사이클이 생각보다 짧아졌다는 게 가장 큰 소득이었다.&lt;/p&gt;
&lt;div style=&quot;height: 1px; background: #eceef1; margin: 36px 0 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;!-- CTA --&gt;
&lt;div style=&quot;background: #f8f9fa; border: 1px solid #eceef1; border-radius: 12px; padding: 20px; text-align: center; margin-bottom: 12px;&quot;&gt;
&lt;div style=&quot;font-weight: bold; color: #191f28; font-size: 15px;&quot;&gt;Unity &amp;middot; AI 게임 개발(MCP) &amp;middot; 모바일 게임 문의&lt;/div&gt;
&lt;div style=&quot;font-size: 14px; color: #6b7684; margin-top: 6px;&quot;&gt;댓글로 편하게 남겨주세요.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>AI  개발</category>
      <category>ai게임개발</category>
      <category>c#</category>
      <category>MCP</category>
      <category>NewInputSystem</category>
      <category>unity6</category>
      <category>게임개발</category>
      <category>모바일게임</category>
      <category>슈팅게임</category>
      <category>오브젝트풀링</category>
      <category>유니티</category>
      <author>oldserver</author>
      <guid isPermaLink="true">https://oldserver.tistory.com/17</guid>
      <comments>https://oldserver.tistory.com/17#entry17comment</comments>
      <pubDate>Wed, 21 Oct 2026 10:16:37 +0900</pubDate>
    </item>
    <item>
      <title>Unity 6 공식 MCP로 AI(Kiro) 연동하고 Android APK까지 빌드하기</title>
      <link>https://oldserver.tistory.com/16</link>
      <description>&lt;div style=&quot;max-width: 720px; margin: 0 auto; padding: 0 8px; color: #2b2f36; line-height: 2.0; font-size: 17px; word-break: keep-all; font-family: -apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot;&gt;&lt;!-- 카테고리 + 제목 --&gt;
&lt;div style=&quot;font-size: 13px; font-weight: bold; color: #3182f6; letter-spacing: .5px; margin: 18px 0 14px;&quot;&gt;GAMEDEV &amp;middot; Unity + AI&lt;/div&gt;
&lt;div style=&quot;font-size: 32px; font-weight: 800; line-height: 1.4; color: #191f28; letter-spacing: -.3px;&quot;&gt;Unity 6 + AI(MCP)로 Android APK까지 빌드하기&lt;/div&gt;
&lt;div style=&quot;font-size: 17px; color: #6b7684; margin: 16px 0 8px; line-height: 1.8;&quot;&gt;씬 조작&amp;middot;스크립트는 AI에게, 최종 빌드는 사람이. 중간에 반드시 걸리는 함정 세 개를 넘은 기록.&lt;/div&gt;
&lt;div style=&quot;height: 1px; background: #eceef1; margin: 28px 0;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;!-- 도입 --&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;오늘 Unity 6으로 간단한 모바일 게임을 만들어 &lt;b&gt;Android APK까지 뽑았다.&lt;/b&gt; 특이한 건 씬 조작과 스크립트 작성을 상당 부분 &lt;b&gt;AI 어시스턴트(Kiro)에게 시켰다&lt;/b&gt;는 점이다. Unity 6에는 &lt;b&gt;공식 MCP(Model Context Protocol) Server&lt;/b&gt; 기능이 들어 있어서, 서드파티 플러그인 없이 AI를 Unity 에디터에 바로 붙일 수 있다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;이 글은 그 연동 설치부터 APK 빌드까지 전 과정이다. 중간에 반드시 걸리는 함정 세 개 &amp;mdash; &lt;b&gt;에디터 버전(6000.3 미만이면 메뉴가 안 보임)&lt;/b&gt;, &lt;b&gt;New Input System&lt;/b&gt;, 그리고 &lt;b&gt;MCP로는 APK 빌드가 끝까지 안 되는 문제&lt;/b&gt; &amp;mdash; 를 어떻게 넘겼는지까지 정리했다. (로컬 경로는 플레이스홀더다.)&lt;/p&gt;
&lt;!-- 연동 구성도 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;연동 구조&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;먼저 그림부터. Unity 에디터가 &quot;외부에 노출하는 MCP 서버&quot;를 켜면, AI 클라이언트가 relay를 거쳐 거기에 붙는다. 에디터가 켜져 있어야 브리지가 돈다.&lt;/p&gt;
&lt;div style=&quot;margin: 24px 0;&quot;&gt;
&lt;div style=&quot;border: 1px solid #eceef1; border-radius: 12px; background: #fbfcfd; padding: 14px;&quot;&gt;&lt;!-- AI 클라이언트 --&gt; AI 클라이언트 mcp.json 설정 &lt;!-- relay --&gt; relay .unity/relay/ &lt;!-- Unity MCP 브리지 (에디터 내부, 점선 그룹) --&gt; Unity 에디터 (켜져 있어야 함) Unity MCP Bridge (Start) 콘솔&amp;middot;스크립트&amp;middot;씬 도구 &lt;!-- 화살표 --&gt;&lt;/div&gt;
&lt;div style=&quot;font-size: 14px; color: #8b95a1; margin-top: 10px; text-align: center;&quot;&gt;그림 1. AI 클라이언트 &amp;rarr; relay &amp;rarr; Unity 에디터의 MCP 브리지. 브리지는 에디터 안에서 돈다.&lt;/div&gt;
&lt;/div&gt;
&lt;!-- 왜 공식 MCP --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;왜 Unity 공식 MCP인가&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;AI를 Unity에 붙이는 서드파티 방법은 예전부터 있었다(Python&amp;middot;uv 설치, 별도 브리지 등). 그런데 Unity 6부터는 &lt;b&gt;공식 Assistant 패키지에 MCP Server가 내장&lt;/b&gt;됐다. 외부 런타임을 안 깔아도 되고, Unity가 직접 &quot;에디터를 노출하는 MCP 서버&quot;를 켜면 AI가 콘솔 읽기&amp;middot;C# 실행&amp;middot;씬 캡처 같은 도구를 바로 쓴다.&lt;/p&gt;
&lt;div style=&quot;background: #f2f4f6; border-left: 3px solid #3182f6; border-radius: 0 6px 6px 0; padding: 16px 20px; margin: 24px 0; color: #4e5968; font-size: 16px;&quot;&gt;단, &lt;b&gt;에디터가 켜져 있어야&lt;/b&gt; 동작한다. 브리지가 에디터 안에서 돌기 때문에, 에디터를 닫으면 AI가 연결돼 보여도 명령이 안 먹는다.&lt;/div&gt;
&lt;!-- 함정 1 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;함정 ① 에디터 버전 &amp;mdash; 6000.3 미만이면 메뉴가 없다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;제일 먼저 걸리는 지점이다. 공식 MCP 기능은 &lt;b&gt;Unity 6.3(에디터 버전 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;6000.3&lt;/code&gt;) 이상&lt;/b&gt;에서만 나온다.&lt;/p&gt;
&lt;ul style=&quot;margin: 0 0 20px; padding-left: 20px; color: #2b2f36;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;그냥 &quot;Unity 6&quot; = &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;6000.0&lt;/code&gt;을 깔면 &lt;b&gt;MCP 메뉴 자체가 안 보인다.&lt;/b&gt; 버전 선택 때 반드시 6000.3 이상을 고른다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;&lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;6000.3&lt;/code&gt;(6.3 LTS), &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;6000.6&lt;/code&gt;(6.6) 등 6.3 이상이면 다 된다. 안정성을 원하면 LTS인 6.3이 낫다.&lt;/li&gt;
&lt;li&gt;아직 Pre-release라 Package Manager에 Assistant가 안 보이면, &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;Project Settings &amp;rarr; Package Manager&lt;/code&gt;에서 &lt;b&gt;Show Pre-Release Package Versions&lt;/b&gt;를 켠다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background: #f2f4f6; border-left: 3px solid #3182f6; border-radius: 0 6px 6px 0; padding: 16px 20px; margin: 24px 0; color: #4e5968; font-size: 16px;&quot;&gt;Unity 6은 버전이 &lt;code style=&quot;background: #fff; border-radius: 4px; padding: 1px 6px; font-size: 14px;&quot;&gt;6000.x&lt;/code&gt;로 표기된다. &quot;6&quot;이 아니라 &quot;6000&quot;으로 보이는 게 정상이다. 이미 6000.0을 깔았어도 지울 필요 없이, Hub에서 6000.3 이상을 추가 설치하면 된다.&lt;/div&gt;
&lt;!-- 설치 흐름 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;설치 흐름 &amp;mdash; Hub &amp;rarr; 에디터 &amp;rarr; Assistant &amp;rarr; MCP&lt;/h2&gt;
&lt;div style=&quot;display: flex; flex-direction: column; gap: 8px; margin: 20px 0;&quot;&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start;&quot;&gt;&lt;span style=&quot;flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%; background: #3182f6; color: #fff; font-size: 12px; font-weight: bold; display: inline-flex; align-items: center; justify-content: center;&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color: #2b2f36;&quot;&gt;Unity Hub 설치 후 로그인 (개인/소규모는 Personal 무료)&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start;&quot;&gt;&lt;span style=&quot;flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%; background: #3182f6; color: #fff; font-size: 12px; font-weight: bold; display: inline-flex; align-items: center; justify-content: center;&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color: #2b2f36;&quot;&gt;Hub &amp;rarr; Installs &amp;rarr; &lt;b&gt;Unity 6.3 LTS&lt;/b&gt; 설치. 모바일이면 모듈에서 Android Build Support 체크&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start;&quot;&gt;&lt;span style=&quot;flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%; background: #3182f6; color: #fff; font-size: 12px; font-weight: bold; display: inline-flex; align-items: center; justify-content: center;&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color: #2b2f36;&quot;&gt;프로젝트 생성 (3D 템플릿 무난), 에디터 버전이 6000.3 이상인지 확인&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start;&quot;&gt;&lt;span style=&quot;flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%; background: #3182f6; color: #fff; font-size: 12px; font-weight: bold; display: inline-flex; align-items: center; justify-content: center;&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color: #2b2f36;&quot;&gt;Window &amp;rarr; Package Manager &amp;rarr; Unity Registry &amp;rarr; &lt;b&gt;Assistant 설치&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start;&quot;&gt;&lt;span style=&quot;flex: 0 0 auto; width: 24px; height: 24px; border-radius: 50%; background: #3182f6; color: #fff; font-size: 12px; font-weight: bold; display: inline-flex; align-items: center; justify-content: center;&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color: #2b2f36;&quot;&gt;Project Settings &amp;rarr; AI &amp;rarr; Unity MCP &amp;rarr; Bridge &lt;b&gt;Start&lt;/b&gt; (권한 요청 허용)&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background: #f2f4f6; border-left: 3px solid #3182f6; border-radius: 0 6px 6px 0; padding: 16px 20px; margin: 24px 0; color: #4e5968; font-size: 16px;&quot;&gt;헷갈리는 지점. 같은 AI 메뉴의 &quot;MCP Client &amp;rarr; MCP Servers&quot;는 &quot;Unity가 &lt;b&gt;접속하러 가는&lt;/b&gt; 서버&quot; 설정이다. 지금 쓰는 &lt;b&gt;Unity MCP&lt;/b&gt; 페이지는 &quot;Unity가 &lt;b&gt;외부에 노출하는&lt;/b&gt; 서버&quot;를 켜는 곳이다. 둘은 방향이 반대다.&lt;/div&gt;
&lt;!-- Kiro 연결 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;AI 클라이언트 연결&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;Unity MCP 설정 페이지에서 &lt;b&gt;Integrations &amp;rarr; (클라이언트) &amp;rarr; Configure&lt;/b&gt;를 누르면, 하단에 OS별 설정 파일 경로와 붙여넣을 JSON 블록이 표시된다.&lt;/p&gt;
&lt;pre class=&quot;json&quot; style=&quot;background: #1e2430; color: #e6edf3; border-radius: 10px; padding: 16px; white-space: pre-wrap; word-break: break-all; font-size: 13.5px; line-height: 1.6; margin: 24px 0;&quot;&gt;&lt;code&gt;{
  &quot;mcpServers&quot;: {
    &quot;unity&quot;: {
      &quot;command&quot;: &quot;&amp;lt;Unity 화면에 표시된 relay 실행 경로&amp;gt;&quot;,
      &quot;args&quot;: [&quot;&amp;lt;Unity 화면에 표시된 인자들&amp;gt;&quot;],
      &quot;disabled&quot;: false,
      &quot;autoApprove&quot;: []
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;이 JSON을 AI 클라이언트의 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;mcp.json&lt;/code&gt;에 붙여넣는다. 전역이면 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;C:\Users\&amp;lt;사용자명&amp;gt;\.kiro\settings\mcp.json&lt;/code&gt;, 프로젝트 한정이면 워크스페이스의 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;.kiro/settings/mcp.json&lt;/code&gt;.&lt;/p&gt;
&lt;div style=&quot;background: #f2f4f6; border-left: 3px solid #3182f6; border-radius: 0 6px 6px 0; padding: 16px 20px; margin: 24px 0; color: #4e5968; font-size: 16px;&quot;&gt;&lt;b&gt;경로를 추측하지 않는다.&lt;/b&gt; &lt;code style=&quot;background: #fff; border-radius: 4px; padding: 1px 6px; font-size: 14px;&quot;&gt;command&lt;/code&gt;&amp;middot;&lt;code style=&quot;background: #fff; border-radius: 4px; padding: 1px 6px; font-size: 14px;&quot;&gt;args&lt;/code&gt;&amp;middot;relay 경로는 Unity 화면에 뜬 값을 그대로 복사한다. 버전마다 달라서 손으로 지어내면 relay를 못 찾아 연결이 실패한다. 그리고 &lt;code style=&quot;background: #fff; border-radius: 4px; padding: 1px 6px; font-size: 14px;&quot;&gt;&quot;disabled&quot;: false&lt;/code&gt;인지 꼭 확인한다(일부 안내가 true로 쓰라는데, 실제로 쓰려면 false다).&lt;/div&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;연결되면 AI 쪽 MCP 패널에 &lt;b&gt;unity Connected (N tools)&lt;/b&gt;로 뜬다. 가장 안전한 첫 테스트는 &quot;현재 콘솔 로그 읽어줘&quot; &amp;mdash; 로그가 돌아오면 정상이다. 그다음 &quot;원점에 큐브 만들고 Rigidbody 붙여줘&quot; 같은 씬 조작을 시켜볼 수 있다.&lt;/p&gt;
&lt;!-- 함정 2 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;함정 ② New Input System &amp;mdash; 구형 Input API가 안 먹는다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;Unity 6 신규 프로젝트는 기본이 &lt;b&gt;New Input System 전용&lt;/b&gt;이다. 이 상태에서 구형 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;Input.GetKey()&lt;/code&gt; / &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;Input.GetAxisRaw()&lt;/code&gt;는 동작하지 않는다. AI가 생성한 예제가 구형 입력을 쓰면 이동&amp;middot;발사가 전부 안 먹는 현상이 난다.&lt;/p&gt;
&lt;ul style=&quot;margin: 0 0 20px; padding-left: 20px; color: #2b2f36;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;&lt;b&gt;정석&lt;/b&gt;: 처음부터 New Input System API로 쓴다. &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;Keyboard.current.spaceKey.isPressed&lt;/code&gt; 식, &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;using UnityEngine.InputSystem;&lt;/code&gt; 추가.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;&lt;b&gt;비권장 대안&lt;/b&gt;: Player &amp;rarr; Active Input Handling을 Both로 바꾸는 방법도 있으나, &lt;b&gt;Both로 바꿔도 런타임에서 구형 Input이 안 먹는 경우가 있었다&lt;/b&gt;(재시작 후에도). New API로 바꾸자 즉시 정상. Both에 기대지 말 것.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디버깅 팁&lt;/b&gt;: 입력이 안 먹으면 매 프레임 입력값을 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;Debug.Log&lt;/code&gt;로 찍는 프로브를 붙여 확인. 그리고 키 입력은 &lt;b&gt;Game 뷰에 포커스&lt;/b&gt;(화면 클릭)가 있어야 들어온다.&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- 함정 3 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;함정 ③ MCP로는 APK 빌드가 끝까지 안 된다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;오늘의 핵심 교훈이다. 씬 조작&amp;middot;스크립트 작성은 MCP(AI)로 잘 됐는데, &lt;b&gt;Android APK 빌드만은 MCP로 끝까지 안 됐다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background: #1e2430; color: #e6edf3; border-radius: 10px; padding: 16px; white-space: pre-wrap; word-break: break-all; font-size: 13.5px; line-height: 1.6; margin: 24px 0;&quot;&gt;&lt;code&gt;User interactions are not supported for MCP tool calls&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;BuildPipeline.BuildPlayer&lt;/code&gt;가 내부적으로 &lt;b&gt;상호작용(SDK 라이선스 동의 등)을 요구&lt;/b&gt;하는데, MCP 도구 호출은 그런 대화형 입력을 지원하지 않아 막힌다. 모듈&amp;middot;설정이 전부 정상이어도 마찬가지다.&lt;/p&gt;
&lt;div style=&quot;background: #f2f4f6; border-left: 3px solid #3182f6; border-radius: 0 6px 6px 0; padding: 16px 20px; margin: 24px 0; color: #4e5968; font-size: 16px;&quot;&gt;&lt;b&gt;해결 &amp;mdash; 빌드는 에디터에서 직접 한다.&lt;/b&gt; AI는 준비(스크립트&amp;middot;씬&amp;middot;설정)까지만 맡고, 최종 빌드 버튼은 사람이 누른다.&lt;/div&gt;
&lt;ul style=&quot;margin: 0 0 20px; padding-left: 20px; color: #2b2f36;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;&lt;b&gt;모듈 추가&lt;/b&gt;: Hub &amp;rarr; Installs &amp;rarr; 버전 ⚙ &amp;rarr; Add Modules에서 Android Build Support + OpenJDK + Android SDK &amp;amp; NDK Tools 추가.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;&lt;b&gt;모듈 추가 후 에디터 재시작 필수&lt;/b&gt;: &quot;No Android module loaded. Editor will need to be restarted&quot; 경고가 뜬다. 껐다 켜야 Android 빌드가 활성화된다.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;&lt;b&gt;SDK/NDK/JDK 임베디드&lt;/b&gt;: 경로가 비면 Edit &amp;rarr; Preferences &amp;rarr; External Tools에서 Hub 동봉(embedded)으로 체크.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;빌드 실행&lt;/b&gt;: File &amp;rarr; Build Settings/Build Profiles &amp;rarr; Android 선택 &amp;rarr; Build &amp;rarr; 출력 경로 지정. 처음엔 Gradle 다운로드로 수 분, SDK 라이선스 창이 뜨면 Accept.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 에디터에서 직접 빌드하니 APK가 정상적으로 떨어졌다.&lt;/p&gt;
&lt;!-- 모바일 입력 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;모바일 입력 &amp;mdash; 키보드 스크립트는 폰에서 안 돈다&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;APK를 폰에 올리기 전에 하나 더. 데스크톱에서 키보드로 테스트한 스크립트는 폰에서 그대로는 동작하지 않는다. 터치 입력을 따로 넣어야 한다. &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;Touchscreen.current.primaryTouch&lt;/code&gt;로 터치(드래그 이동 + 자동 발사 등)를 처리하고, 화면 방향은 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;PlayerSettings.defaultInterfaceOrientation&lt;/code&gt;으로 고정한다.&lt;/p&gt;
&lt;!-- Play 모드 함정 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;보너스 &amp;mdash; MCP로 씬 편집할 때 Play 모드 함정&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;MCP로 씬을 편집할 때 &lt;b&gt;Play 모드 중에 가한 변경은 Play를 멈추면 전부 되돌아간다&lt;/b&gt;(오브젝트 생성, 컴포넌트 연결, 스프라이트 교체 등).&lt;/p&gt;
&lt;ul style=&quot;margin: 0 0 20px; padding-left: 20px; color: #2b2f36;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;씬 편집은 반드시 &lt;b&gt;에디트 모드&lt;/b&gt;(&lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;EditorApplication.isPlaying == false&lt;/code&gt;)에서 하고 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;EditorSceneManager.SaveOpenScenes()&lt;/code&gt;로 저장.&lt;/li&gt;
&lt;li style=&quot;margin-bottom: 8px;&quot;&gt;Play 모드에서는 저장 API가 예외나 &quot;User interactions are not supported&quot; 오류를 낸다. MCP 명령 전에 &lt;code style=&quot;background: #f2f4f6; border-radius: 4px; padding: 1px 6px; font-size: 15px;&quot;&gt;isPlaying&lt;/code&gt;을 먼저 로그로 확인하면 헛작업을 막는다.&lt;/li&gt;
&lt;li&gt;스크립트 교체 직후엔 Unity가 재컴파일하느라 MCP 연결이 잠깐 끊긴다(&quot;Unity not detected&quot;). 잠시 후 재시도하면 복구된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- 마치며 --&gt;
&lt;h2 style=&quot;font-size: 24px; font-weight: 800; color: #191f28; margin: 44px 0 14px; letter-spacing: -.3px;&quot; data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;Unity 6의 공식 MCP는 &quot;AI가 에디터를 직접 조작&quot;하는 걸 플러그인 없이 가능하게 해준다. 콘솔 읽기&amp;middot;스크립트 작성&amp;middot;씬 구성까지는 AI에게 맡기고, &lt;b&gt;대화형 승인이 필요한 작업(APK 빌드의 SDK 라이선스 등)은 MCP로는 못 넘어가니 사람이 마무리&lt;/b&gt;한다. 이 경계만 이해하면 &quot;AI가 밑작업, 사람이 최종 빌드&quot;라는 분담이 꽤 자연스럽다.&lt;/p&gt;
&lt;p style=&quot;margin: 0 0 20px;&quot; data-ke-size=&quot;size16&quot;&gt;가장 많이 데인 셋을 다시 정리하면 &amp;mdash; &lt;b&gt;버전은 6000.3 이상&lt;/b&gt;(아니면 MCP 메뉴가 아예 없음), &lt;b&gt;입력은 New Input System으로&lt;/b&gt;(Both는 못 믿음), &lt;b&gt;빌드는 에디터에서 직접&lt;/b&gt;(MCP는 상호작용 못 함). 이 세 가지만 미리 알면 Unity 6 + AI로 APK까지 가는 길이 훨씬 매끄럽다.&lt;/p&gt;
&lt;div style=&quot;height: 1px; background: #eceef1; margin: 36px 0 20px;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;!-- CTA --&gt;
&lt;div style=&quot;background: #f8f9fa; border: 1px solid #eceef1; border-radius: 12px; padding: 20px; text-align: center; margin-bottom: 12px;&quot;&gt;
&lt;div style=&quot;font-weight: bold; color: #191f28; font-size: 15px;&quot;&gt;Unity &amp;middot; AI 코딩(MCP) &amp;middot; 모바일 빌드 문의&lt;/div&gt;
&lt;div style=&quot;font-size: 14px; color: #6b7684; margin-top: 6px;&quot;&gt;댓글로 편하게 남겨주세요.&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>AI  개발</category>
      <category>AI코딩</category>
      <category>android</category>
      <category>apk</category>
      <category>kiro</category>
      <category>MCP</category>
      <category>NewInputSystem</category>
      <category>unity6</category>
      <category>게임개발</category>
      <category>빌드</category>
      <category>유니티</category>
      <author>oldserver</author>
      <guid isPermaLink="true">https://oldserver.tistory.com/16</guid>
      <comments>https://oldserver.tistory.com/16#entry16comment</comments>
      <pubDate>Tue, 20 Oct 2026 10:45:12 +0900</pubDate>
    </item>
    <item>
      <title>AWS 비용 절감 실행: 롤백 가능/불가 구분하고 예약 인스턴스는 맨 마지막에</title>
      <link>https://oldserver.tistory.com/15</link>
      <description>&lt;div style=&quot;max-width:720px;margin:0 auto;padding:0 8px;color:#2b2f36;line-height:2.0;font-size:17px;word-break:keep-all;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot;&gt;

  &lt;!-- 카테고리 + 제목 --&gt;
  &lt;div style=&quot;font-size:13px;font-weight:700;color:#3182f6;letter-spacing:.5px;margin:18px 0 14px;&quot;&gt;INFRA · 비용 최적화&lt;/div&gt;
  &lt;div style=&quot;font-size:32px;font-weight:800;line-height:1.4;color:#191f28;letter-spacing:-.3px;&quot;&gt;AWS 비용 다이어트, 계획을 실제로 적용하기&lt;/div&gt;
  &lt;div style=&quot;font-size:17px;color:#6b7684;margin:16px 0 8px;line-height:1.8;&quot;&gt;Aurora 7대 → 5대. 삭제 순서, 되돌릴 수 있는 것과 없는 것, 그리고 예약 인스턴스를 맨 마지막에 사는 이유.&lt;/div&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:28px 0;&quot;&gt;&lt;/div&gt;

  &lt;!-- 도입 --&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;7일차에서 &quot;AWS 청구서는 큰데 CPU는 놀고 있다&quot;는 데이터를 근거로 다운사이징 계획을 세웠다. 이번 글은 그 &lt;strong&gt;계획을 실제 점검(서비스 차단) 중에 적용한 확정·실행편&lt;/strong&gt;이다. 계획 글은 많지만, 막상 &lt;strong&gt;어떤 순서로, 어디까지 되돌릴 수 있게, 다운타임은 얼마나&lt;/strong&gt; 잡고 실행하는지는 잘 안 다뤄진다. 그 실무 부분을 정리했다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;요지는 셋이다. 삭제는 순서가 중요하다(되돌릴 수 없으니까). 타입 변경과 삭제는 리스크가 다르다(전자는 롤백 가능, 후자는 영구). 예약 인스턴스(RI)는 맨 마지막에 산다. (모든 비용·수치는 더미 예시다.)&lt;/p&gt;

  &lt;!-- 확정안 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;확정안 — 계획에서 &quot;실제 적용값&quot;으로&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;분석(어느 게 과잉인가)과 확정(그래서 뭘로 바꾸나)은 다르다. 특히 &lt;strong&gt;가격 역전&lt;/strong&gt; 때문에 &quot;최신 세대로 바꾸면 무조건 싸다&quot;가 안 통하는 경우가 있었다.&lt;/p&gt;
  &lt;h3 style=&quot;font-size:19px;font-weight:800;color:#191f28;margin:28px 0 10px;&quot;&gt;RDS: 7대 → 5대&lt;/h3&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;인스턴스&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;현재(예시)&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;변경&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;share(메인)&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;r계열 xlarge(32GB)&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;r계열 large(16GB)&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;game writer ×2&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;r계열 large&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;유지 (가격 역전)&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;game reader ×2&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;r계열 large&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#c0392b;&quot;&gt;삭제 (HA 포기)&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;log ×2&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;r계열 large&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;버스터블 large(8GB)&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;strong&gt;함정 — 가격 역전.&lt;/strong&gt; game writer를 더 최신 세대로 바꾸려 했는데, 해당 리전·시점에선 오히려 더 비쌌다. 세대가 올라간다고 항상 싸지는 게 아니라 &lt;strong&gt;리전별 실단가를 그때그때 확인&lt;/strong&gt;해야 한다. 그래서 writer는 세대 교체 없이 유지하는 게 더 쌌다.
  &lt;/div&gt;
  &lt;h3 style=&quot;font-size:19px;font-weight:800;color:#191f28;margin:28px 0 10px;&quot;&gt;Redis / EC2&lt;/h3&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;대상&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;현재(예시)&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;변경&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;Redis 메인&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;m계열 large&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;버스터블 t계열 medium&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;API 서버 ×2&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;연산최적 large&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;버스터블 t계열 medium&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;API 서버 ×1&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;연산최적 large&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#c0392b;&quot;&gt;삭제&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;채팅 서버 ×1&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;버스터블 medium&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#c0392b;&quot;&gt;삭제&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;API 서버를 메모리 4GB로 내리면 php-fpm의 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;pm.max_children&lt;/code&gt;를 함께 조정해야 피크 때 메모리가 안 터진다. 안전을 더 사고 싶으면 8GB로 두는 선택지도 있다 — 월 몇 달러 차이. &quot;비용 최소&quot;와 &quot;여유&quot; 사이의 트레이드오프를 의식적으로 고른다.&lt;/p&gt;

  &lt;!-- 실행 순서 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;실행 순서 — 삭제는 순서가 생명&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;점검(서비스 차단) 중 아래 순서로 진행했다. 순서를 정한 이유는 전부 &lt;strong&gt;&quot;되돌릴 수 없는 작업을 안전한 뒤에 두기&quot;&lt;/strong&gt;다.&lt;/p&gt;
  &lt;div style=&quot;display:flex;flex-direction:column;gap:8px;margin:20px 0;&quot;&gt;
    &lt;div style=&quot;display:flex;gap:12px;align-items:flex-start;&quot;&gt;&lt;span style=&quot;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3182f6;color:#fff;font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#2b2f36;&quot;&gt;game writer 상태 확인 → 그다음 &lt;strong&gt;reader 삭제&lt;/strong&gt;&lt;/span&gt;&lt;/div&gt;
    &lt;div style=&quot;display:flex;gap:12px;align-items:flex-start;&quot;&gt;&lt;span style=&quot;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3182f6;color:#fff;font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#2b2f36;&quot;&gt;log ×2 → 버스터블로 타입 변경&lt;/span&gt;&lt;/div&gt;
    &lt;div style=&quot;display:flex;gap:12px;align-items:flex-start;&quot;&gt;&lt;span style=&quot;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3182f6;color:#fff;font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#2b2f36;&quot;&gt;share → 한 단계 아래로 타입 변경&lt;/span&gt;&lt;/div&gt;
    &lt;div style=&quot;display:flex;gap:12px;align-items:flex-start;&quot;&gt;&lt;span style=&quot;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3182f6;color:#fff;font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;color:#2b2f36;&quot;&gt;Redis → 한 단계 아래로 타입 변경&lt;/span&gt;&lt;/div&gt;
    &lt;div style=&quot;display:flex;gap:12px;align-items:flex-start;&quot;&gt;&lt;span style=&quot;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3182f6;color:#fff;font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#2b2f36;&quot;&gt;API 서버 2대 → 버스터블 변경, 나머지 1대 + 채팅 1대 &lt;strong&gt;삭제&lt;/strong&gt;&lt;/span&gt;&lt;/div&gt;
    &lt;div style=&quot;display:flex;gap:12px;align-items:flex-start;&quot;&gt;&lt;span style=&quot;flex:0 0 auto;width:24px;height:24px;border-radius:50%;background:#3182f6;color:#fff;font-size:12px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;&quot;&gt;6&lt;/span&gt;&lt;span style=&quot;color:#2b2f36;&quot;&gt;서비스 재개 → 24~48시간 지표 관찰 → &lt;strong&gt;RI 구매(마지막)&lt;/strong&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;왜 reader를 먼저 안 지우고 &lt;strong&gt;writer 확인부터&lt;/strong&gt; 하냐면 — reader는 writer 장애 시 자동 페일오버(HA) 대상이다. writer가 멀쩡한지 확인하기 전에 reader를 지우면, 그 사이 writer에 문제가 생겼을 때 받아줄 대상이 없다. 그래서 &quot;writer 정상 확인 → reader 삭제&quot; 순서가 안전하다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;reader 삭제는 곧 HA 포기다. 장애 시 복구가 1분에서 수 분으로 길어지는 것을 감수한다는 합의가 전제여야 한다.&lt;/p&gt;

  &lt;!-- 롤백 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;되돌릴 수 있는 것 / 없는 것&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;실행 전에 이 구분을 명확히 하고 들어간다. 이게 &quot;과감하게 줄이되 안전하게&quot;의 핵심이다.&lt;/p&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;작업&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;롤백&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;안전장치&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;타입 변경(RDS/Redis/EC2)&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#1a7a3f;&quot;&gt;상위 타입으로 즉시 복구, 데이터 유지&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;지표 이상 시 바로 원복&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;삭제(terminate)&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;color:#c0392b;&quot;&gt;&lt;strong&gt;복구 불가&lt;/strong&gt;&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;삭제 전 ELB 제외·cron 확인·스냅샷&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;타입 변경은 마음 편히 한다 — 지표가 나빠지면 되돌리면 된다. 삭제만 신중하게 한다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 12px;&quot;&gt;삭제 전 체크리스트는 세 가지다.&lt;/p&gt;
  &lt;ul style=&quot;margin:0 0 20px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:6px;&quot;&gt;ELB 타겟에서 제외됐는지&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:6px;&quot;&gt;그 인스턴스에만 걸린 cron/배치가 있는지&lt;/li&gt;
    &lt;li&gt;스냅샷은 떠뒀는지&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;불안하면 바로 terminate하지 말고 &lt;strong&gt;1~2주 정지(stop)로 운영&lt;/strong&gt;해 본 뒤 삭제하는 방법도 있다. 정지 상태는 컴퓨팅 비용이 안 나가니, 비용은 거의 줄이면서 &quot;진짜 안 써도 되는지&quot;를 안전하게 확인할 수 있다.&lt;/p&gt;

  &lt;!-- RI --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;예약 인스턴스(RI)는 맨 마지막&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;RI나 Savings Plan은 1년 약정으로 20% 이상을 더 깎아주지만, &lt;strong&gt;반드시 다운사이징·안정화가 끝난 뒤&lt;/strong&gt; 산다. 먼저 사면 그 타입에 묶여서, 나중에 더 줄이고 싶어도 약정 때문에 못 바꾼다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;그래서 순서가 &quot;타입 확정 → 24~48시간 관찰 → 이상 없으면 그 &lt;strong&gt;최종 타입 기준으로&lt;/strong&gt; RI/SP 구매&quot;다. 관찰 때 보는 지표는 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;CPU&lt;/code&gt;, &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;FreeableMemory&lt;/code&gt;, &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;ReadIOPS&lt;/code&gt;, &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;Evictions&lt;/code&gt;. 메모리 급락·ReadIOPS 급증·Evictions 발생은 &quot;너무 줄였다&quot;는 신호라 상위 타입으로 되돌린다. 그 신호가 없을 때만 RI를 건다.&lt;/p&gt;

  &lt;!-- 점검 운영 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;점검 운영 — 공지는 넉넉히, 작업은 짧게&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;공지 다운타임은 실제 작업 시간보다 넉넉히 잡는다. 실작업은 60~90분이어도, 공지는 검증·롤백 버퍼까지 포함해 몇 시간으로 잡았다. 작업이 예정대로 끝나면 일찍 열면 되지만, 문제가 생겼을 때 &lt;strong&gt;롤백할 시간&lt;/strong&gt;이 공지 안에 들어있어야 하기 때문이다. &quot;딱 맞게&quot; 공지하면 롤백 중에 다운타임을 초과한다. 그리고 트래픽이 가장 적은 시간대(새벽 등)에 잡아 영향을 최소화한다.&lt;/p&gt;

  &lt;!-- 결과 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;결과 — 1차로 약 1/3, RI까지 가면 더&lt;/h2&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;단계&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:right;color:#191f28;&quot;&gt;월 비용(예시)&lt;/th&gt;&lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:right;color:#191f28;&quot;&gt;절감률&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;현재&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;text-align:right;&quot;&gt;~$5,000&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;text-align:right;&quot;&gt;-&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;타입 변경 + 삭제&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;text-align:right;&quot;&gt;~$3,300&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;text-align:right;color:#1a7a3f;font-weight:700;&quot;&gt;~33%&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;+ RI 1년&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;text-align:right;&quot;&gt;~$3,000&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;text-align:right;color:#1a7a3f;font-weight:700;&quot;&gt;~40%&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;1차(다운사이징·삭제)만으로 약 33%가 줄고, 안정화 후 RI까지 적용하면 40% 선이다. 더 내리려면 &lt;strong&gt;2차로 기타 비용&lt;/strong&gt;을 정리한다 — 미사용 EBS/스냅샷/EIP, CloudWatch 로그 보존기간, NAT Gateway 구성, 상시 안 쓰는 dev 서버 정지. 1차가 &quot;큰 덩어리(컴퓨팅)&quot;였다면 2차는 &quot;자잘하지만 쌓이면 큰 것들&quot;이다.&lt;/p&gt;

  &lt;!-- 마치며 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;마치며&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;7일차가 &quot;줄일 수 있다는 근거&quot;였다면, 이번은 &quot;실제로 줄이는 법&quot;이었다. 실행에서 중요한 건 절감액 자체보다 &lt;strong&gt;순서와 되돌림&lt;/strong&gt;이다. 되돌릴 수 있는 것(타입 변경)은 과감하게, 되돌릴 수 없는 것(삭제·RI)은 뒤로 미루고 신중하게. 세대가 올라간다고 항상 싸지 않다는 것(가격 역전)도 실단가로 확인한다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 20px;&quot;&gt;트래픽을 포기한 게 아니라 &quot;안 쓰던 여유분&quot;을 걷어낸 것이라 서비스 체감은 그대로다. &quot;청구서는 큰데 CPU는 노는&quot; 서버를 운영하면서 손대기가 겁나는 분께, 순서만 지키면 생각보다 안전하다는 이야기로 도움이 되면 좋겠다.&lt;/p&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:36px 0 20px;&quot;&gt;&lt;/div&gt;

  &lt;!-- CTA --&gt;
  &lt;div style=&quot;background:#f8f9fa;border:1px solid #eceef1;border-radius:12px;padding:20px;text-align:center;margin-bottom:12px;&quot;&gt;
    &lt;div style=&quot;font-weight:700;color:#191f28;font-size:15px;&quot;&gt;AWS 비용 최적화 · 다운사이징 · 인프라 운영 문의&lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#6b7684;margin-top:6px;&quot;&gt;댓글로 편하게 남겨주세요.&lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;</description>
      <category>클라우드 운영</category>
      <category>aurora</category>
      <category>AWS</category>
      <category>ec2</category>
      <category>elasticache</category>
      <category>RDS</category>
      <category>SavingsPlan</category>
      <category>게임서버</category>
      <category>다운타임</category>
      <category>비용절감</category>
      <category>예약인스턴스</category>
      <author>oldserver</author>
      <guid isPermaLink="true">https://oldserver.tistory.com/15</guid>
      <comments>https://oldserver.tistory.com/15#entry15comment</comments>
      <pubDate>Mon, 19 Oct 2026 11:34:51 +0900</pubDate>
    </item>
    <item>
      <title>Rocky Linux에 PHP 8.5 + MySQL 8.4 LTS 설치하기</title>
      <link>https://oldserver.tistory.com/14</link>
      <description>&lt;div style=&quot;max-width:720px;margin:0 auto;padding:0 8px;color:#2b2f36;line-height:1.9;font-size:17px;word-break:keep-all;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot;&gt;

  &lt;!-- 카테고리 + 제목 --&gt;
  &lt;div style=&quot;font-size:13px;font-weight:700;color:#3182f6;letter-spacing:.5px;margin:18px 0 14px;&quot;&gt;INFRA · 서버 구축&lt;/div&gt;
  &lt;div style=&quot;font-size:32px;font-weight:800;line-height:1.4;color:#191f28;letter-spacing:-.3px;&quot;&gt;Rocky Linux에 PHP 8.5 + MySQL 8.4 LTS 설치하기&lt;/div&gt;
  &lt;div style=&quot;font-size:17px;color:#6b7684;margin:16px 0 8px;line-height:1.8;&quot;&gt;데이터는 전부 /data 디스크에. 중간에 반드시 만나는 두 에러(GPG check FAILED, socket 불일치)까지 함께 정리했다.&lt;/div&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:28px 0;&quot;&gt;&lt;/div&gt;

  &lt;!-- 도입 --&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;게임 서버를 새로 올릴 때, OS에 PHP·MySQL을 어떤 버전으로 어떻게 설치하느냐가 이후 운영을 한참 좌우한다. 이번엔 &lt;strong&gt;Rocky Linux 10에 PHP 8.5(Remi) + MySQL 8.4 LTS&lt;/strong&gt;를, 데이터는 전부 데이터 디스크(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/data&lt;/code&gt;)에 두는 구성으로 세팅했다. 이 서버 하나를 이미지로 떠서 라이브 API·DB 서버를 찍어낼 베이스가 되므로, 처음에 깔끔하게 잡아두는 게 중요하다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;이 글은 그 설치 전 과정을 순서대로 정리한다. SELinux 방침, Remi로 PHP 8.5 올리기, MySQL 8.4의 datadir을 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/data&lt;/code&gt;로 옮기기, 그리고 중간에 반드시 만나는 두 에러 — &lt;strong&gt;GPG check FAILED(키 만료)&lt;/strong&gt;와 &lt;strong&gt;Can't connect through socket&lt;/strong&gt; — 를 어떻게 넘겼는지까지. (모든 경로·주소는 예시다.)&lt;/p&gt;

  &lt;!-- 데이터 레이아웃 구성도 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;데이터 레이아웃&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;먼저 구조부터. 소스·DB 데이터는 OS 디스크가 아니라 &lt;strong&gt;데이터 디스크(/data)&lt;/strong&gt;에 두고, 접근은 홈 아래 심볼릭 링크로 한다. OS와 데이터를 분리해 두면 디스크 확장·이미지 복제가 깔끔하다.&lt;/p&gt;
  &lt;div style=&quot;margin:24px 0;&quot;&gt;
    &lt;div style=&quot;border:1px solid #eceef1;border-radius:12px;background:#fbfcfd;padding:14px;&quot;&gt;
      &lt;svg viewBox=&quot;0 0 760 230&quot; style=&quot;width:100%;height:auto;display:block;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;OS 디스크의 홈 경로가 심볼릭 링크로 데이터 디스크의 소스/DB를 가리키는 레이아웃&quot;&gt;
        &lt;defs&gt;
          &lt;marker id=&quot;ah13&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;
            &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#879196&quot;/&gt;
          &lt;/marker&gt;
          &lt;linearGradient id=&quot;d13blue&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;&lt;stop offset=&quot;0&quot; stop-color=&quot;#4D9BF5&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#2160C4&quot;/&gt;&lt;/linearGradient&gt;
          &lt;linearGradient id=&quot;d13green&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;&lt;stop offset=&quot;0&quot; stop-color=&quot;#34B37E&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#1C8F5F&quot;/&gt;&lt;/linearGradient&gt;
          &lt;linearGradient id=&quot;d13orange&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;&lt;stop offset=&quot;0&quot; stop-color=&quot;#F58536&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#C8511B&quot;/&gt;&lt;/linearGradient&gt;
        &lt;/defs&gt;

        &lt;!-- OS 디스크 그룹 --&gt;
        &lt;rect x=&quot;20&quot; y=&quot;30&quot; width=&quot;300&quot; height=&quot;170&quot; rx=&quot;12&quot; fill=&quot;#fff&quot; stroke=&quot;#cbd5e1&quot; stroke-dasharray=&quot;5 4&quot;/&gt;
        &lt;text x=&quot;36&quot; y=&quot;52&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;#64748b&quot;&gt;OS 디스크 (시스템)&lt;/text&gt;
        &lt;g&gt;
          &lt;rect x=&quot;40&quot; y=&quot;68&quot; width=&quot;46&quot; height=&quot;46&quot; rx=&quot;9&quot; fill=&quot;url(#d13blue)&quot;/&gt;
          &lt;path d=&quot;M52 86 h22 M52 94 h22 M52 102 h14&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.6&quot; stroke-linecap=&quot;round&quot;/&gt;
          &lt;text x=&quot;100&quot; y=&quot;86&quot; font-size=&quot;12.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;/home/appsvr/&lt;/text&gt;
          &lt;text x=&quot;100&quot; y=&quot;101&quot; font-size=&quot;10.5&quot; fill=&quot;#687078&quot;&gt;myapp_dev/server_root&lt;/text&gt;
          &lt;text x=&quot;100&quot; y=&quot;114&quot; font-size=&quot;10&quot; fill=&quot;#3182f6&quot;&gt;심볼릭 링크 →&lt;/text&gt;
        &lt;/g&gt;

        &lt;!-- 데이터 디스크 그룹 --&gt;
        &lt;rect x=&quot;440&quot; y=&quot;30&quot; width=&quot;300&quot; height=&quot;170&quot; rx=&quot;12&quot; fill=&quot;#fff&quot; stroke=&quot;#cbd5e1&quot; stroke-dasharray=&quot;5 4&quot;/&gt;
        &lt;text x=&quot;456&quot; y=&quot;52&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;#64748b&quot;&gt;데이터 디스크 /data&lt;/text&gt;
        &lt;g&gt;
          &lt;rect x=&quot;460&quot; y=&quot;68&quot; width=&quot;46&quot; height=&quot;46&quot; rx=&quot;9&quot; fill=&quot;url(#d13green)&quot;/&gt;
          &lt;path d=&quot;M472 80 h22 M472 90 h22 M472 100 h22&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.4&quot; stroke-linecap=&quot;round&quot;/&gt;
          &lt;text x=&quot;518&quot; y=&quot;86&quot; font-size=&quot;12.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;server_root&lt;/text&gt;
          &lt;text x=&quot;518&quot; y=&quot;101&quot; font-size=&quot;10.5&quot; fill=&quot;#687078&quot;&gt;PHP 게임 소스 (실제 저장)&lt;/text&gt;
        &lt;/g&gt;
        &lt;g&gt;
          &lt;rect x=&quot;460&quot; y=&quot;134&quot; width=&quot;46&quot; height=&quot;46&quot; rx=&quot;9&quot; fill=&quot;url(#d13orange)&quot;/&gt;
          &lt;rect x=&quot;471&quot; y=&quot;146&quot; width=&quot;24&quot; height=&quot;10&quot; rx=&quot;2&quot; fill=&quot;none&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2&quot;/&gt;
          &lt;rect x=&quot;471&quot; y=&quot;160&quot; width=&quot;24&quot; height=&quot;10&quot; rx=&quot;2&quot; fill=&quot;none&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2&quot;/&gt;
          &lt;text x=&quot;518&quot; y=&quot;152&quot; font-size=&quot;12.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;/data/mysql&lt;/text&gt;
          &lt;text x=&quot;518&quot; y=&quot;167&quot; font-size=&quot;10.5&quot; fill=&quot;#687078&quot;&gt;MySQL 8.4 datadir&lt;/text&gt;
        &lt;/g&gt;

        &lt;!-- 심볼릭 링크 화살표 (OS → data) --&gt;
        &lt;line x1=&quot;320&quot; y1=&quot;92&quot; x2=&quot;456&quot; y2=&quot;90&quot; stroke=&quot;#3182f6&quot; stroke-width=&quot;2&quot; stroke-dasharray=&quot;6 3&quot; marker-end=&quot;url(#ah13)&quot;/&gt;
      &lt;/svg&gt;
    &lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#8b95a1;margin-top:10px;text-align:center;&quot;&gt;그림 1. 홈 경로는 심볼릭 링크일 뿐, 소스·DB의 실제 저장은 데이터 디스크(/data)에 있다.&lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- 버전 선택 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;버전 선택 — 왜 PHP 8.5 / MySQL 8.4 LTS&lt;/h2&gt;
  &lt;ul style=&quot;margin:0 0 16px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;PHP 8.5&lt;/strong&gt; — 기존 운영이 8.4였고 한 세대 올렸다. 8.4 → 8.5는 deprecated·동작 변경이 있을 수 있어, 이 베이스 서버에서 &lt;strong&gt;소스 구동 검증&lt;/strong&gt;을 전제로 올린다(실패 시 Remi 모듈을 8.4로 내릴 수 있게).&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;MySQL 8.4 LTS&lt;/strong&gt; — 8.0은 2026년 EOL이라 쓰면 안 된다. 8.4는 LTS라 다음 베이스로 적합하다. &quot;지금 최신&quot;보다 &quot;오래 지원되는 LTS&quot;가 서버 수명 관점에서 유리하다.&lt;/li&gt;
  &lt;/ul&gt;

  &lt;!-- STEP 0 SELinux --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;STEP 0. SELinux — 방침부터 정하고 간다&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;프로젝트 전 서버에서 SELinux를 Off로 통일했다. MySQL datadir을 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/data&lt;/code&gt;로 옮길 때 SELinux가 켜져 있으면 컨텍스트 작업(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;semanage&lt;/code&gt;/&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;restorecon&lt;/code&gt;)이 필요하고, 깜빡하면 기동이 실패한다. Off로 두면 그 변수를 없애 세팅이 단순해진다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;getenforce                                           # 현재 상태
sudo sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config
sudo setenforce 0                                    # 재부팅 전까지 Permissive&lt;/code&gt;&lt;/pre&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    완전한 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;Disabled&lt;/code&gt;는 재부팅 후 반영된다. SELinux Off는 커널 레벨 접근제어를 끄는 것이므로 &lt;strong&gt;보안그룹(방화벽)과 SSH 접근제어로 보완&lt;/strong&gt;하는 전제에서만 택한다.
  &lt;/div&gt;

  &lt;!-- STEP 0-1 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;STEP 0-1. 호스트명 + 데이터 디렉토리&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;콘솔에서 만들면 호스트명이 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;VM-0-11-rockylinux&lt;/code&gt; 같은 식이라 서버 구분이 안 된다. 식별 가능한 이름으로 바꾸고, 소스는 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/data&lt;/code&gt;에 두되 접근은 홈 아래 심볼릭 링크로 하는 구조를 잡는다(그림 1).&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;# 호스트명 (소문자 권장)
sudo hostnamectl set-hostname myapp-dev-01
exec bash

# 소스는 /data에 실제 저장, /home 아래에서 심볼릭 링크로 접근
sudo mkdir -p /data/myapp_dev/server_root
sudo mkdir -p /home/appsvr/myapp_dev
sudo ln -s /data/myapp_dev/server_root /home/appsvr/myapp_dev/server_root&lt;/code&gt;&lt;/pre&gt;

  &lt;!-- STEP 1 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;STEP 1. 레포 추가 (EPEL + Remi)&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;PHP 최신 버전은 OS 기본 레포에 없으니 Remi를 쓴다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;sudo dnf -y update
sudo dnf -y install dnf-utils epel-release
sudo dnf -y install https://rpms.remirepo.net/enterprise/remi-release-10.rpm&lt;/code&gt;&lt;/pre&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;remi-release-10.rpm&lt;/code&gt;은 Rocky 10(el10)용이다. OS 버전과 레포 버전(el8/el9/el10)을 꼭 맞춰야 한다. el10 패키지가 아직 없으면 Remi 사이트에서 최신 경로를 확인한다.
  &lt;/div&gt;

  &lt;!-- STEP 2 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;STEP 2. PHP 8.5 설치&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;Remi 모듈을 리셋하고 8.5를 활성화한 뒤, 기존 서버와 동일한 확장 세트를 깐다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;sudo dnf -y module reset php
sudo dnf -y module enable php:remi-8.5

sudo dnf -y install \
  php php-cli php-fpm php-common \
  php-mysqlnd php-pdo \
  php-mbstring php-xml php-gd php-gmp php-sodium php-bz2 \
  php-opcache php-pecl-msgpack php-pecl-redis6 php-pecl-igbinary \
  php-process php-sqlite3

# 개발 서버에만 Xdebug (운영 서버엔 금지 — 성능 저하)
sudo dnf -y install php-pecl-xdebug3

php -v    # 8.5.x
php -m    # 확장 목록을 기존 서버와 비교&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;실제로 PHP 8.5.x에 msgpack·redis·igbinary·mysqli·mbstring·sodium·gd·gmp·openssl 등 기존 8.4 구성이 전부 로드됐다. &lt;strong&gt;설치 직후 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;php -m&lt;/code&gt;을 기존 서버와 비교&lt;/strong&gt;하는 습관이 중요하다. 확장 하나가 빠지면 소스가 특정 기능에서만 조용히 죽는다. el10에서 패키지명이 다르면(&quot;No match&quot;) &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;dnf search php-pecl-redis&lt;/code&gt;로 실제 이름을 확인한다.&lt;/p&gt;

  &lt;!-- STEP 3 GPG --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;STEP 3. MySQL 8.4 설치 — 첫 번째 에러 (GPG check FAILED)&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;MySQL 공식 레포를 추가하고 설치한다. el10 패키지가 없으면 el9로 받아도 호환된다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;sudo dnf -y install https://dev.mysql.com/get/mysql84-community-release-el9-1.noarch.rpm
sudo dnf -y install mysql-community-server&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;여기서 바로 막힌다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;Certificate B7B3B788A8D3785C ... Expired on 2025-10-22
GPG check FAILED&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;&lt;strong&gt;원인&lt;/strong&gt; — MySQL 서명 키가 2025-10-22에 만료됐고, &lt;strong&gt;패키지 자체가 그 만료된 키로 서명&lt;/strong&gt;돼 있다. 그래서 키를 최신으로 갱신해도(키서버 재수신, &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/etc/pki/rpm-gpg&lt;/code&gt; 키 파일 교체) 서명 검증이 통과되지 않는다. 구키 import, &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;gpg --recv-keys&lt;/code&gt;, 키링 교체까지 다 시도했지만 &quot;Expired&quot;가 계속 떴다. 사용자 측에서 키로는 못 푸는, Oracle 쪽 서명 이슈다.&lt;/p&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;strong&gt;해결&lt;/strong&gt; — 공식 레포에서 받는 패키지가 맞으므로, 이번 설치 &lt;strong&gt;한 번만&lt;/strong&gt; 서명 검증을 생략한다. 레포 전체를 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;gpgcheck=0&lt;/code&gt;으로 끄는 것보다 1회성이 안전하다.
  &lt;/div&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;sudo dnf -y install mysql-community-server --nogpgcheck
mysql --version    # 8.4.x&lt;/code&gt;&lt;/pre&gt;

  &lt;!-- STEP 4 socket --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;STEP 4. datadir을 /data로 — 두 번째 에러 (socket)&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;MySQL 데이터를 데이터 디스크(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/data/mysql&lt;/code&gt;)에 두려 한다. 핵심은 &lt;strong&gt;최초 기동 전에&lt;/strong&gt; 바꾸는 것이다. 이미 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/var/lib/mysql&lt;/code&gt;로 초기화된 뒤면 서비스를 멈추고 데이터를 옮겨야 해서 번거롭다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;sudo mkdir -p /data/mysql
sudo chown -R mysql:mysql /data/mysql
sudo chmod 750 /data/mysql
sudo vi /etc/my.cnf&lt;/code&gt;&lt;/pre&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;[mysqld]
datadir=/data/mysql
socket=/data/mysql/mysql.sock
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

# ↓ 이 [client] 섹션이 핵심 — 없으면 socket 에러가 난다
[client]
socket=/data/mysql/mysql.sock&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;기동 후 보안 설정을 하려는데 두 번째 에러가 난다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;sudo mysql_secure_installation
→ Can't connect through socket '/var/lib/mysql/mysql.sock'&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;&lt;strong&gt;원인&lt;/strong&gt; — 서버는 socket을 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/data/mysql/mysql.sock&lt;/code&gt;에 만들었는데(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;[mysqld]&lt;/code&gt; 설정), &lt;strong&gt;클라이언트는 기본값 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/var/lib/mysql/mysql.sock&lt;/code&gt;을 찾는다.&lt;/strong&gt; 서버와 클라이언트가 서로 다른 소켓 경로를 보고 있는 것이다.&lt;/p&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;strong&gt;해결&lt;/strong&gt; — &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;/etc/my.cnf&lt;/code&gt;에 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;[client]&lt;/code&gt; 섹션으로 소켓 경로를 명시한다(위 설정에 이미 포함). 서버 재시작도 필요 없이, 추가하자마자 다시 하면 된다.
  &lt;/div&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;sudo grep 'temporary password' /var/log/mysqld.log   # 임시 root 비번
sudo mysql_secure_installation                        # 익명계정/원격root/testDB 제거
mysql -u root -p -e &quot;SELECT @@datadir, @@socket, VERSION();&quot;
#   → /data/mysql/ , /data/mysql/mysql.sock , 8.4.x&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;SELinux를 STEP 0에서 꺼둔 덕에 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;/data/mysql&lt;/code&gt; 접근에 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;semanage&lt;/code&gt;/&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;restorecon&lt;/code&gt;이 필요 없었다. 켜져 있었다면 여기서 datadir 접근 거부로 기동이 실패했을 것이다.&lt;/p&gt;

  &lt;!-- STEP 5 Nginx --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;STEP 5. Nginx + SSL (개발은 Nginx가 직접 SSL 종료)&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;웹서버는 Nginx, php-fpm과는 유닉스 소켓으로 연결한다. 개발 서버는 앞단에 로드밸런서가 없으니 &lt;strong&gt;Nginx가 직접 SSL을 종료&lt;/strong&gt;한다(와일드카드 정식 인증서).&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;server {
    listen 7222 ssl;
    server_name _;
    root /home/appsvr/myapp_dev/server_root;

    ssl_certificate     /etc/nginx/ssl/example.crt.pem;
    ssl_certificate_key /etc/nginx/ssl/example.key.pem;
    ssl_protocols TLSv1.2;

    index index.php;
    location / { try_files $uri $uri/ /index.php?$query_string; }
    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm/www.sock;   # php-fpm listen과 짝 맞추기
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    location ~ /\.ht { deny all; }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;fastcgi_pass&lt;/code&gt;는 php-fpm의 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;listen&lt;/code&gt; 설정과 반드시 짝이 맞아야 한다(여기선 둘 다 소켓). 소켓/TCP가 어긋나면 502가 난다.&lt;/p&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;strong&gt;개발 vs 라이브 — SSL 종료 지점이 다르다.&lt;/strong&gt; 개발 서버는 Nginx가 직접 SSL(&lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;listen 7222 ssl&lt;/code&gt;), 라이브는 CLB가 SSL을 종료하고 API 서버 Nginx는 평문으로 받는다. 같은 코드베이스라도 &quot;SSL을 어디서 끊느냐&quot;가 환경마다 다르다는 걸 처음부터 설계에 반영해 둔다.
  &lt;/div&gt;

  &lt;!-- STEP 6 검증 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;STEP 6. 호환 검증 — 이 서버의 진짜 목적&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;이 베이스 서버의 목적은 설치 자체가 아니라 &lt;strong&gt;&quot;PHP 8.5 + MySQL 8.4에서 기존 게임 소스가 그대로 도느냐&quot;&lt;/strong&gt;를 검증하는 것이다.&lt;/p&gt;
  &lt;ul style=&quot;margin:0 0 16px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;PHP 확장 전부 로드(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;php -m&lt;/code&gt;을 기존 서버와 대조)&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;MySQL 8.4 접속·datadir·socket 확인&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;Nginx + SSL + php-fpm(sock) 연동 → 실제 HTTPS 응답 확인&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;PHP 8.5에서 소스 구동&lt;/strong&gt; — deprecated/변경점(예: &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;settype&lt;/code&gt; 사용부, &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;mcrypt&lt;/code&gt; 제거 등) 점검 ← 가장 중요&lt;/li&gt;
    &lt;li&gt;AES-256-CBC 등 암호화 로직이 OpenSSL 3.x에서 정상 동작하는지&lt;/li&gt;
  &lt;/ul&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    검증 실패 시 되돌릴 길을 미리 둔다. PHP는 Remi 모듈을 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;php:remi-8.4&lt;/code&gt;로 내리고, 그래도 안 되면 OS를 재설치하되 데이터 디스크(&lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;/data&lt;/code&gt;)는 보존한다. &quot;앞으로 못 돌아가는 설치&quot;는 베이스 서버에선 피한다.
  &lt;/div&gt;

  &lt;!-- 마치며 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;마치며&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;설치 글은 명령어 나열이 되기 쉽지만, 실제로 시간을 잡아먹는 건 늘 두세 개의 함정이다. 이번엔 그게 MySQL &lt;strong&gt;GPG 키 만료(GPG check FAILED)&lt;/strong&gt;와 &lt;strong&gt;datadir 변경 후 socket 불일치&lt;/strong&gt;였다. 둘 다 &quot;원래 그런 것&quot;이 아니라 원인이 분명하다 — 전자는 패키지가 만료 키로 서명된 것(1회 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;--nogpgcheck&lt;/code&gt;), 후자는 서버/클라이언트 소켓 경로 불일치(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;[client]&lt;/code&gt; 섹션 추가).&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;베이스 서버를 만들 땐 &lt;strong&gt;버전은 LTS로, 데이터는 분리 디스크로, 되돌릴 길은 남겨두고&lt;/strong&gt; 간다. 이 세 가지가 이후 라이브 서버를 이미지로 찍어낼 때 그대로 자산이 된다.&lt;/p&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:36px 0 20px;&quot;&gt;&lt;/div&gt;

  &lt;!-- CTA --&gt;
  &lt;div style=&quot;background:#f8f9fa;border:1px solid #eceef1;border-radius:12px;padding:20px;text-align:center;margin-bottom:12px;&quot;&gt;
    &lt;div style=&quot;font-weight:700;color:#191f28;font-size:15px;&quot;&gt;Rocky Linux · PHP/MySQL 서버 구축 · 인프라 문의&lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#6b7684;margin-top:6px;&quot;&gt;댓글로 편하게 남겨주세요.&lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;</description>
      <category>게임 서버구축</category>
      <category>GPG</category>
      <category>lts</category>
      <category>mysql8.4</category>
      <category>NGINX</category>
      <category>PHP8.5</category>
      <category>remi</category>
      <category>RockyLinux</category>
      <category>게임서버</category>
      <category>리눅스</category>
      <category>서버구축</category>
      <author>oldserver</author>
      <guid isPermaLink="true">https://oldserver.tistory.com/14</guid>
      <comments>https://oldserver.tistory.com/14#entry14comment</comments>
      <pubDate>Sun, 18 Oct 2026 11:31:45 +0900</pubDate>
    </item>
    <item>
      <title>AWS Bedrock 모델 단종(EOL) 트러블슈팅 + 폴백 모델 + IAM 설정</title>
      <link>https://oldserver.tistory.com/13</link>
      <description>&lt;div style=&quot;max-width:720px;margin:0 auto;padding:0 8px;color:#2b2f36;line-height:1.9;font-size:17px;word-break:keep-all;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot;&gt;

  &lt;!-- 카테고리 + 제목 --&gt;
  &lt;div style=&quot;font-size:13px;font-weight:700;color:#3182f6;letter-spacing:.5px;margin:18px 0 14px;&quot;&gt;OPERATION · AI 운영&lt;/div&gt;
  &lt;div style=&quot;font-size:32px;font-weight:800;line-height:1.4;color:#191f28;letter-spacing:-.3px;&quot;&gt;&quot;모델이 수명을 다했다&quot; — Bedrock EOL을 Converse로 복구하기&lt;/div&gt;
  &lt;div style=&quot;font-size:17px;color:#6b7684;margin:16px 0 8px;line-height:1.8;&quot;&gt;알람 메일의 AI 분석이 &quot;This model version has reached the end of its life&quot;로 멈췄다. 특정 모델에 안 묶이는 구조로 다시 짠 복구기.&lt;/div&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:28px 0;&quot;&gt;&lt;/div&gt;

  &lt;!-- 도입 --&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;11일차에서 CloudWatch 알람에 AI 분석을 붙였다. 에러가 터지면 Lambda가 로그를 긁어 Bedrock의 Claude에게 원인·영향·조치·심각도를 분석시켜 메일로 보내는 구조였다. 그런데 얼마 뒤 &quot;  AI 분석 결과&quot; 칸에 이런 문구가 찍히기 시작했다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;(AI 분석 실패: This model version has reached the end of its life.)&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;이번 글은 그 &quot;모델이 수명을 다했다(EOL)&quot; 에러를 추적하고, 특정 모델에 묶이지 않는 구조로 다시 짠 실전 복구기다. 핵심은 Bedrock의 &lt;strong&gt;InvokeModel에서 Converse API로 전환&lt;/strong&gt;해서, 앞으로 모델이 또 단종돼도 &lt;strong&gt;환경변수만 바꾸면 되게&lt;/strong&gt; 만든 것이다. (모든 식별자·ARN은 예시다.)&lt;/p&gt;

  &lt;!-- 전체 구성도 (SVG, 반응형: viewBox + width:100%) --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;전체 구성&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;먼저 전체 그림이다. 에러가 임계값을 넘으면 알람이 Lambda를 깨우고, Lambda가 로그를 조회해 Bedrock으로 분석한 뒤, 그 결과를 메일로 다시 보낸다. 이번에 고친 부분은 그림의 &lt;strong&gt;Bedrock 호출&lt;/strong&gt;(Converse + 주/폴백 모델)이다.&lt;/p&gt;
  &lt;div style=&quot;margin:24px 0;&quot;&gt;
    &lt;div style=&quot;border:1px solid #eceef1;border-radius:12px;background:#fbfcfd;padding:14px;&quot;&gt;
      &lt;svg viewBox=&quot;0 0 760 380&quot; style=&quot;width:100%;height:auto;display:block;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot; role=&quot;img&quot; aria-label=&quot;CloudWatch 알람에서 메일까지의 AI 분석 파이프라인 구성도 (AWS 아이콘 스타일)&quot;&gt;
        &lt;defs&gt;
          &lt;marker id=&quot;ah12&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;8&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;
            &lt;path d=&quot;M0,0 L10,5 L0,10 z&quot; fill=&quot;#879196&quot;/&gt;
          &lt;/marker&gt;
          &lt;!-- AWS 카테고리 그라데이션 --&gt;
          &lt;linearGradient id=&quot;awsOrange&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;
            &lt;stop offset=&quot;0&quot; stop-color=&quot;#F58536&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#C8511B&quot;/&gt;
          &lt;/linearGradient&gt;
          &lt;linearGradient id=&quot;awsPink&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;
            &lt;stop offset=&quot;0&quot; stop-color=&quot;#F34B9A&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#BC1356&quot;/&gt;
          &lt;/linearGradient&gt;
          &lt;linearGradient id=&quot;awsPurple&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;
            &lt;stop offset=&quot;0&quot; stop-color=&quot;#A166FF&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#6B2FD6&quot;/&gt;
          &lt;/linearGradient&gt;
          &lt;linearGradient id=&quot;awsBlue&quot; x1=&quot;0&quot; y1=&quot;0&quot; x2=&quot;1&quot; y2=&quot;1&quot;&gt;
            &lt;stop offset=&quot;0&quot; stop-color=&quot;#4D9BF5&quot;/&gt;&lt;stop offset=&quot;1&quot; stop-color=&quot;#2160C4&quot;/&gt;
          &lt;/linearGradient&gt;
        &lt;/defs&gt;

        &lt;!-- ========== 상단 행 ========== --&gt;
        &lt;!-- PHP 서버 (일반 리소스: 파랑) --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;24&quot; y=&quot;34&quot; width=&quot;56&quot; height=&quot;56&quot; rx=&quot;10&quot; fill=&quot;url(#awsBlue)&quot;/&gt;
          &lt;path d=&quot;M40 50 h24 M40 58 h24 M40 66 h16&quot; stroke=&quot;#fff&quot; stroke-width=&quot;3&quot; stroke-linecap=&quot;round&quot;/&gt;
          &lt;text x=&quot;52&quot; y=&quot;106&quot; text-anchor=&quot;middle&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;PHP 서버&lt;/text&gt;
          &lt;text x=&quot;52&quot; y=&quot;121&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#687078&quot;&gt;에러 로그&lt;/text&gt;
        &lt;/g&gt;
        &lt;!-- CloudWatch (주황: 모니터 그래프) --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;186&quot; y=&quot;34&quot; width=&quot;56&quot; height=&quot;56&quot; rx=&quot;10&quot; fill=&quot;url(#awsOrange)&quot;/&gt;
          &lt;path d=&quot;M199 72 L208 58 L215 66 L224 48 L231 56&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.6&quot; fill=&quot;none&quot; stroke-linjoin=&quot;round&quot; stroke-linecap=&quot;round&quot;/&gt;
          &lt;circle cx=&quot;224&quot; cy=&quot;48&quot; r=&quot;2.4&quot; fill=&quot;#fff&quot;/&gt;
          &lt;text x=&quot;214&quot; y=&quot;106&quot; text-anchor=&quot;middle&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;CloudWatch&lt;/text&gt;
          &lt;text x=&quot;214&quot; y=&quot;121&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#687078&quot;&gt;Alarm · 임계값 초과&lt;/text&gt;
        &lt;/g&gt;
        &lt;!-- SNS (분홍: 알림 종) --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;348&quot; y=&quot;34&quot; width=&quot;56&quot; height=&quot;56&quot; rx=&quot;10&quot; fill=&quot;url(#awsPink)&quot;/&gt;
          &lt;path d=&quot;M376 48 a8 8 0 0 1 8 8 v6 l3 4 h-22 l3-4 v-6 a8 8 0 0 1 8-8 z&quot; fill=&quot;#fff&quot;/&gt;
          &lt;circle cx=&quot;376&quot; cy=&quot;46&quot; r=&quot;2.4&quot; fill=&quot;#fff&quot;/&gt;
          &lt;path d=&quot;M372 70 a4 3 0 0 0 8 0&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2&quot; fill=&quot;none&quot;/&gt;
          &lt;text x=&quot;376&quot; y=&quot;106&quot; text-anchor=&quot;middle&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;SNS&lt;/text&gt;
          &lt;text x=&quot;376&quot; y=&quot;121&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#687078&quot;&gt;알람 발행&lt;/text&gt;
        &lt;/g&gt;
        &lt;!-- Lambda (주황: λ) --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;510&quot; y=&quot;34&quot; width=&quot;56&quot; height=&quot;56&quot; rx=&quot;10&quot; fill=&quot;url(#awsOrange)&quot;/&gt;
          &lt;path d=&quot;M528 48 h8 l14 28 h-8 l-5-11 -7 14 h-8 z&quot; fill=&quot;#fff&quot;/&gt;
          &lt;text x=&quot;538&quot; y=&quot;106&quot; text-anchor=&quot;middle&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;Lambda&lt;/text&gt;
          &lt;text x=&quot;538&quot; y=&quot;121&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;#687078&quot;&gt;알람 수신·처리&lt;/text&gt;
        &lt;/g&gt;

        &lt;!-- 상단 화살표 --&gt;
        &lt;line x1=&quot;84&quot; y1=&quot;62&quot; x2=&quot;182&quot; y2=&quot;62&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah12)&quot;/&gt;
        &lt;line x1=&quot;246&quot; y1=&quot;62&quot; x2=&quot;344&quot; y2=&quot;62&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah12)&quot;/&gt;
        &lt;line x1=&quot;408&quot; y1=&quot;62&quot; x2=&quot;506&quot; y2=&quot;62&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah12)&quot;/&gt;
        &lt;line x1=&quot;538&quot; y1=&quot;128&quot; x2=&quot;538&quot; y2=&quot;162&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah12)&quot;/&gt;

        &lt;!-- ========== Lambda 내부 처리 (점선 그룹) ========== --&gt;
        &lt;rect x=&quot;300&quot; y=&quot;162&quot; width=&quot;312&quot; height=&quot;150&quot; rx=&quot;14&quot; fill=&quot;#ffffff&quot; stroke=&quot;#A166FF&quot; stroke-dasharray=&quot;5 4&quot;/&gt;
        &lt;text x=&quot;316&quot; y=&quot;182&quot; font-size=&quot;11&quot; font-weight=&quot;700&quot; fill=&quot;#6B2FD6&quot;&gt;Lambda 처리&lt;/text&gt;

        &lt;!-- Logs Insights (주황) --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;320&quot; y=&quot;196&quot; width=&quot;44&quot; height=&quot;44&quot; rx=&quot;9&quot; fill=&quot;url(#awsOrange)&quot;/&gt;
          &lt;circle cx=&quot;338&quot; cy=&quot;214&quot; r=&quot;8&quot; fill=&quot;none&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.6&quot;/&gt;
          &lt;line x1=&quot;344&quot; y1=&quot;220&quot; x2=&quot;352&quot; y2=&quot;228&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.6&quot; stroke-linecap=&quot;round&quot;/&gt;
          &lt;text x=&quot;384&quot; y=&quot;212&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;Logs Insights&lt;/text&gt;
          &lt;text x=&quot;384&quot; y=&quot;227&quot; font-size=&quot;10&quot; fill=&quot;#687078&quot;&gt;최근 5분 로그 조회&lt;/text&gt;
        &lt;/g&gt;

        &lt;!-- Bedrock (보라, 강조) --&gt;
        &lt;rect x=&quot;452&quot; y=&quot;178&quot; width=&quot;146&quot; height=&quot;122&quot; rx=&quot;11&quot; fill=&quot;#f6f1ff&quot; stroke=&quot;#6B2FD6&quot; stroke-width=&quot;2&quot;/&gt;
        &lt;g&gt;
          &lt;rect x=&quot;466&quot; y=&quot;190&quot; width=&quot;40&quot; height=&quot;40&quot; rx=&quot;9&quot; fill=&quot;url(#awsPurple)&quot;/&gt;
          &lt;circle cx=&quot;486&quot; cy=&quot;210&quot; r=&quot;3&quot; fill=&quot;#fff&quot;/&gt;
          &lt;circle cx=&quot;476&quot; cy=&quot;202&quot; r=&quot;2.4&quot; fill=&quot;#fff&quot;/&gt;&lt;circle cx=&quot;496&quot; cy=&quot;202&quot; r=&quot;2.4&quot; fill=&quot;#fff&quot;/&gt;
          &lt;circle cx=&quot;476&quot; cy=&quot;218&quot; r=&quot;2.4&quot; fill=&quot;#fff&quot;/&gt;&lt;circle cx=&quot;496&quot; cy=&quot;218&quot; r=&quot;2.4&quot; fill=&quot;#fff&quot;/&gt;
          &lt;path d=&quot;M486 210 L476 202 M486 210 L496 202 M486 210 L476 218 M486 210 L496 218&quot; stroke=&quot;#fff&quot; stroke-width=&quot;1.4&quot;/&gt;
          &lt;text x=&quot;516&quot; y=&quot;206&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;Bedrock&lt;/text&gt;
          &lt;text x=&quot;516&quot; y=&quot;220&quot; font-size=&quot;9.5&quot; fill=&quot;#687078&quot;&gt;Converse API&lt;/text&gt;
        &lt;/g&gt;
        &lt;rect x=&quot;466&quot; y=&quot;238&quot; width=&quot;118&quot; height=&quot;24&quot; rx=&quot;6&quot; fill=&quot;#ede4ff&quot; stroke=&quot;#c9b4ff&quot;/&gt;
        &lt;text x=&quot;525&quot; y=&quot;254&quot; text-anchor=&quot;middle&quot; font-size=&quot;9.5&quot; fill=&quot;#5b25b8&quot;&gt;주 모델 (DeepSeek)&lt;/text&gt;
        &lt;rect x=&quot;466&quot; y=&quot;266&quot; width=&quot;118&quot; height=&quot;24&quot; rx=&quot;6&quot; fill=&quot;#f5efff&quot; stroke=&quot;#ddd0ff&quot;/&gt;
        &lt;text x=&quot;525&quot; y=&quot;282&quot; text-anchor=&quot;middle&quot; font-size=&quot;9.5&quot; fill=&quot;#6B2FD6&quot;&gt;폴백 (Kimi)&lt;/text&gt;

        &lt;line x1=&quot;364&quot; y1=&quot;218&quot; x2=&quot;448&quot; y2=&quot;214&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah12)&quot;/&gt;
        &lt;line x1=&quot;456&quot; y1=&quot;312&quot; x2=&quot;456&quot; y2=&quot;334&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah12)&quot;/&gt;

        &lt;!-- ========== 하단: SNS 재발행 → 메일 ========== --&gt;
        &lt;g&gt;
          &lt;rect x=&quot;300&quot; y=&quot;334&quot; width=&quot;44&quot; height=&quot;40&quot; rx=&quot;9&quot; fill=&quot;url(#awsPink)&quot;/&gt;
          &lt;path d=&quot;M322 346 a7 7 0 0 1 7 7 v5 l2 3 h-18 l2-3 v-5 a7 7 0 0 1 7-7 z&quot; fill=&quot;#fff&quot;/&gt;
          &lt;text x=&quot;356&quot; y=&quot;352&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;SNS 재발행&lt;/text&gt;
          &lt;text x=&quot;356&quot; y=&quot;367&quot; font-size=&quot;10&quot; fill=&quot;#687078&quot;&gt;마커로 무한루프 차단&lt;/text&gt;
        &lt;/g&gt;
        &lt;g&gt;
          &lt;rect x=&quot;508&quot; y=&quot;334&quot; width=&quot;44&quot; height=&quot;40&quot; rx=&quot;9&quot; fill=&quot;url(#awsBlue)&quot;/&gt;
          &lt;rect x=&quot;517&quot; y=&quot;346&quot; width=&quot;26&quot; height=&quot;18&quot; rx=&quot;2.5&quot; fill=&quot;none&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.2&quot;/&gt;
          &lt;path d=&quot;M517 348 l13 9 l13-9&quot; stroke=&quot;#fff&quot; stroke-width=&quot;2.2&quot; fill=&quot;none&quot;/&gt;
          &lt;text x=&quot;564&quot; y=&quot;358&quot; font-size=&quot;11.5&quot; font-weight=&quot;700&quot; fill=&quot;#232f3e&quot;&gt;이메일&lt;/text&gt;
        &lt;/g&gt;
        &lt;line x1=&quot;470&quot; y1=&quot;354&quot; x2=&quot;504&quot; y2=&quot;354&quot; stroke=&quot;#879196&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#ah12)&quot;/&gt;
      &lt;/svg&gt;
    &lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#8b95a1;margin-top:10px;text-align:center;&quot;&gt;그림 1. 알람 → Lambda(Logs Insights 조회 + Bedrock 분석) → SNS 재발행 → 메일. 보라색 블록이 이번에 고친 Bedrock 호출부.&lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- 증상 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;증상 — 알람은 오는데 AI 분석만 실패&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;알람 자체(에러 임계값 초과 메일)는 정상이었다. 로그도 잘 붙어 왔다. 딱 하나, AI 분석 칸만 매번 실패로 떴다. Lambda 로그를 보니 Bedrock 호출에서 예외가 나고 있었고, 메시지는 아주 명확했다 — &quot;이 모델 버전은 수명을 다했다.&quot;&lt;/p&gt;

  &lt;!-- 원인 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;원인 — Bedrock 모델이 EOL(단종)됨&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;11일차에서 쓴 모델은 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;anthropic.claude-3-haiku-20240307-v1:0&lt;/code&gt;이었다. 그런데 AWS Bedrock이 이 특정 모델 버전을 EOL 처리하면서 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;InvokeModel&lt;/code&gt; 호출이 더 이상 안 먹혔다. 여기서 두 가지 교훈이 나온다.&lt;/p&gt;
  &lt;ul style=&quot;margin:0 0 16px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;클라우드 AI 모델은 버전이 단종된다.&lt;/strong&gt; 특정 모델 ID를 코드에 박아두면 그 모델이 사라지는 순간 기능이 죽는다. 모델은 &quot;상수&quot;가 아니라 &quot;언젠가 바뀌는 값&quot;이다.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;AI 분석은 부가 기능인데 조용히 실패하고 있었다.&lt;/strong&gt; 알람 메일은 왔으니 아무도 몰랐다. 부가 기능일수록 &quot;실패해도 본체는 살아있게&quot; + &quot;실패를 눈에 띄게&quot;가 중요하다.&lt;/li&gt;
  &lt;/ul&gt;

  &lt;!-- 해결 1 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;해결 ① InvokeModel → Converse API로 전환&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;가장 중요한 결정이다. 기존 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;InvokeModel&lt;/code&gt;은 모델마다 요청/응답 body 포맷이 다르다. Anthropic은 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;anthropic_version&lt;/code&gt;·&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;messages&lt;/code&gt; 구조를 쓰고 다른 벤더는 또 다른 포맷을 쓴다. 그래서 모델을 바꾸면 코드도 같이 바꿔야 했다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;Bedrock의 &lt;strong&gt;Converse API&lt;/strong&gt;는 이 포맷 차이를 흡수한다. 어떤 모델이든 동일한 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;messages / content&lt;/code&gt; 구조로 호출된다. 즉 모델 교체가 코드 변경 없이 &quot;모델 ID만 교체&quot;로 끝난다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime';
const bedrockClient = new BedrockRuntimeClient({});

async function invokeConverse(modelId, logContent) {
  const prompt = `당신은 PHP 게임 서버 운영 전문가입니다.
아래 PHP 에러 로그를 분석해 한국어로 간결하게 답하세요:
1. 주요 원인  2. 영향 범위  3. 조치 방법  4. 심각도(높음/중간/낮음)

로그:
${logContent.substring(0, 4000)}`;

  const response = await bedrockClient.send(new ConverseCommand({
    modelId,                                   // ← 모델 ID만 바꾸면 끝
    messages: [{ role: 'user', content: [{ text: prompt }] }],
    inferenceConfig: { maxTokens: 1024, temperature: 0.2 },
  }));

  // Converse 응답: output.message.content[].text
  const blocks = response?.output?.message?.content || [];
  const text = blocks.map(b =&amp;gt; (typeof b?.text === 'string' ? b.text : '')).join('').trim();
  if (!text) throw new Error('모델 응답이 비어 있습니다');  // 빈 응답도 실패로 → 폴백 유도
  return text;
}&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;InvokeModel&lt;/code&gt; 시절의 모델 전용 body 조립(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;anthropic_version&lt;/code&gt; 등)이 전부 사라졌다. Converse가 알아서 모델별 포맷으로 변환해준다.&lt;/p&gt;

  &lt;!-- 해결 2 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;해결 ② 주 모델 + 폴백 모델&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;EOL 사건의 교훈을 코드에 녹였다. 모델을 하나만 믿지 않기. 주 모델이 실패하면 폴백 모델로 1회 재시도한다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;// 환경변수로 모델 지정 — 코드 재배포 없이 교체 가능
const BEDROCK_MODEL_ID          = process.env.BEDROCK_MODEL_ID          || 'deepseek.v3.2';
const BEDROCK_FALLBACK_MODEL_ID = process.env.BEDROCK_FALLBACK_MODEL_ID || 'moonshotai.kimi-k2.5';

async function analyzeWithBedrock(logContent) {
  // 주 모델 → 폴백 순서, 중복 제거
  const candidates = [BEDROCK_MODEL_ID, BEDROCK_FALLBACK_MODEL_ID]
    .filter((id, i, arr) =&amp;gt; id &amp;amp;&amp;amp; arr.indexOf(id) === i);

  let lastErr;
  for (const modelId of candidates) {
    try {
      return await invokeConverse(modelId, logContent);
    } catch (err) {
      lastErr = err;
      console.log(`모델 ${modelId} 실패: ${err.message} → 다음 후보 시도`);
    }
  }
  throw lastErr || new Error('사용 가능한 Bedrock 모델이 없습니다');
}&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;Converse가 포맷을 흡수하니, 주 모델과 폴백 모델이 &lt;strong&gt;서로 다른 벤더여도 상관없다.&lt;/strong&gt; 여기선 주 모델을 DeepSeek, 폴백을 Kimi로 뒀다. 둘 다 같은 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;invokeConverse&lt;/code&gt; 코드로 호출된다. 11일차처럼 Claude를 써도 되고 — 그냥 환경변수 값만 다를 뿐이다.&lt;/p&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;strong&gt;핵심 설계&lt;/strong&gt; — 모델 ID를 환경변수로 뺀 게 포인트다. 다음에 또 어떤 모델이 EOL 되거나 더 좋은/싼 모델이 나오면, 코드 재배포 없이 Lambda 환경변수만 바꾸면 끝난다.
  &lt;/div&gt;

  &lt;!-- 해결 3 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;해결 ③ IAM 권한을 두 모델 모두 허용으로&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;폴백을 넣었으니 IAM도 주 모델 + 폴백 모델 ARN을 둘 다 허용해야 한다. 하나만 열어두면 폴백이 권한 거부로 또 실패한다. 이번에 IAM 정책을 이렇게 다시 설정했다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Action&quot;: &quot;bedrock:InvokeModel&quot;,
  &quot;Resource&quot;: [
    &quot;arn:aws:bedrock:ap-northeast-1::foundation-model/deepseek.v3.2&quot;,
    &quot;arn:aws:bedrock:ap-northeast-1::foundation-model/moonshotai.kimi-k2.5&quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
  &lt;ul style=&quot;margin:0 0 16px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;Converse API도 권한은 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;bedrock:InvokeModel&lt;/code&gt;&lt;/strong&gt;이다(별도 Converse 액션이 아님). 헷갈리기 쉽다.&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;모델을 추가/교체하면 이 Resource 목록도 같이 갱신해야 한다. 환경변수로 모델만 바꾸고 IAM을 안 바꾸면 새 모델에서 AccessDenied가 난다.&lt;/li&gt;
    &lt;li&gt;Bedrock 콘솔의 &lt;strong&gt;Model access&lt;/strong&gt;에서 해당 모델이 &quot;액세스 허용(Access granted)&quot; 상태인지도 먼저 확인해야 한다. IAM과 별개로 모델 자체 사용 승인이 필요하다.&lt;/li&gt;
  &lt;/ul&gt;

  &lt;!-- 함정 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;함정 — 모델 ID와 리전 (on-demand vs inference profile)&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;마지막으로 데인 지점이다. 모델 ID를 넣었는데 이런 에러가 날 수 있다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1e2430;color:#e6edf3;border-radius:10px;padding:16px;white-space:pre-wrap;word-break:break-all;font-size:13.5px;line-height:1.6;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;on-demand throughput isn't supported&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;그 모델이 해당 리전에서 온디맨드 호출을 바로 지원하지 않고, cross-region inference profile을 거쳐야 하는 경우다. 이때는 모델 ID 앞에 리전 접두사가 붙은 inference profile ID(예: &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;apac.&amp;lt;model-id&amp;gt;&lt;/code&gt;)로 바꿔야 한다. 중요한 건 이것도 코드를 안 고쳐도 된다는 점이다. Converse 기반이라 환경변수 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;BEDROCK_MODEL_ID&lt;/code&gt;만 콘솔에서 확인한 inference profile ID로 바꾸면 된다. 정확한 호출 가능 ID는 Bedrock 콘솔 → Model catalog에서 확인한다.&lt;/p&gt;

  &lt;!-- 결과 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;결과 — 복구 + &quot;모델에 안 묶이는&quot; 구조&lt;/h2&gt;
  &lt;ul style=&quot;margin:0 0 16px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;AI 분석 복구&lt;/strong&gt; — 알람 메일에 다시 원인·영향·조치·심각도가 정상 출력&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;모델 독립&lt;/strong&gt; — 어떤 모델이 EOL 돼도 환경변수만 교체. 코드 재배포 불필요&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;이중화&lt;/strong&gt; — 주 모델이 죽어도 폴백이 받아줌&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;가장 좋은 건 &quot;급할 땐 환경변수만&quot;이다. 다음에 또 &quot;AI 분석 실패&quot; 메일이 오면, 코드를 열 필요 없이 Lambda 환경변수 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;BEDROCK_MODEL_ID&lt;/code&gt;를 살아있는 모델로 바꾸면 즉시 복구된다.&lt;/p&gt;

  &lt;!-- 마치며 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;마치며&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;이번 사건의 교훈은 하나로 요약된다. &lt;strong&gt;클라우드 AI 모델 ID는 &quot;상수&quot;가 아니라 &quot;언젠가 바뀌는 설정값&quot;이다.&lt;/strong&gt; 그래서 (1) 포맷을 흡수하는 Converse 같은 추상 API를 쓰고, (2) 모델 ID를 환경변수로 빼고, (3) 폴백을 두고, (4) IAM과 모델 액세스를 그에 맞게 열어두는 것. 이 네 가지면 다음 EOL이 와도 몇 분 만에 넘어간다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;부가 기능(AI 분석)이 조용히 죽어 있어도 본체(알람)는 멀쩡했다는 점도 다시 새겼다. 중요한 자동화일수록 &quot;실패를 눈에 띄게&quot; 만들어 두는 게 좋겠더라.&lt;/p&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:36px 0 20px;&quot;&gt;&lt;/div&gt;

  &lt;!-- CTA --&gt;
  &lt;div style=&quot;background:#f8f9fa;border:1px solid #eceef1;border-radius:12px;padding:20px;text-align:center;margin-bottom:12px;&quot;&gt;
    &lt;div style=&quot;font-weight:700;color:#191f28;font-size:15px;&quot;&gt;AWS Bedrock · AI 운영 자동화 · 트러블슈팅 문의&lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#6b7684;margin-top:6px;&quot;&gt;댓글로 편하게 남겨주세요.&lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;</description>
      <category>클라우드 운영</category>
      <category>ai운영</category>
      <category>AWS</category>
      <category>bedrock</category>
      <category>Claude</category>
      <category>converse</category>
      <category>deepseek</category>
      <category>Iam</category>
      <category>Lambda</category>
      <category>모델EOL</category>
      <category>트러블슈팅</category>
      <author>oldserver</author>
      <guid isPermaLink="true">https://oldserver.tistory.com/13</guid>
      <comments>https://oldserver.tistory.com/13#entry13comment</comments>
      <pubDate>Fri, 16 Oct 2026 10:55:30 +0900</pubDate>
    </item>
    <item>
      <title>CloudWatch 알람 메일에 AI 분석 붙이기 &amp;mdash; Lambda + Bedrock Claude 파이프라인</title>
      <link>https://oldserver.tistory.com/12</link>
      <description>&lt;div style=&quot;max-width:720px;margin:0 auto;padding:0 8px;color:#2b2f36;line-height:1.9;font-size:17px;word-break:keep-all;font-family:-apple-system,'Segoe UI','Malgun Gothic',sans-serif;&quot;&gt;

  &lt;!-- 카테고리 + 제목 --&gt;
  &lt;div style=&quot;font-size:13px;font-weight:700;color:#3182f6;letter-spacing:.5px;margin:18px 0 14px;&quot;&gt;OPERATION · 모니터링 자동화&lt;/div&gt;
  &lt;div style=&quot;font-size:32px;font-weight:800;line-height:1.4;color:#191f28;letter-spacing:-.3px;&quot;&gt;CloudWatch 알람에 AI 오류 분석을 붙이기&lt;/div&gt;
  &lt;div style=&quot;font-size:17px;color:#6b7684;margin:16px 0 8px;line-height:1.8;&quot;&gt;에러가 터지면 Lambda가 최근 로그를 긁어 AWS Bedrock의 Claude에게 분석시키고, 원인·영향·조치·심각도가 정리된 메일을 보내준다.&lt;/div&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:28px 0;&quot;&gt;&lt;/div&gt;

  &lt;!-- 도입 --&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;6일차에서 &quot;PHP 에러 알람이 울려 공격을 잡았다&quot;는 글을 썼다. 그 글을 읽고 &quot;그 알람은 어떻게 세팅한 거냐&quot;는 질문을 받았다. 이번 글은 그 &lt;strong&gt;알람 파이프라인을 처음부터 만드는 과정&lt;/strong&gt;이다. 그리고 핵심은 단순 알람이 아니라, 에러가 터지면 &lt;strong&gt;AWS Bedrock의 Claude가 로그를 분석해 &quot;원인·영향·조치·심각도&quot;를 정리한 메일을 보내주는&lt;/strong&gt; 부분이다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;소스 코드 변경 없이 설정만으로 되는 로그 수집부터, AI 분석 Lambda의 프롬프트 설계와 무한루프 방지까지 순서대로 정리했다. (모든 식별자·주소는 예시다.)&lt;/p&gt;

  &lt;!-- 왜 알람만으로 부족 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;왜 &quot;알람&quot;만으로는 부족한가&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;CloudWatch 알람은 &quot;PHP 에러가 분당 30건을 넘었다&quot;는 숫자만 알려준다. 그런데 새벽에 그 메일을 받으면, 서버에 접속해 로그를 열어보기 전까지는 아무것도 모른다. 진짜 심각한 건지, 일시적 스파이크인지, 어느 API가 문제인지 매번 사람이 로그를 뒤져야 했다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;그래서 알람에 두 가지를 더 얹었다. 첫째, 알람 메일에 &lt;strong&gt;최근 에러 로그를 같이&lt;/strong&gt; 붙여 접속 전에 뭐가 터졌는지 바로 본다. 둘째, &lt;strong&gt;AI가 그 로그를 분석&lt;/strong&gt;해 원인·영향·조치·심각도를 요약한다. 결과적으로 메일 하나만 봐도 &quot;지금 당장 일어나야 하나, 아침에 봐도 되나&quot;를 판단할 수 있게 됐다.&lt;/p&gt;

  &lt;!-- 파이프라인 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;전체 파이프라인&lt;/h2&gt;
  &lt;div style=&quot;margin:24px 0;&quot;&gt;
    &lt;div style=&quot;display:flex;flex-wrap:wrap;gap:8px;align-items:stretch;&quot;&gt;
      &lt;div style=&quot;flex:1 1 150px;background:#f8f9fa;border:1px solid #eceef1;border-radius:10px;padding:14px;text-align:center;&quot;&gt;&lt;b style=&quot;display:block;font-size:14px;color:#191f28;&quot;&gt;PHP 서버&lt;/b&gt;&lt;span style=&quot;font-size:12px;color:#8b95a1;&quot;&gt;에러 로그 파일 → Agent 수집&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;flex:1 1 150px;background:#f8f9fa;border:1px solid #eceef1;border-radius:10px;padding:14px;text-align:center;&quot;&gt;&lt;b style=&quot;display:block;font-size:14px;color:#191f28;&quot;&gt;CloudWatch Alarm&lt;/b&gt;&lt;span style=&quot;font-size:12px;color:#8b95a1;&quot;&gt;임계값 초과 → ALARM&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;flex:1 1 150px;background:#f8f9fa;border:1px solid #eceef1;border-radius:10px;padding:14px;text-align:center;&quot;&gt;&lt;b style=&quot;display:block;font-size:14px;color:#191f28;&quot;&gt;Lambda&lt;/b&gt;&lt;span style=&quot;font-size:12px;color:#8b95a1;&quot;&gt;로그 조회 → AI 분석&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;flex:1 1 150px;background:#eef4ff;border:1px solid #c8dcff;border-radius:10px;padding:14px;text-align:center;&quot;&gt;&lt;b style=&quot;display:block;font-size:14px;color:#1b64da;&quot;&gt;이메일&lt;/b&gt;&lt;span style=&quot;font-size:12px;color:#5b7cc0;&quot;&gt;로그 + AI 분석 수신&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#8b95a1;margin-top:10px;text-align:center;&quot;&gt;그림 1. 로그 수집 → 알람 → Lambda(로그조회+AI분석) → SNS → 메일&lt;/div&gt;
  &lt;/div&gt;

  &lt;!-- Step 1 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;Step 1. 로그 수집 — CloudWatch Agent&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;가장 좋은 점은 애플리케이션 코드를 안 건드린다는 것이다. EC2에 CloudWatch Agent를 설치하고, PHP가 이미 파일로 남기는 에러 로그를 그대로 수집하게 설정만 하면 된다. 설정의 핵심은 &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;logs.logs_collected.files.collect_list&lt;/code&gt;다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1c2027;color:#e6edf3;border-radius:10px;padding:18px;white-space:pre-wrap;word-break:break-all;font-size:14px;line-height:1.7;font-family:ui-monospace,monospace;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;{
  &quot;metrics&quot;: {
    &quot;namespace&quot;: &quot;MyGame/System&quot;,
    &quot;metrics_collected&quot;: {
      &quot;mem&quot;:  { &quot;measurement&quot;: [&quot;mem_used_percent&quot;] },
      &quot;disk&quot;: { &quot;measurement&quot;: [&quot;used_percent&quot;], &quot;resources&quot;: [&quot;/&quot;, &quot;/data&quot;] }
    }
  },
  &quot;logs&quot;: { &quot;logs_collected&quot;: { &quot;files&quot;: { &quot;collect_list&quot;: [
    { &quot;file_path&quot;: &quot;/data/app/logs/php_errors.log&quot;,
      &quot;log_group_name&quot;: &quot;/mygame/live/php-errors&quot;,
      &quot;log_stream_name&quot;: &quot;{instance_id}&quot;, &quot;retention_in_days&quot;: 30 },
    { &quot;file_path&quot;: &quot;/data/app/logs/exception_log_*&quot;,
      &quot;log_group_name&quot;: &quot;/mygame/live/exception&quot;,
      &quot;log_stream_name&quot;: &quot;{instance_id}&quot;, &quot;retention_in_days&quot;: 30 }
  ] } } }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    EC2에는 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;CloudWatchAgentServerPolicy&lt;/code&gt;가 붙은 IAM Role이 필요하다. 없으면 로그가 안 올라간다. 그리고 CloudWatch 기본 메트릭에는 &lt;strong&gt;메모리가 없어서&lt;/strong&gt;(EC2는 CPU만 기본 제공), Agent를 깔아야 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;mem_used_percent&lt;/code&gt;를 볼 수 있다.
  &lt;/div&gt;

  &lt;!-- Step 2 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;Step 2. 알람 — 임계값 초과하면 SNS로&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;로그가 올라오면 &quot;분당 에러 N건 초과&quot; 같은 조건으로 알람을 건다. 메트릭 알람은 CLI로 거는 게 재현·관리에 편하다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1c2027;color:#e6edf3;border-radius:10px;padding:18px;white-space:pre-wrap;word-break:break-all;font-size:14px;line-height:1.7;font-family:ui-monospace,monospace;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;REGION=&quot;ap-northeast-1&quot;
SNS_TOPIC=&quot;arn:aws:sns:${REGION}:&amp;lt;ACCOUNT_ID&amp;gt;:mygame-live-alerts&quot;

# SNS 토픽 + 이메일 구독 (최초 1회)
aws sns create-topic --name mygame-live-alerts --region $REGION
aws sns subscribe --topic-arn $SNS_TOPIC --protocol email \
  --notification-endpoint &quot;alerts@example.com&quot; --region $REGION

# 예: EC2 CPU 80% 초과 (5분 평균이 2회 연속)
aws cloudwatch put-metric-alarm --region $REGION \
  --alarm-name &quot;mygame-live-ec2-cpu-high&quot; \
  --namespace AWS/EC2 --metric-name CPUUtilization \
  --dimensions Name=InstanceId,Value=&amp;lt;INSTANCE_ID&amp;gt; \
  --statistic Average --period 300 --threshold 80 \
  --comparison-operator GreaterThanThreshold \
  --evaluation-periods 2 \
  --alarm-actions $SNS_TOPIC --ok-actions $SNS_TOPIC&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;PHP 에러 수 알람도 같은 방식이다(메트릭 필터로 로그에서 에러 건수를 메트릭으로 만들어 두고 임계값을 건다). RDS CPU·FreeableMemory·Connections, Redis Memory·Evictions도 같은 패턴으로 걸어둔다. 6일차에서 다룬 &quot;임계값을 올리지 말고 평가 기간을 늘려라&quot;가 여기 적용된다. &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:15px;&quot;&gt;--evaluation-periods&lt;/code&gt;로 순간 스파이크는 무시하고 지속되는 에러만 알람으로 만든다.&lt;/p&gt;

  &lt;!-- Step 3 핵심 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;Step 3. AI 오류 분석 — 이 글의 핵심&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;여기서부터가 진짜다. SNS 알람을 사람이 받는 게 아니라 &lt;strong&gt;Lambda가 먼저 받아서&lt;/strong&gt; 로그를 긁고 AI에게 분석시킨다. 흐름은 세 단계다.&lt;/p&gt;

  &lt;h3 style=&quot;font-size:19px;font-weight:800;color:#191f28;margin:30px 0 10px;&quot;&gt;3-1. Logs Insights로 최근 에러 로그 긁기&lt;/h3&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;Lambda는 알람을 받으면 여러 로그 그룹(php-errors, exception 등)에 대해 최근 5분치 로그를 Logs Insights 쿼리로 가져온다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1c2027;color:#e6edf3;border-radius:10px;padding:18px;white-space:pre-wrap;word-break:break-all;font-size:14px;line-height:1.7;font-family:ui-monospace,monospace;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;const queryString = `fields @timestamp, @message
| sort @timestamp desc
| limit ${QUERY_LIMIT}`;

const { queryId } = await logsClient.send(new StartQueryCommand({
  logGroupName: groupName, startTime, endTime, queryString,
}));

// StartQuery는 비동기 — Complete 될 때까지 폴링
let status = 'Running';
while (status === 'Running' || status === 'Scheduled') {
  await sleep(1000);
  const res = await logsClient.send(new GetQueryResultsCommand({ queryId }));
  status = res.status;
  results = res.results || [];
}&lt;/code&gt;&lt;/pre&gt;
  &lt;div style=&quot;background:#f2f4f6;border-left:3px solid #3182f6;border-radius:0 6px 6px 0;padding:16px 20px;margin:24px 0;color:#4e5968;font-size:16px;&quot;&gt;
    &lt;strong&gt;함정&lt;/strong&gt; — Logs Insights의 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;StartQuery&lt;/code&gt;는 즉시 결과를 주지 않는다. queryId만 돌려주고, &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;GetQueryResults&lt;/code&gt;로 &lt;code style=&quot;background:#fff;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;Complete&lt;/code&gt;가 될 때까지 폴링해야 한다. 그래서 Lambda &lt;strong&gt;Timeout을 120초&lt;/strong&gt;로 넉넉히 잡아야 한다(기본 3초면 무조건 실패).
  &lt;/div&gt;

  &lt;h3 style=&quot;font-size:19px;font-weight:800;color:#191f28;margin:30px 0 10px;&quot;&gt;3-2. Bedrock Claude에게 분석 요청&lt;/h3&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;긁어온 로그를 프롬프트에 넣어 Bedrock의 Claude에게 던진다. 핵심은 &lt;strong&gt;&quot;무엇을 답하라&quot;를 구조화&lt;/strong&gt;하는 것이다. 자유 서술로 두면 매번 형식이 달라 메일로 읽기 나쁘다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1c2027;color:#e6edf3;border-radius:10px;padding:18px;white-space:pre-wrap;word-break:break-all;font-size:14px;line-height:1.7;font-family:ui-monospace,monospace;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;const prompt = `당신은 PHP 게임 서버 운영 전문가입니다.
아래는 최근 발생한 PHP 에러 로그입니다. 다음을 한국어로 간결하게 답하세요:

1. **주요 원인** (에러 패턴 요약)
2. **영향 범위** (어떤 기능/API에 영향이 있는지)
3. **조치 방법** (즉시 조치 + 근본 해결책)
4. **심각도** (높음/중간/낮음)

로그:
${logContent.substring(0, 4000)}`;

const body = JSON.stringify({
  anthropic_version: 'bedrock-2023-05-31',
  max_tokens: 1024,
  messages: [{ role: 'user', content: prompt }],
});

const response = await bedrockClient.send(new InvokeModelCommand({
  modelId: 'anthropic.claude-3-haiku-20240307-v1:0',  // 빠르고 저렴
  contentType: 'application/json', accept: 'application/json', body,
}));

const parsed = JSON.parse(new TextDecoder().decode(response.body));
const aiAnalysis = parsed.content[0].text;&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 10px;&quot;&gt;프롬프트 설계에서 신경 쓴 포인트는 다음과 같다.&lt;/p&gt;
  &lt;ul style=&quot;margin:0 0 16px;padding-left:20px;color:#2b2f36;&quot;&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;역할 부여&lt;/strong&gt; — &quot;운영 전문가&quot;라고 역할을 주면 일반 설명 대신 운영 관점(어느 API가 영향받는지)으로 답한다.&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;출력 항목 고정&lt;/strong&gt; — 원인/영향/조치/심각도 4개로 못박아 메일에서 늘 같은 구조로 읽히게.&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;심각도 라벨 강제&lt;/strong&gt; — &quot;높음/중간/낮음&quot;으로 대응 우선순위를 한눈에.&lt;/li&gt;
    &lt;li style=&quot;margin-bottom:8px;&quot;&gt;&lt;strong&gt;로그 길이 컷&lt;/strong&gt;(&lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;substring(0, 4000)&lt;/code&gt;) — 토큰 비용·한도 보호. 최근 로그만으로도 패턴 파악엔 충분.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;모델 선택&lt;/strong&gt; — &lt;code style=&quot;background:#f2f4f6;border-radius:4px;padding:1px 6px;font-size:14px;&quot;&gt;claude-3-haiku&lt;/code&gt;는 빠르고 저렴. 알람 요약엔 가벼운 모델로 충분하고, 깊은 분석이 필요하면 상위 모델로 교체.&lt;/li&gt;
  &lt;/ul&gt;

  &lt;h3 style=&quot;font-size:19px;font-weight:800;color:#191f28;margin:30px 0 10px;&quot;&gt;3-3. 로그 + AI 분석을 합쳐 메일로&lt;/h3&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;분석 결과와 원본 로그를 하나의 메일 본문으로 합쳐 SNS에 다시 발행한다. 그러면 이메일 구독자가 받는다. 실제 수신 메일은 이런 모습이다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1c2027;color:#e6edf3;border-radius:10px;padding:18px;white-space:pre-wrap;word-break:break-all;font-size:14px;line-height:1.7;font-family:ui-monospace,monospace;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;  PHP Error 임계값 초과 알림
━━━━━━━━━━━━━━━━━━━━━━━━
■ 알람: mygame-live-php-errors-high
■ 시각: 2026-10-xx 03:14 (KST)
■ 사유: Threshold Crossed: 42 &amp;gt; 30

  AI 분석 결과
━━━━━━━━━━━━━━━━━━━━━━━━
1. 주요 원인: 특정 유저 ID의 보상수령 API 반복 호출 →
   foreach 타입 경고 다수
2. 영향 범위: reward 관련 API. 정상 유저 영향은 적음
3. 조치: 해당 IP ALB 차단 + 입력 검증 보강
4. 심각도: 중간 (공격 패턴이나 서버 검증으로 차단 중)

  최근 5분 에러 로그
━━━━━━━━━━━━━━━━━━━━━━━━
[로그 원문 ...]&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;실제로 6일차에서 겪은 공격 때, 이 AI 분석이 &quot;단일 유저의 반복 호출, 공격 의심, 심각도 중간&quot;이라고 먼저 짚어줘서 상황 파악이 훨씬 빨랐다.&lt;/p&gt;

  &lt;!-- 함정: 무한루프 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;함정 — AI 알람이 자기 자신을 다시 트리거(무한루프)&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;이 구조에서 제일 조심할 건 무한루프다. Lambda가 SNS에 메일을 재발행하는데, 그 SNS가 다시 Lambda를 트리거하면 영원히 돈다(그리고 Bedrock 호출 비용이 폭발). 막는 방법은 간단하다. &lt;strong&gt;Lambda가 발행하는 메시지에 마커를 심고, 수신 시 마커가 있으면 즉시 스킵&lt;/strong&gt;한다.&lt;/p&gt;
  &lt;pre style=&quot;background:#1c2027;color:#e6edf3;border-radius:10px;padding:18px;white-space:pre-wrap;word-break:break-all;font-size:14px;line-height:1.7;font-family:ui-monospace,monospace;margin:24px 0;&quot;&gt;&lt;code style=&quot;background:transparent;color:#e6edf3;&quot;&gt;// 수신: Lambda가 보낸 메시지면 스킵
const attrs = event.Records?.[0]?.Sns?.MessageAttributes || {};
if (attrs.source?.Value === 'lambda-alert') {
  return { statusCode: 200, body: 'self-published, skip' };
}

// ... 처리 후 발행: 마커를 심어서 보냄
await snsClient.send(new PublishCommand({
  TopicArn: SNS_TOPIC_ARN, Subject: subject, Message: messageBody,
  MessageAttributes: { source: { DataType: 'String', StringValue: 'lambda-alert' } },
}));&lt;/code&gt;&lt;/pre&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;추가 안전장치로 &lt;strong&gt;AlarmName이 없거나 JSON 파싱이 실패하면 스킵&lt;/strong&gt;, &lt;strong&gt;ALARM 상태가 아니면 스킵&lt;/strong&gt;도 넣었다. &quot;내가 만든 메시지는 내가 처리하지 않는다&quot;가 핵심이다.&lt;/p&gt;

  &lt;!-- IAM --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;필요한 IAM 권한&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;Lambda 실행 역할에 딱 세 가지만 주면 된다(최소 권한).&lt;/p&gt;
  &lt;div style=&quot;overflow-x:auto;margin:20px 0;&quot;&gt;
  &lt;table style=&quot;border-collapse:collapse;width:100%;min-width:420px;font-size:15px;&quot;&gt;
    &lt;thead&gt;&lt;tr style=&quot;background:#f8f9fa;&quot;&gt;
      &lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;액션&lt;/th&gt;
      &lt;th style=&quot;border:1px solid #eceef1;padding:11px;text-align:left;color:#191f28;&quot;&gt;용도&lt;/th&gt;
    &lt;/tr&gt;&lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;font-family:ui-monospace,monospace;font-size:13px;&quot;&gt;logs:StartQuery, logs:GetQueryResults&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;Logs Insights로 에러 로그 조회&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;font-family:ui-monospace,monospace;font-size:13px;&quot;&gt;sns:Publish&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;분석 결과 메일 재발행&lt;/td&gt;&lt;/tr&gt;
      &lt;tr&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;font-family:ui-monospace,monospace;font-size:13px;&quot;&gt;bedrock:InvokeModel&lt;/td&gt;&lt;td style=&quot;border:1px solid #eceef1;padding:11px;&quot;&gt;Claude 호출&lt;/td&gt;&lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
  &lt;/div&gt;

  &lt;!-- 마치며 --&gt;
  &lt;h2 style=&quot;font-size:24px;font-weight:800;color:#191f28;margin:44px 0 14px;letter-spacing:-.3px;&quot;&gt;마치며&lt;/h2&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;알람은 &quot;숫자&quot;지만, 운영자가 진짜 원하는 건 &quot;그래서 이게 뭔데, 지금 일어나야 해?&quot;에 대한 답이다. CloudWatch 알람 뒤에 Lambda + Bedrock을 한 겹 끼우면, 메일 하나에 로그 원문과 AI가 정리한 원인·영향·조치·심각도가 같이 와서 그 판단이 빨라진다.&lt;/p&gt;
  &lt;p style=&quot;margin:0 0 16px;&quot;&gt;핵심은 세 가지였다. Logs Insights는 비동기라 폴링 + 넉넉한 Timeout, 프롬프트는 출력 항목을 고정(원인/영향/조치/심각도), 그리고 SNS 재발행 무한루프를 마커로 차단. 가벼운 모델(haiku)로도 알람 요약은 충분하고, 비용도 알람당 몇 원 수준이다.&lt;/p&gt;

  &lt;div style=&quot;height:1px;background:#eceef1;margin:36px 0 20px;&quot;&gt;&lt;/div&gt;

  &lt;!-- CTA --&gt;
  &lt;div style=&quot;background:#f8f9fa;border:1px solid #eceef1;border-radius:12px;padding:20px;text-align:center;margin-bottom:12px;&quot;&gt;
    &lt;div style=&quot;font-weight:700;color:#191f28;font-size:15px;&quot;&gt;AWS 운영 자동화 · AI 로그 분석 · 모니터링 문의&lt;/div&gt;
    &lt;div style=&quot;font-size:14px;color:#6b7684;margin-top:6px;&quot;&gt;댓글로 편하게 남겨주세요.&lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;</description>
      <category>AI  개발</category>
      <category>ai운영</category>
      <category>AWS</category>
      <category>bedrock</category>
      <category>Claude</category>
      <category>cloudwatch</category>
      <category>Lambda</category>
      <category>php</category>
      <category>SNS</category>
      <category>로그분석</category>
      <category>모니터링</category>
      <author>oldserver</author>
      <guid isPermaLink="true">https://oldserver.tistory.com/12</guid>
      <comments>https://oldserver.tistory.com/12#entry12comment</comments>
      <pubDate>Thu, 15 Oct 2026 10:34:50 +0900</pubDate>
    </item>
  </channel>
</rss>