<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>ASSU BLOG.</title>
    <description>ASSU BLOG...
</description>
    <link>https://assu10.github.io/</link>
    <atom:link href="https://assu10.github.io/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Sun, 09 Aug 2026 09:37:26 +0000</pubDate>
    <lastBuildDate>Sun, 09 Aug 2026 09:37:26 +0000</lastBuildDate>
    <generator>Jekyll v3.9.1</generator>
    
      <item>
        <title>Big-O(빅오) 표기법</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#big-o빅오-표기법이란&quot;&gt;Big-O(빅오) 표기법이란?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#주요-시간-복잡도-한-눈에-보기&quot;&gt;주요 시간 복잡도 한 눈에 보기&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#시간-복잡도big-o-표기법&quot;&gt;시간 복잡도(Big-O) 표기법&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#big-o-계산-규칙&quot;&gt;Big-O 계산 규칙&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#실무-개발자를-위한-요약&quot;&gt;실무 개발자를 위한 요약&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;개발을 하다보면 ‘이 코드의 시간 복잡도는 \(O(N)\) 이다.’, ‘이 쿼리는 \(O(N^2)\)이라 위험하다.’와 같은 말을 자주 듣는다.&lt;/p&gt;

&lt;p&gt;여기서는 서비스의 성능과 직결되는 &lt;strong&gt;Big-O(빅오)&lt;/strong&gt; 표기법에 대해 가볍게 훑어본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;big-o빅오-표기법이란&quot;&gt;Big-O(빅오) 표기법이란?&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;‘입력 데이터의 크기(N)가 늘어날 때, 프로그램의 실행 시간이 얼마나 늘어나는가?’&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Big-O 표기법은 알고리즘의 효율성을 수학적으로 나타낸 표기법이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;컴퓨터 성능과 무관&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하드웨어가 아무리 좋아도 알고리즘 자체가 비효율적이면 데이터가 커졌을 때 터지게 된다.&lt;/li&gt;
      &lt;li&gt;Big-O는 하드웨어 사양을 배제하고 알고리즘 자체의 구조적 성능만 평가한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;최악의 시나리오 기준&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;‘아무리 안 좋아도 이 정도 시간 안에는 끝난다.’는 성능의 상한선을 보장하기 위해 주로 최악의 경우를 기재한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;주요-시간-복잡도-한-눈에-보기&quot;&gt;주요 시간 복잡도 한 눈에 보기&lt;/h1&gt;

\[\text{(빠름) } O(1) &amp;lt; O(\log N) &amp;lt; O(N) &amp;lt; O(N \log N) &amp;lt; O(N^2) &amp;lt; O(2^N) \text{ (느림)}\]

&lt;h3 id=&quot;시간-복잡도big-o-표기법&quot;&gt;시간 복잡도(Big-O) 표기법&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;표기법&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;명칭&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;설명&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;대표적인 예시&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;\(O(1)\)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;상수 시간 (Constant)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;데이터 양과 상관없이 &lt;strong&gt;항상 일정한 시간&lt;/strong&gt; 소요&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;배열 인덱스 접근, 해시맵(HashMap) 조회&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;\(O(\log N)\)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;로그 시간 (Logarithmic)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;데이터가 늘어나도 수행 시간이 &lt;strong&gt;매우 천천히 증가&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;이진 탐색(Binary Search), DB B-Tree 인덱스&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;\(O(N)\)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;선형 시간 (Linear)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;데이터 양에 &lt;strong&gt;정비례&lt;/strong&gt;하여 시간 증가&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;단일 반복문(for), DB Full Table Scan&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;\(O(N \log N)\)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;선형 로그 시간&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;\(O(N)\)보다는 느리지만 효율적인 정렬 알고리즘&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;병합 정렬(Merge Sort), 퀵 정렬(Quick Sort) 평균&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;\(O(N^2)\)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2차 제곱 시간 (Quadratic)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;데이터가 늘어나면 작업량이 &lt;strong&gt;제곱으로 폭발&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;중첩 반복문 (2중 for문), 상관 서브쿼리&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;\(O(2^N)\)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;지수 시간 (Exponential)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;데이터가 1개 늘어날 때마다 &lt;strong&gt;작업량이 2배씩 곱해짐&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;단순 재귀 피보나치 수열&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;big-o-계산-규칙&quot;&gt;Big-O 계산 규칙&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;① 상수는 버린다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;N이 무한히 커지면 비례 계수인 상수는 의미가 사라진다.&lt;br /&gt;
\(O(2N) \rightarrow \mathbf{O(N)}\)&lt;br /&gt;
\(O(50) \rightarrow \mathbf{O(1)}\)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;② 가장 영향력이 큰 항만 남긴다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;최악의 상황에서 최고차항이 압도적인 비중을 차지하므로 낮은 차수의 항은 무시한다.&lt;br /&gt;
\(O(N^2 + N + 100) \rightarrow \mathbf{O(N^2)}\)&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;실무-개발자를-위한-요약&quot;&gt;실무 개발자를 위한 요약&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;\(O(1)\) ~ \(O(N \log N)\)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;실무/운영 환경에서 권장하는 안정적인 성능 범위&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;\(O(N^2)\) 이상&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터가 적을때는 티가 안 나지만, 수십만 건 이상의 실제 DB나 서버 환경에 올라가는 순간 서비스 지연 및 서버 다운을 유발하는 주범&lt;/li&gt;
      &lt;li&gt;인덱스 안 탄 쿼리, 중첩 반복문 등&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 08 Aug 2026 00:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/08/08/big-o/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/08/08/big-o/</guid>
        
        <category>backend</category>
        
        <category>빅오표기법</category>
        
        <category>Big-O</category>
        
        <category>시간복잡도</category>
        
        <category>time-complexity</category>
        
        <category>알고리즘</category>
        
        <category>algorithm</category>
        
        <category>백엔드</category>
        
        <category>컴퓨터사이언스</category>
        
        <category>CS</category>
        
        <category>성능최적화</category>
        
        <category>쿼리최적화</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(2) - 실시간 게임 리더보드 아키텍처(Redis Sorted Set부터 DynamoDB 샤딩까지)</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-문제-이해-및-요구사항-정의&quot;&gt;1. 문제 이해 및 요구사항 정의&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-기능-요구사항&quot;&gt;1.1 기능 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#12-비기능-요구사항&quot;&gt;1.2. 비기능 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#13-qps-및-트래픽-산정&quot;&gt;1.3. QPS 및 트래픽 산정&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#북미-지역-기준-저녁-피크-타임이-갖는-의미는-무엇일까&quot;&gt;💡북미 지역 기준 저녁 피크 타임이 갖는 의미는 무엇일까?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-개략적-시스템-설계-및-데이터-흐름&quot;&gt;2. 개략적 시스템 설계 및 데이터 흐름&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-api-설계&quot;&gt;2.1. API 설계&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-개략적-아키텍처와-서비스-분리&quot;&gt;2.2. 개략적 아키텍처와 서비스 분리&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#221-대안1-클라이언트가-순위표-서비스와-직접-통신해야-할까&quot;&gt;2.2.1. 대안1: 클라이언트가 순위표 서비스와 직접 통신해야 할까?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#다른-게임들도-게임-서비스가-게임이-언제-끝나는지-아는-것-아닐까&quot;&gt;💡다른 게임들도 게임 서비스가 게임이 언제 끝나는지 아는 것 아닐까?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#222-대안-2-게임-서비스와-순위표-서버-사이에-메시지-큐가-필요한가&quot;&gt;2.2.2. 대안 2: 게임 서비스와 순위표 서버 사이에 메시지 큐가 필요한가?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-데이터-모델-rdb-vs-redis-sorted-set&quot;&gt;2.3. 데이터 모델: RDB vs Redis Sorted Set&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#231-rdb의-한계와-sql-쿼리-분석&quot;&gt;2.3.1. RDB의 한계와 SQL 쿼리 분석&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#rdb는-왜-규모-확장성이-좋지-않을까&quot;&gt;💡RDB는 왜 규모 확장성이 좋지 않을까?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#상관-서브쿼리correlated-subquery란&quot;&gt;💡상관 서브쿼리(Correlated Subquery)란?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#232-redis-sorted-set을-활용한-실시간-순위표-최적화&quot;&gt;2.3.2. Redis Sorted Set을 활용한 실시간 순위표 최적화&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#2321-정렬-집합sorted-set과-스킵-리스트skip-list의-원리&quot;&gt;2.3.2.1. 정렬 집합(Sorted Set)과 스킵 리스트(Skip List)의 원리&lt;/a&gt;
                &lt;ul&gt;
                  &lt;li&gt;&lt;a href=&quot;#스킵-리스트에서-노드-사이의-거리가-n-1이-될-때-더-이상-색인을-추가하지-않는-이유는&quot;&gt;💡스킵 리스트에서 노드 사이의 거리가 n-1이 될 때 더 이상 색인을 추가하지 않는 이유는?&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
              &lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#2322-redis-sorted-set-핵심-명령어&quot;&gt;2.3.2.2. Redis Sorted Set 핵심 명령어&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#2323-sorted-set-기반-실시간-순위표-처리-프로세스&quot;&gt;2.3.2.3. Sorted Set 기반 실시간 순위표 처리 프로세스&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#233-저장소-요구사항-및-용량-산정&quot;&gt;2.3.3. 저장소 요구사항 및 용량 산정&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#점수는-왜-16비트일까&quot;&gt;💡점수는 왜 16비트일까?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#상위-10명이-계속-바뀔텐데-프로필-캐시가-효과적일까&quot;&gt;💡상위 10명이 계속 바뀔텐데 ‘프로필 캐시’가 효과적일까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-대규모-확장을-위한-상세-설계&quot;&gt;3. 대규모 확장을 위한 상세 설계&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-클라우드-서비스-활용-및-인프라-구성&quot;&gt;3.1. 클라우드 서비스 활용 및 인프라 구성&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#311-자체-서비스를-이용하는-방안&quot;&gt;3.1.1. 자체 서비스를 이용하는 방안&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#312-클라우드-서비스를-이용하는-방안서버리스-아키텍처&quot;&gt;3.1.2. 클라우드 서비스를 이용하는 방안(서버리스 아키텍처)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#api-gateway에-lambda-말고-ec2나-k8s-pod도-연결할-수-있을까&quot;&gt;💡API Gateway에 Lambda 말고 EC2나 k8s pod도 연결할 수 있을까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-redis-규모-확장&quot;&gt;3.2. Redis 규모 확장&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#321-redis-데이터-샤딩-방안&quot;&gt;3.2.1. Redis 데이터 샤딩 방안&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#322-고정-파티션sticky-partition&quot;&gt;3.2.2. 고정 파티션(Sticky Partition)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#샤드-간-유저-이동-시-데이터-일관성-이슈&quot;&gt;💡샤드 간 유저 이동 시 데이터 일관성 이슈&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#redis의-info-keyspace-란&quot;&gt;💡Redis의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INFO keyspace&lt;/code&gt; 란?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#redis의-고정-파티션범위-샤딩은-redis-cluster를-사용하지-않는걸까&quot;&gt;💡Redis의 고정 파티션(범위 샤딩)은 Redis Cluster를 사용하지 않는걸까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#323-해시-파티션hash-partition-방식과-scatter-gather-패턴&quot;&gt;3.2.3. 해시 파티션(Hash Partition) 방식과 Scatter-Gather 패턴&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#redis-clster는-왜-하필-16384214개-슬롯을-사용할까&quot;&gt;💡Redis Clster는 왜 하필 16,384(\(2^{14}\))개 슬롯을 사용할까?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#crc16cyclic-redundancy-check-이란&quot;&gt;💡CRC16(Cyclic Redundancy Check) 이란?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#실무에서는-고정-파티션과-해시-파티션-중-무엇을-더-많이-사용할까&quot;&gt;💡실무에서는 고정 파티션과 해시 파티션 중 무엇을 더 많이 사용할까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#324-레디스-노드-크기-조정-및-벤치마킹&quot;&gt;3.2.4. 레디스 노드 크기 조정 및 벤치마킹&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#redis-benchmark란&quot;&gt;💡&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redis-benchmark&lt;/code&gt;란?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-대안-nosql-데이터베이스dynamodb&quot;&gt;3.3. 대안: NoSQL 데이터베이스(DynamoDB)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#331-비정규화-테이블의-한계와-핫-파티션hot-partition-문제&quot;&gt;3.3.1. 비정규화 테이블의 한계와 핫 파티션(Hot Partition) 문제&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#gsiglobal-secondary-index란&quot;&gt;💡GSI(Global Secondary Index)란?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#gsi는-어떻게-파티션-키와-정렬-키를-사용하는-걸까&quot;&gt;💡GSI는 어떻게 파티션 키와 정렬 키를 사용하는 걸까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#332-쓰기-샤딩write-sharding-패턴과-파티션-개수-트레이드오프&quot;&gt;3.3.2. 쓰기 샤딩(Write Sharding) 패턴과 파티션 개수 트레이드오프&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#쓰기-샤딩write-sharding이란&quot;&gt;💡쓰기 샤딩(Write Sharding)이란?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#벤치마킹을-통해-파티션-수의-최적값을-파악하는-방법은&quot;&gt;💡벤치마킹을 통해 파티션 수의 최적값을 파악하는 방법은?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#333-nosql-환경에서의-상대적-순위-계산-백분위수percentile-활용&quot;&gt;3.3.3. NoSQL 환경에서의 상대적 순위 계산: 백분위수(Percentile) 활용&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-운영-이슈-및-고도화-방안&quot;&gt;4. 운영 이슈 및 고도화 방안&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-redis-hash를-활용한-조회-최적화-및-동점자-순위-판정-방안&quot;&gt;4.1. Redis Hash를 활용한 조회 최적화 및 동점자 순위 판정 방안&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#redis-hash란&quot;&gt;💡Redis Hash란?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#42-시스템-장애-복구-전략&quot;&gt;4.2. 시스템 장애 복구 전략&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-요약-및-결론&quot;&gt;5. 요약 및 결론&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;온라인 게임의 리더보드(순위표)는 토너먼트가 경쟁전에서 선두를 달리는 플레이어가 누구인지 보여주며, 유저에게 승부욕과 지속적인 플레이 동기를 부여한다.&lt;/p&gt;

&lt;p&gt;실시간 순위표는 최상위 권 플레이어의 순위뿐 아니라 &lt;strong&gt;순위표를 조회하는 현재 유저 자신의 순위와 앞뒤 순위 유저들의 정보&lt;/strong&gt;까지 실시간으로 정확히 보여줄 수 있어야 한다.&lt;/p&gt;

&lt;p&gt;이번 포스트에서는 수백만 DAU 규모를 지탱할 수 있는 대규모 실시간 리더보드 시스템 설계 과정을 요구사항 정의부터 살펴본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-문제-이해-및-요구사항-정의&quot;&gt;1. 문제 이해 및 요구사항 정의&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;점수 계산 방식&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 순위표의 점수는 어떻게 계산하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 게임에서 승리하면 포인트를 얻으며, 이 포인트로 점수를 계산한다. 경기에서 이길 때마다 1점의 포인트를 추가로 획득하게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;대상 플레이어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 모든 플레이어가 순위표에 포함되어야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 그렇다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;유효 기간&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 한 순위표는 얼마 동안이나 유효한가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 매달 새로운 토너먼트를 시작할 때마다 새로운 순위표를 만든다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;표시 범위&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 상위 10명의 사용자만 신경써도 되는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 상위 10명의 사용자와 특정 사용자의 순위를 순위표에 표시할 수 있어야 한다. 가능하면 해당 사용자의 4순위 위(상위) 또는 아래(하위)에 있는 사용자들까지 함께 반환한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;사용자 규모&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 토너먼트에 참가하는 사용자는 몇 명인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 평균 일간 활성 사용자 수(DAU)는 500만 명, 월간 활성 사용자 수(MAU)는 2,500만 명으로 가정한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;경기 빈도&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 토너먼트 기간 동안 평균 몇 경기가 진행되는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 각 선수는 하루 평균 10경기를 치른다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;동점자 처리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 두 플레이어가 점수가 같은 경우 어떻게 순위를 결정하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 이 경우 두 사람은 동점이다. 가능하다면 동점자 사이의 순위를 가르는 방법도 마련하면 좋다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;실시간성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 순위표가 실시간이어야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 그렇다 누적된 결과 이력을 주기적으로 보여주는 방식은 바람직하지 않다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-기능-요구사항&quot;&gt;1.1 기능 요구사항&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;순위표에 상위 10명의 플레이어를 표시한다.&lt;/li&gt;
  &lt;li&gt;특정 사용자의 현재 순위를 표시한다.&lt;/li&gt;
  &lt;li&gt;특정 사용자를 기준으로 위로 4명, 아래로 4명의 순위/플레이어 정보를 표시한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;12-비기능-요구사항&quot;&gt;1.2. 비기능 요구사항&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;실시간성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;점수 업데이트는 실시간으로 순위표에 반영되어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;확장성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;트래픽 급증에도 지연 없이 순위를 조회/갱신할 수 있어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;고가용성 및 안정성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시스템 일부에 장애가 발생해도 서비스가 중단되지 않아야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;13-qps-및-트래픽-산정&quot;&gt;1.3. QPS 및 트래픽 산정&lt;/h2&gt;

&lt;p&gt;트래픽을 수치화하여 백엔드 인프라 수용량을 추정해본다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;기본 조건&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;DAU(Daily Active Users): 5,000,000명(500만 명)&lt;/li&gt;
      &lt;li&gt;하루 = 86,400 \(\approx 10^5\text{초}\)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;트래픽 산정&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;게임을 하는 사용자가 24시간 동안 완벽히 고르게 분포한다고 가정하면 평균 초당 사용자 수는 다음과 같다.&lt;/p&gt;

\[\text{평균 초당 접속 유저} = \frac{5,000,000 \text{ DAU}}{10^5 \text{초}} = 50 \text{ users/sec}\]

&lt;p&gt;하지만 실제로 트래픽은 절대로 균등하게 분산되지 않는다.&lt;br /&gt;
특정 시간대에 트래픽이 몰리는 피크 타임이 존재하며, 서로 다른 시간대의 사람들이 동시에 게임을 할 수 있는 북미 지역 기준 저녁 시간이 그 피크 시간대일 확률이 높다.&lt;br /&gt;
여기서는 &lt;strong&gt;최대 부하(Peak Load)를 평균의 5배&lt;/strong&gt;라고 가정한다.&lt;/p&gt;

\[\text{피크 타임 초당 접속 유저} = 50 \times 5 = 250 \text{ users/sec}\]

&lt;hr /&gt;

&lt;h3 id=&quot;북미-지역-기준-저녁-피크-타임이-갖는-의미는-무엇일까&quot;&gt;💡북미 지역 기준 저녁 피크 타임이 갖는 의미는 무엇일까?&lt;/h3&gt;

&lt;p&gt;북미(미국/캐나다) 대륙은 동부(EST), 중부(CST), 산악(MST), 태평양(PST) 등 &lt;strong&gt;4개의 주요 시차(총 3시간 차이)&lt;/strong&gt;가 존재한다.&lt;br /&gt;
한국이나 단일 시차 국가는 저녁 8시~10시 사이에 피크 타임이 찍히고 빠르게 지나가지만, 북미는 동부 저녁 8시(태평양 오후 5시)부터 시작되어 태평양 저녁 9시(동부 자정)까지 
&lt;strong&gt;약 4~5시간 동안 북미 대륙 전역의 저녁 퇴근/방학 시간대가 연속적으로 겹치는 거대한 ‘합성 피크 타임 구간’&lt;/strong&gt;이 형성된다.&lt;/p&gt;

&lt;p&gt;또한 글로벌 단일 서버/지역 서비스를 운영할 경우, 가장 많은 플레이어 지출과 동시 접속자가 발생하는 북미 저녁 시간대는 인프라의 최대 수용량을 결정짓는 ‘글로벌 최고 피크 타임’의 대명사로 
자주 인용된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;QPS(Query Per Second) 및 TPS 산정&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) 사용자 점수 획득 이벤트 TPS&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;한 사용자가 하루 평균 10개의 게임을 플레이한다.&lt;/li&gt;
  &lt;li&gt;점수 획득 TPS는 다음과 같이 계산할 수 있다.&lt;/li&gt;
&lt;/ul&gt;

\[\text{점수 획득 피크 TPS} = 250 \text{ users/sec} \times 10 \text{ games/day} = 2,500 \text{ TPS}\]

&lt;p&gt;&lt;strong&gt;2) 상위 10명 순위표 조회 QPS&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;각 사용자가 하루에 한 번 게임을 열고, 상위 10명의 순위표는 사용자가 처음 게임을 열 때만 표시한다고 가정한다.&lt;/li&gt;
  &lt;li&gt;따라서 상위 10명 조회 피크 QPS는 다음과 같다.&lt;/li&gt;
&lt;/ul&gt;

\[\text{상위 10명 조회 QPS} = \frac{5,000,000 \text{ users}}{10^5 \text{초}} = 50 \text{ QPS}\]

&lt;hr /&gt;

&lt;h1 id=&quot;2-개략적-시스템-설계-및-데이터-흐름&quot;&gt;2. 개략적 시스템 설계 및 데이터 흐름&lt;/h1&gt;

&lt;p&gt;실시간 게임 순위표 시스템은 크게 &lt;strong&gt;게임 진행을 담당하는 게임 서비스&lt;/strong&gt;와 &lt;strong&gt;점수 및 순위를 관리하는 순위표 서비스&lt;/strong&gt;로 역할이 분리된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-api-설계&quot;&gt;2.1. API 설계&lt;/h2&gt;

&lt;p&gt;클라이언트 및 게임 서버가 클라이언트와 통신하기 위한 RESTful API 명세이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1) POST /v1/scores&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;역할:&lt;/strong&gt; 사용자가 게임에서 승리했을 때 순위표 상의 사용자 점수를 갱신한다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;보안 법칙:&lt;/strong&gt; 이 API는 게임 서버 내부에서만 호출할 수 있는 &lt;strong&gt;내부 API&lt;/strong&gt;이다. 클라이언트(앱/웹)가 이 API를 직접 호출하여 점수를 주작하거나 업데이트할 수 없다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;요청 인자(Request Body)&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;필드&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;타입&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;설명&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;String&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;게임에서 승리한 사용자 식별자&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;points&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Integer&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;사용자가 게임 승리로 획득한 포인트 수&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;응답(Response)&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;상태 코드&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;설명&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;200 OK&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;사용자 점수를 성공적으로 갱신함&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;400 Bad Request&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;잘못된 인자가 전달되어 점수를 갱신하지 못함&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2) GET /v1/scores&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;역할:&lt;/strong&gt; 순위표에서 상위 10명의 플레이어 정보를 가져온다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;응답 예시(Response)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;data&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;aaa&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;assu&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;rank&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;999&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;bbb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;silby&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;rank&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;996&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;total&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;3) GET v1/scores/{:user_id}&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;역할:&lt;/strong&gt; 특정 사용자의 현재 순위 및 점수 정보를 가져온다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;파라미터(Path Parameter)&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;필드&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;설명&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;user_id&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;순위 정보를 가져올 대상 사용자 ID&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;응답 예시(Response)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user_info&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;aaa&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;score&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;999&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;rank&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-개략적-아키텍처와-서비스-분리&quot;&gt;2.2. 개략적 아키텍처와 서비스 분리&lt;/h2&gt;

&lt;p&gt;아래 다이어그램은 게임 서비스와 순위표 서비스의 역할 분담을 나타낸 개략적인 아키텍처이다.&lt;br /&gt;
게임 서비스는 사용자가 게임을 플레이할 수 있는 서비스이고, 순위표 서비스는 순위표를 생성하고 표시한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/overall.png&quot; alt=&quot;개략적 설계&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;① 승리 요청&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 게임에서 승리하면 클라이언트는 &lt;strong&gt;게임 서비스&lt;/strong&gt;로 승리 결과를 전송한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;② 검증 및 점수 갱신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;게임 서비스는 해당 승리가 정상적인 플레이인지 검증한 후, &lt;strong&gt;순위표 서비스&lt;/strong&gt; 내부의 API(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/scores&lt;/code&gt;)를 호출한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;③ 저장소 반영&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;순위표 서비스는 순위표 저장소(Leaderboard Store)에 해당 사용자의 점수를 갱신한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;④ 순위 조회&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클라이언트는 순위표가 필요할 때 &lt;strong&gt;순위표 서비스&lt;/strong&gt;로 직접 요청을 보내 (a)상위 10명 순위표, (b)자기 자신의 순위 정보를 가져온다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;최종적으로 이 설계안을 택하기 전에 다른 대안도 고려했지만 채택하지 않은 이유에 대해서 살펴본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;221-대안1-클라이언트가-순위표-서비스와-직접-통신해야-할까&quot;&gt;2.2.1. 대안1: 클라이언트가 순위표 서비스와 직접 통신해야 할까?&lt;/h3&gt;

&lt;p&gt;클라이언트가 순위표 서비스에 직접 점수를 업데이트하는 방식을 사용해서는 안된다.&lt;/p&gt;

&lt;p&gt;클라이언트와 순위표 서비스가 직접 통신할 경우, 사용자가 Proxy 툴(예: Fiddler, Charles)을 중간에 설치하여 요청 패킷을 위변조하는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Man-in-the-middle_attack&quot;&gt;중간자 공격(Man-In-The-Middle-Attack)&lt;/a&gt;에 무방비로 노출된다.&lt;/p&gt;

&lt;p&gt;따라서 점수 설정 권한은 반드시 서버(게임 서비스)에 두어야 한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/who.png&quot; alt=&quot;순위표 점수는 누가 설정할까?&quot; /&gt;&lt;/p&gt;

&lt;p&gt;온라인 포커처럼 서버가 게임 전반을 통솔하는 경우에는 클라이언트가 점수를 설정하기 위해 게임 서버를 명시적으로 호출할 필요가 없을수도 있다.&lt;br /&gt;
게임 서버가 모든 게임 로직을 처리하고 게임이 언제 끝나는지 알기 때문에 클라이언트의 개입 없이도 점수를 정할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;다른-게임들도-게임-서비스가-게임이-언제-끝나는지-아는-것-아닐까&quot;&gt;💡다른 게임들도 게임 서비스가 게임이 언제 끝나는지 아는 것 아닐까?&lt;/h3&gt;

&lt;p&gt;모든 게임이 게임 서버에서 완벽히 통제되는 것은 아니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;클라이언트 완결형 게임(예: 싱글 퍼블, 캐주얼 오프라인 게임)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;게임의 플레이 과정과 종료 판정이 유저의 스마트폰(클라이언트) 내부에서 실행된다.&lt;/li&gt;
      &lt;li&gt;게임이 끝난 시점에 클라이언트가 서버로 ‘나 100점 얻었어’라고 결과만 통보하는 구조이다.&lt;/li&gt;
      &lt;li&gt;이 경우 서버는 게임 진행 상황을 알 수 없으므로 클라이언트의 전달에 의존하므로 위변조에 매우 취약하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;서버 통솔형 게임(예: 온라인 포커, 체스)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;중앙 게임 서버에서 모든 카드 덱, 기물의 이동, 세션 종료 시점을 직접 계산한다.&lt;/li&gt;
      &lt;li&gt;클라이언트는 단순 입력 전달 장치일 뿐이며, 게임이 언제 끝났는지 서버가 100% 실시간으로 파악하고 결과를 직접 반영한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;222-대안-2-게임-서비스와-순위표-서버-사이에-메시지-큐가-필요한가&quot;&gt;2.2.2. 대안 2: 게임 서비스와 순위표 서버 사이에 메시지 큐가 필요한가?&lt;/h3&gt;

&lt;p&gt;해당 데이터가 순위표 외에 분석, 푸시 알림 등 다른 서비스에서도 동시에 사용되어야 한다면 카프카와 같은 메시지 큐를 사이에 두는 것이 합리적이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/option1.png&quot; alt=&quot;게임 점수를 여러 서비스에서 사용하는 방안&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-데이터-모델-rdb-vs-redis-sorted-set&quot;&gt;2.3. 데이터 모델: RDB vs Redis Sorted Set&lt;/h2&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;231-rdb의-한계와-sql-쿼리-분석&quot;&gt;2.3.1. RDB의 한계와 SQL 쿼리 분석&lt;/h3&gt;

&lt;p&gt;규모 확장성이 그다지 중요하지 않고 사용자 수가 많지 않다면 RDB를 이용할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;rdb는-왜-규모-확장성이-좋지-않을까&quot;&gt;💡RDB는 왜 규모 확장성이 좋지 않을까?&lt;/h4&gt;

&lt;p&gt;RDB는 디스크 기반 B-Tree 인덱스를 사용한다.&lt;br /&gt;
실시간으로 수천 명의 점수가 바뀔 때마다 B-Tree 인덱스 재배치(Page Split)와 디스크 I/O가 발생하여 쓰기 성능이 급격히 저하된다.&lt;/p&gt;

&lt;p&gt;또한, N명의 전체 데이터를 정렬하고 순위를 계산하는 연산은 \(O(N \log N)\) 또는  \(O(N)\)의 시간 복잡도를 가지므로,&lt;br /&gt;
데이터가 수백만 건을 넘어서면 CPU와 메모리가 고갈되어 Scale-out(Sharding)이 극도로 어려워진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;순위표 DB 테이블:&lt;/strong&gt;
&lt;img src=&quot;/assets/img/dev/2026/0801/leader.png&quot; alt=&quot;leaderboard 테이블&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;사용자가 점수를 획득한 경우:&lt;/strong&gt;
&lt;img src=&quot;/assets/img/dev/2026/0801/user.png&quot; alt=&quot;사용자가 점수를 획득한 경우&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;특정 사용자 순위 검색:&lt;/strong&gt;
&lt;img src=&quot;/assets/img/dev/2026/0801/specific.png&quot; alt=&quot;사용자의 순위 검색&quot; /&gt;&lt;/p&gt;

&lt;p&gt;사용자 순위를 가져오려면 순위표 테이블을 점수 기준으로 정렬한 후 순위를 매긴다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rownum&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rownum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;score&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;leaderboard&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;DESC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;위 질의의 실행 결과는 아래와 같다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;rank&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;user_id&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;score&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;aaa&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;987&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;bbb&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;902&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ccc&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;870&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;4&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ddd&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;850&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;데이터가 몇천 건 수준일 때는 이 쿼리가 문제없이 작동하지만, 레코드가 수백만 건 이상으로 늘어나면 성능이 심각하게 저하된다.&lt;/p&gt;

&lt;p&gt;유저의 순위를 정확히 찾으려면 수백만 개의 행 전체를 디스크에서 읽어와 정렬해야 한다.&lt;br /&gt;
지속적으로 점수가 변하는 대량의 데이터를 RDB가 매번 실시간으로 정렬하는 것은 불가능에 가깝다.&lt;br /&gt;
수백만 건을 정렬하는데 수십 초 이상 걸리기 때문에 실시간 응답이 불가능하며, 점수가 계속 변하므로 정렬 결과를 캐시에 보관하기로 어렵다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;즉, RDBMS는 여기서 요구하는 다량의 읽기 부하를 처리하기 어렵다.  일괄 작업으로 하면 가능하겠지만, 이는 실시간 순위를 보여주어야 한다는 요구 사항에 적합하지 않다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LIMIT&lt;/code&gt;절을 이용해 상위 10명만 가져오는 최적화를 적용할 수는 있다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rownum&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rownum&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;score&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;leaderboard&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;DESC&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 이 역시 전체 확장성 문제를 해결해주지 못한다.&lt;br /&gt;
상위 10명은 제한할 수 있어도, 순위표 하단에 위치한 &lt;strong&gt;특정 일반 유저의 현재 순위&lt;/strong&gt;를 알아내려면 결국 전체 테이블을 스캔해야 하기 때문이다.&lt;/p&gt;

&lt;p&gt;특정 유저의 순위를 구하기 위해 아래와 같이 상관 서브쿼리(Correlated Subquery)를 실행하는 방안도 있다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;COUNT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;leaderboard&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lb2&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lb2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;score&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lb1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AS&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rank&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;leaderboard&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lb1&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;lb1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 쿼리는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lb1&lt;/code&gt;에 존재하는 대상 유저의 점수보다 높거나 같은 점수를 가진 행의 개수를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lb2&lt;/code&gt; 테이블 전체에서 매번 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COUNT(*)&lt;/code&gt;로 세어오는 방식이다.&lt;br /&gt;
전체 유저가 100만 명이라면 유저 1명의 순위를 조회하기 위해 100만 번의 비교 스캔 연산이 수행되어 \(O(N^2)\) 수준의 최악의 지연 시간이 발생한다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Big-O(빅오) 표기법에 대한 내용은 &lt;a href=&quot;https://assu10.github.io/dev/2026/08/08/big-o/&quot;&gt;Big-O(빅오) 표기법&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;상관-서브쿼리correlated-subquery란&quot;&gt;💡상관 서브쿼리(Correlated Subquery)란?&lt;/h4&gt;

&lt;p&gt;서브쿼리가 단독으로 실행되지 못하고, 외부 쿼리의 각 행을 바인딩받아 실행되는 의존적인 구조를 가지는(=외부 쿼리와 연관되어 있는) 쿼리를 말한다.&lt;br /&gt;
위 쿼리의 경우 내부 서브쿼리가 외부 쿼리의 컬럼 값인 lb1.score를 참조해서 동작하고 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;232-redis-sorted-set을-활용한-실시간-순위표-최적화&quot;&gt;2.3.2. Redis Sorted Set을 활용한 실시간 순위표 최적화&lt;/h3&gt;

&lt;p&gt;수백만 명의 유저 환경에서도 일관되게 빠른 속도를 보장하고 복잡한 쿼리 없이 정렬을 처리할 수 있는 최고의 솔루션은 &lt;strong&gt;Redis&lt;/strong&gt;이다.&lt;br /&gt;
Redis는 모든 데이터를 메모리에 보관하는 In-Memory 저장소로, 실시간 리더보드 구현에 완벽히 부합하는 &lt;strong&gt;정렬 집합(Sorted Set)&lt;/strong&gt; 자료구조를 기본으로 제공한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2321-정렬-집합sorted-set과-스킵-리스트skip-list의-원리&quot;&gt;2.3.2.1. 정렬 집합(Sorted Set)과 스킵 리스트(Skip List)의 원리&lt;/h4&gt;

&lt;p&gt;&lt;a href=&quot;https://assu10.github.io/dev/2022/07/30/redis-zset/&quot;&gt;Sorted Set&lt;/a&gt;은 집합(Set)처럼 각 원소(Member)가 중복되지 않는 고유한 값을 가지면서, 각 원소마다 점수(Score)가 연결되어 있어 점수를 기준으로 자동 정렬되는 자료형이다.&lt;/p&gt;

&lt;p&gt;Redis Sorted Set은 내부적으로 &lt;a href=&quot;https://github.com/redis/redis/blob/unstable/src/t_zset.c&quot;&gt;해시 테이블(Hash Table)과 스킵 리스트(Skip List)라는 두 가지 자료구조&lt;/a&gt;를 조합하여 동작한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;해시 테이블&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;\(O(1)\) 시간 복잡도로 유저의 현재 점수를 즉시 찾아낸다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;스킵 리스트&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;\(O(log N)\) 시간 복잡도로 점수순 정렬 상태를 유지하며 특정 순위 위치를 검색한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;아래는 Sorted Set을 이해하기 위한 그림이다.&lt;br /&gt;
Score 및 Member 열이 있는 테이블로 이해하면 되며, 이 테이블은 Score의 내림차순으로 정렬된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/sort.png&quot; alt=&quot;Sorted Set으로 표현한 2월 순위표&quot; /&gt;&lt;/p&gt;

&lt;p&gt;스킵 리스트는 정렬된 단방향 연결 리스트(Singly-lined List)에 다단계 색인(Multi-level Index)을 얹은 자료 구조이다.&lt;br /&gt;
기본 연결 리스트에서 특정 노드를 찾으려면 처음부터 끝까지 순회해야 하므로 시간 복잡도는 \(O(N)\)이 걸린다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/skip.png&quot; alt=&quot;스킵 리스트&quot; /&gt;&lt;/p&gt;

&lt;p&gt;검색 속도를 정렬된 배열의 이진 탐색 수준으로 높이기 위해 중간 노드들을 건너뛰는 1차 색인, 2차 색인을 단계적으로 쌓아올린다.&lt;br /&gt;
새로운 색인이 추가될 때마다 이전 레이어 노드를 하나씩 건너뛰어 탐색 속도를 비약적으로 향상시킨다.&lt;/p&gt;

&lt;p&gt;아래 그림처럼 5차 색인까지 구축된 스킵 리스트에서는, 기본 리스트만 이용할 경우 62개 노드를 일일이 거쳐야 했던 목적지에 단 11번의 이동만으로 다다를 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/index.png&quot; alt=&quot;5차 색인까지 사용하는 스킵 리스트&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h5 id=&quot;스킵-리스트에서-노드-사이의-거리가-n-1이-될-때-더-이상-색인을-추가하지-않는-이유는&quot;&gt;💡스킵 리스트에서 노드 사이의 거리가 n-1이 될 때 더 이상 색인을 추가하지 않는 이유는?&lt;/h5&gt;

&lt;p&gt;스킵 리스트에서 1차, 2차.. 계속 위로 색인 레이어를 올리다 보면, 최상단 레벨 색인은 단 2개의 노드(첫 번째와 마지막 노드)만 연결하게 된다.&lt;br /&gt;
이 때 최상단 색인 노드가 한 번에 점프하여 건너뛰는 물리적 노드의 간격이 전체 리스트 개수 n에 해당하는 n-1개의 노드 전체가 된다는 뜻이다.&lt;/p&gt;

&lt;p&gt;즉, &lt;strong&gt;더 이상 상위 색인 레이어를 쌓을 수 없는 최상단 헤드 색인 층에 도달했다.&lt;/strong&gt;는 의미이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2322-redis-sorted-set-핵심-명령어&quot;&gt;2.3.2.2. Redis Sorted Set 핵심 명령어&lt;/h4&gt;

&lt;blockquote&gt;
  &lt;p&gt;실시간 리더 보드에 Redis Sorted Set을 활용하는 예시는 아래를 참고하세요.&lt;/p&gt;

  &lt;p&gt;&lt;a href=&quot;https://medium.com/@sandeep4.verma/building-real-time-leaderboard-with-redis-82c98aa47b9f&quot;&gt;Building real-time Leaderboard with Redis&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://aws.amazon.com/ko/blogs/database/building-a-real-time-gaming-leaderboard-with-amazon-elasticache-for-redis/&quot;&gt;Build a real-time gaming leaderboard with Amazon ElastiCache for Redis&lt;/a&gt;&lt;br /&gt;
&lt;a href=&quot;https://levelup.gitconnected.com/how-we-created-a-real-time-leaderboard-for-a-million-users-555aaa3ccf7b&quot;&gt;How we created a real-time Leaderboard for a million Users&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZADD&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;순위표에 유저를 신규 추가하거나 기존 유저의 점수를 갱신한다. 실행 소요 시간은 \(O(log(N))\)이다.&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZADD key [NX|XX] [GT|LT] [CH] [INCR] score member [score member ...]&lt;/code&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZINCRBY&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;유저의 점수를 지정한 값만큼 가산/감산한다. 실행에 소요되는 시간은 O\((log(N))\)이다.&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZINCRBY key increment member&lt;/code&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZRANGE&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZREVRANGE&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;오름차순 또는 내림차순 정렬 기준 특정 범위의 유저 목록을 가져온다. \(O(log(N) + M)\), M은 가져올 항목 수, N은 Sorted Set의 크기&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZRANGE key min max [BYSCORE|BYLEX] [REV] [LIMIT offset count] [WITHSCORES]&lt;/code&gt;&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZREVRANGE key start stop [WITHSCORES]&lt;/code&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZRANK&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZREVRANK&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;오름차순 또는 내림차순 정렬 기준 특정 유저의 순위 인덱스(O-indexed)를 가져온다. 실행 시간은 \(O(log(N))\)이다.&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZRANK key member&lt;/code&gt;&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZREVRANK key member&lt;/code&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2323-sorted-set-기반-실시간-순위표-처리-프로세스&quot;&gt;2.3.2.3. Sorted Set 기반 실시간 순위표 처리 프로세스&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;1) 유저가 게임에서 승리하여 점수를 획득한 경우&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;매월 신규 순위표 키(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;leaderboard_feb_2021&lt;/code&gt;)를 생성해 운영한다.&lt;br /&gt;
유저가 승리하여 1점을 얻으면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZINCRBY&lt;/code&gt;를 호출한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/user2.png&quot; alt=&quot;사용자가 점수를 획득한 경우&quot; /&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;ZINCRBY leaderboard_feb_2021 1 &apos;assu&apos;
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2) 상위 10명 순위표를 조회하는 경우&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;점수가 가장 높은 순서대로 1위부터 10위까지 가져오기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZREVRANGE&lt;/code&gt; 명령을 사용하며, 유저 ID와 점수를 함께 받아오기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WITHSCORES&lt;/code&gt; 옵션을 붙인다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/order.png&quot; alt=&quot;순위표 상위 10명 조회&quot; /&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;ZREVRANGE leaderboard_feb_2021 0 9 WITHSCORES 
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;반환 예시:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;[(aaa, 998), (silby, 996), ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;3) 유저 본인이 현재 순위를 조회하는 경우&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;내림차순 정렬 기준 유저의 순위를 알기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZREVRANK&lt;/code&gt;를 호출한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/myself.png&quot; alt=&quot;특정 사용자의 순위 조회&quot; /&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;ZREVRANK leaderboard_feb_2021 &apos;assu&apos;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;반환 예시:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;4) 유저 본인 기준 인접 순위(위로 4명, 아래로 4명)를 조회하는 경우&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mallow007&lt;/code&gt; 유저의 현재 순위 인덱스가 361이라면, 357부터 365까지의 인덱스 범위를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZREVRANGE&lt;/code&gt;로 지정하여 한 번에 받아온다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/ordering.png&quot; alt=&quot;특정 사용자 직전 순위 사용자 4명, 직후 순위 사용자 4명&quot; /&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-redis&quot;&gt;ZREVRANGE leaderboard_feb_2021 357 365
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;233-저장소-요구사항-및-용량-산정&quot;&gt;2.3.3. 저장소 요구사항 및 용량 산정&lt;/h3&gt;

&lt;p&gt;시스템 운영에 필요한 메모리 용량을 추정해보자.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;최악의 상황 가정&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;MAU 2,500만 명 전원이 최소 1회 이상 승리하여 월 순위표 집합에 모두 등재되는 경우&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 단위 용량&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저 ID(24바이트 문자열) + 점수(2바이트 = 16비트 정수) = &lt;strong&gt;26바이트&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;전체 순수 데이터 용량&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

\[26 \text{ 바이트} \times 25,000,000 \text{ 명} = 650,000,000 \text{ 바이트} \approx 650 \text{ MB}\]

&lt;hr /&gt;

&lt;h4 id=&quot;점수는-왜-16비트일까&quot;&gt;💡점수는 왜 16비트일까?&lt;/h4&gt;

&lt;p&gt;점수의 최대 한계치가 0~65,535 범위 이내인 게임 규칙을 가정하여 최소 단위 예시(16비트 = 2바이트)로 제시된 값이다.&lt;br /&gt;
만약 점수가 수억 점대라면 32비트(4바이트)나 64비트(8바이트) 정수/실수형을 사용하면 되며, 이 경우에도 전체 메모리 증가량은 수백 MB 수준에 그쳐 단일 서버 수용 범위 내에 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;스킵 리스트 포인터 오버헤드와 해시 테이블 메타데이터를 감안해 메모리 사용량을 2배(약 1.3GB)로 넉넉히 잡아도, 
&lt;strong&gt;단일 레디스 서버 1대만으로 2,500만 유저의 전체 순위표를 RAM에 여유 있게 탑재&lt;/strong&gt;할 수 있다.&lt;/p&gt;

&lt;p&gt;CPU 및 I/O 관점에서도 피크 타임 점수 갱신 트래픽이 초당 2,500 TPS 수준이므로, 초당 5만~10만 TPS 이상을 처리하는 단일 Redis 노드로 충분히 감당 가능하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;영속성에 대한 부분을 살펴보자.&lt;/p&gt;

&lt;p&gt;레디스는 데이터를 디스크에 영속적으로 보관하는 옵션도 지원하지만, 디스크에서 데이터를 읽어 대규모 레디스 인스턴스를 재시작하려면 시간이 오래 걸린다.&lt;br /&gt;
메모리 스펙과 디스크 I/O 속도에 따라 다르겠지만, 보통 수 GB~수십 GB의 RDB 파일 스냅숏을 디스크에서 메모리로 로딩할 때 초당 100MB~500MB 복원 속도가 나온다.&lt;/p&gt;

&lt;p&gt;따라서 대략 &lt;strong&gt;수초에서 수분 이상&lt;/strong&gt; 소요될 수 있으며, 로딩이 완료될 때까지 Redis 인스턴스가 요청을 처리하지 못하므로 서비스 지연이 발생할 수 있다.&lt;/p&gt;

&lt;p&gt;그래서 실무에서는 디스크 복구 대신 고가용성을 위해 &lt;strong&gt;복제본(Read Replica)을 주 서버로 승격(Failover)&lt;/strong&gt;시키는 방식을 사용한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;MySQL과 같은 관계형 데이터베이스를 사용하는 경우에는 2개의 테이블인 사용자 테이블과 점수 테이블이 필요하다.&lt;br /&gt;
사용자 테이블에는 사용자 아이디와 사용자의 게임 내 이름을 저장하고, 점수 테이블에는 사용자 아이디, 점수, 승리 시각을 저장한다.&lt;/p&gt;

&lt;p&gt;한 가지 적용할 수 있는 성능 최적화 방안은 가장 자주 검색되는 상위 10명의 사용자 정보를 캐시하는 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;상위-10명이-계속-바뀔텐데-프로필-캐시가-효과적일까&quot;&gt;💡상위 10명이 계속 바뀔텐데 ‘프로필 캐시’가 효과적일까?&lt;/h4&gt;

&lt;p&gt;결론은 &lt;strong&gt;유저 순위가 계속 바뀌더라도 프로필 캐싱은 여전히 극적인 성능 최적화 효과를 발휘&lt;/strong&gt;한다.&lt;/p&gt;

&lt;p&gt;여기서 캐싱하는 대상은 실시간으로 시시각각 바뀌는 &lt;strong&gt;‘유저의 점수나 순위’&lt;/strong&gt;가 아니라, 유저의 &lt;strong&gt;‘정적 프로필 정보(유저 닉네임, 클랜 이름 등)’&lt;/strong&gt;이다.&lt;br /&gt;
Redis Sorted Set에는 유저 ID와 점수만 저장해두고 빠르게 상위 10명의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;를 가져온 후, 해당 유저들의 프로필 정보는 DB 대신 캐시(Redis)에서 가져옴으로써 
DB 조회를 100% 차단하는 최적화 패턴이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;상위권 유저의 ‘집군(Locality) 효과’&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;전체 유저가 2,500만 명에 달하더라도, 리더보드 1~10위 사이를 두고 실시간으로 랭킹 다툼을 벌이는 유저 층은 대개 &lt;strong&gt;상위 50~100명 이내의 한정된 헤비 유저군&lt;/strong&gt;이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;개별 유저 단위 식별자 캐싱(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user:profile:{user_id}&lt;/code&gt;)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;프로필 캐시는 ‘상위 10명 통짜 묶음’을 캐싱하는 것이 아니라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user:profile:aaa&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user:profile:bbb&lt;/code&gt; 와 같이 &lt;strong&gt;유저 개별 키 단위&lt;/strong&gt;로 레디스에 보관한다.&lt;/li&gt;
      &lt;li&gt;Redis ZSet에서 상위 10명의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt; 목록을 새로 뽑았을 때, 백엔드 서버는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MGET user:profile:aaa user:profile:bbb...&lt;/code&gt; 명령으로 10명의 프로필을 캐시에서 \(O(1)\)로 일괄 조회한다.&lt;/li&gt;
      &lt;li&gt;유저 조합이 어떻게 바뀌든 개별 프로필 캐시는 유효하므로 DB 조회가 발생하지 않는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;조회 트래픽(Read QPS)와 순위 변동(Rank Flip)의 압도적 비율 차이&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;상위 10명의 순위 주인이 뒤바뀌는 주기가 아무리 빠르더라도(예: 수 초~수 분에 1번), 그 짧은 순간 사이에도 일반 유저들이 순위표를 조회하는 횟수(Read QPS)는 수천~수만 건에 달한다.&lt;/li&gt;
      &lt;li&gt;단 1초 동안 상위 10명이 유지되는 사이, 피크 타입에 몰려드는 수천 건(약 2,000~2,500 QPS)의 동시 조회 요청이 DB로 가지 않고 프로필 캐시에서 즉시 리턴되므로, DB의 CPU와 I/O 부담을 차단해준다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-대규모-확장을-위한-상세-설계&quot;&gt;3. 대규모 확장을 위한 상세 설계&lt;/h1&gt;

&lt;p&gt;수백만 명에서 수억 명 규모의 유저를 지탱하기 위해 리더보드 시스템을 클라우드 환경으로 확장하고, Redis 샤딩 및 NoSQL 대안을 적용하는 상세 설계 방안을 살펴본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-클라우드-서비스-활용-및-인프라-구성&quot;&gt;3.1. 클라우드 서비스 활용 및 인프라 구성&lt;/h2&gt;

&lt;p&gt;리더보드 서비스 배포 아키텍처는 기업의 기존 인프라 환경에 따라 &lt;strong&gt;자체 구축(On-Premis) 방안&lt;/strong&gt;과 &lt;strong&gt;서버리스 클라우드(AWS) 방안&lt;/strong&gt;으로 나눌 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;311-자체-서비스를-이용하는-방안&quot;&gt;3.1.1. 자체 서비스를 이용하는 방안&lt;/h3&gt;

&lt;p&gt;이 접근 방식에서는 매월 새로운 Redis Sorted Set을 생성하여 해당 기간의 순위표를 저장하고, 이전 달의 데이터는 이력 DB로 이관한다.&lt;br /&gt;
유저의 ID와 점수는 Redis에 저장하고, 닉네임 및 프로필 이미지 같은 상세 정보는 MySQL RDB에 보관한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/itself.png&quot; alt=&quot;자체 서비스를 이용하는 방안&quot; /&gt;&lt;/p&gt;

&lt;p&gt;웹 서버는 Redis에서 순위 목록(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;score&lt;/code&gt;)를 읽어온 뒤, 화면에 표시할 유저의 닉네임과 프로필 이미지를 MySQL 또는 레디스 프로필 캐시에서 조회하여 조합한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;312-클라우드-서비스를-이용하는-방안서버리스-아키텍처&quot;&gt;3.1.2. 클라우드 서비스를 이용하는 방안(서버리스 아키텍처)&lt;/h3&gt;

&lt;p&gt;AWS의 &lt;strong&gt;API Gateway&lt;/strong&gt;와 &lt;strong&gt;&lt;a href=&quot;https://aws.amazon.com/ko/lambda/&quot;&gt;AWS Lambda&lt;/a&gt;, ElastiCache for Redis&lt;/strong&gt;를 조합하면 인프라 관리 부담 없이 유저 증가에 따라 자동으로 Auto-scaling되는 완전 관리형 순위표 시스템을 구축할 수 있다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;구글 클라우드는 &lt;a href=&quot;https://cloud.google.com/functions&quot;&gt;Cloud Function&lt;/a&gt;을,&lt;br /&gt;
MS는 &lt;a href=&quot;https://azure.microsoft.com/en-us/products/functions/&quot;&gt;Azure Function&lt;/a&gt; 이라는 서버리스를 제공한다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;API&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;람다 함수&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /v1/scores&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LeaderboardFetchTop10&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /v1/scores/{:user_Id}&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LeaderboardFetchPlayerRank&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/scores&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LeaderboardUpdateScore&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1) 점수 갱신 및 저장 흐름&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;점수 획득:&lt;/strong&gt;
&lt;img src=&quot;/assets/img/dev/2026/0801/acquisition.png&quot; alt=&quot;점수 획득&quot; /&gt;&lt;/p&gt;

&lt;p&gt;순위표 서비스가 API Gateway로 요청을 보내면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LeaderboardUpdateScore&lt;/code&gt; 람다 함수가 실행되어 Redis ElastiCache에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZINCRBY&lt;/code&gt; 명령을 수행함과 동시에, 
백업용 MySQL 테이블에 점수 기록을 저장한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2) 순위표 조회 흐름&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;순위 검색:&lt;/strong&gt;
&lt;img src=&quot;/assets/img/dev/2026/0801/search.png&quot; alt=&quot;순위 검색&quot; /&gt;&lt;/p&gt;

&lt;p&gt;클라이언트가 API Gateway를 통해 순위를 조회하면 람다 함수가 Redis에서 상위 10명/특정 유저 순위를 읽어온 뒤, 프로필 정보를 결합하여 응답한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;api-gateway에-lambda-말고-ec2나-k8s-pod도-연결할-수-있을까&quot;&gt;💡API Gateway에 Lambda 말고 EC2나 k8s pod도 연결할 수 있을까?&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;당연히 가능&lt;/strong&gt;하다.&lt;/p&gt;

&lt;p&gt;AWS API Gateway는 Lambda 함수 호출 뿐만 아니라, &lt;strong&gt;HTTP/HTTPS 프록시 integration&lt;/strong&gt;을 지원한다.&lt;br /&gt;
따라서 로드 밸런서(ALB/NLB) 뒷단에 위치한 Amazon EC2 인스턴스 파밍 단지나 Amazon EKS(Kubernetes) Cluster 내의 Pod 서비스 엔드포인트로 API 요청을 직접 
라우팅할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-redis-규모-확장&quot;&gt;3.2. Redis 규모 확장&lt;/h2&gt;

&lt;p&gt;단일 Redis 인스턴스는 &lt;a href=&quot;#점수는-왜-16비트일까&quot;&gt;메모리 약 1.3GB&lt;/a&gt;, 피크 타임 2,500 TPS 수준인 &lt;strong&gt;500만 DAU&lt;/strong&gt; 환경을 1대의 노드로 충분히 처리할 수 있다.&lt;br /&gt;
500만 DAU의 전체 메모리 필요양은 약 1.3GB로 최근 서버 스펙에 비하며 매우 적은 용량이다.&lt;br /&gt;
또한 피크 타임 쓰기 부하인 2,500 TPS 역시 단일 쓰레드로 동작하는 Redis 노드 1대가 초당 처리할 수 있는 성능(보통 5만~10만 TPS 이상)의 5%에 불과하므로, 1대로 충분히 감당 가능하다.&lt;/p&gt;

&lt;p&gt;그러나 트래픽이 100배 증가하여 &lt;strong&gt;5억 DAU&lt;/strong&gt;가 되면, 용량은 &lt;strong&gt;65GB&lt;/strong&gt;, 피크 QPS는 &lt;strong&gt;250,000 QPS&lt;/strong&gt;까지 솟구치므로 단일 노드로는 한계에 다다른다.&lt;br /&gt;
이때부터는 여러 Redis 노드로 데이터를 분산하는 샤딩이 필수적이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;321-redis-데이터-샤딩-방안&quot;&gt;3.2.1. Redis 데이터 샤딩 방안&lt;/h3&gt;

&lt;p&gt;Redis 분산 샤딩 기법으로는 고정 파티션과 해시 파티션 방식이 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;322-고정-파티션sticky-partition&quot;&gt;3.2.2. 고정 파티션(Sticky Partition)&lt;/h3&gt;

&lt;p&gt;고정 파티션은 획득 가능한 점수 범위(Range)에 따라 샤드를 나누는 방식이다.&lt;br /&gt;
예를 들어 1~1,000점 범위를 100점 단위로 나누어 10개의 독립된 Redis 샤드(Sorted Set)로 분할 관리하는 방식이다.&lt;br /&gt;
고정 파티션 구조에서는 각 점수 범위를 담당하는 Redis 샤드 노드(또는 해당 노드 내의 Key) 자체가 독립된 하나하나의 Sorted Set 인스턴스로 동작한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/sticky.png&quot; alt=&quot;고정 파티션&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이 방식이 실제 운영 환경에서 원활하게 작동하려면 아래와 같은 전제 조건과 운영 로직이 갖춰줘야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;점수 분포의 균등성과 동적 구간 재조정&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;고정 파티션이 효율적으로 작동하려면 &lt;strong&gt;전체 유저의 점수가 각 샤드에 비교적 고르게 분포&lt;/strong&gt;되어야 한다.&lt;/li&gt;
      &lt;li&gt;만약 특정 점수대(예: 100~200점 구간)에 유저 90%가 몰려 있다면, 해당 구간을 담당하는 샤드만 과부하가 걸려 샤딩을 한 의미가 사라진다.(핫 파티션 발생)&lt;/li&gt;
      &lt;li&gt;따라서 유저들이 점수 분포 데이터를 지속적으로 모니터링하면서 유저가 밀집된 구간은 점수 범위를 좁히고(예: 100~120점), 유저가 드문 구간은 범위를 넓히는 방식으로 &lt;strong&gt;샤드별 점수 커버리지를 동적으로 재조정&lt;/strong&gt;해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;애플리케이션 주도 샤딩 및 2차 캐시(Secondary Cache) 도입&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;고정 파티션 구조에서는 &lt;strong&gt;애플리케이션 서버가 직접 샤딩의 주체&lt;/strong&gt;가 되어 유저의 요청을 적절한 샤드로 라우팅해야 한다.&lt;/li&gt;
      &lt;li&gt;유저의 점수를 등록하거나 갱신하려면, 먼저 해당 유저가 현재 어느 샤드(점수 구간)에 존재하는지 파악해야 한다.
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;DB 직접 조회 방식&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;유저의 현재 점수를 알기 위해 매번 MySQL과 같은 RDB를 조회하는 것은 DB에 심각한 읽기 병목을 일으킨다.&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;2차 캐시 활용 방안&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;따라서 유저 ID와 현재 점수(또는 속한 샤드 ID)의 매핑 정보를 빠르게 읽을 수 있는 Redis Key-Value 형태의 2차 캐시에 보관하는 것이 성능상 훨씬 유리하다(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user:100:score&lt;/code&gt; -&amp;gt; 150)&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;샤드 간 유저 데이터 이동(Shard Migration) 처리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저가 게임에서 승리하여 점수가 올라가면 기존 샤드의 점수 구간을 벗어나 &lt;strong&gt;상위 점수를 담당하는 새 샤드로 이동&lt;/strong&gt;해야 한다.&lt;/li&gt;
      &lt;li&gt;이 때 애플리케이션은 Atomic하게 다음 연산을 순차 실행해야 한다.
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;기존 샤드 제거:&lt;/strong&gt; 이전 점수 대역을 담당하던 Redis 샤드에서 유저를 삭제(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZREM&lt;/code&gt;)&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;신규 샤드 추가:&lt;/strong&gt; 점수가 상승하여 진입한 새로운 상위 Redis 샤드에 유저 추가(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZADD&lt;/code&gt;)&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;2차 캐시 갱신:&lt;/strong&gt; 2차 캐시에 저장된 유저의 현재 점수 및 샤드 매핑 정보 최신화&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;샤드-간-유저-이동-시-데이터-일관성-이슈&quot;&gt;💡샤드 간 유저 이동 시 데이터 일관성 이슈&lt;/h4&gt;

&lt;p&gt;기존 샤드에서 삭제(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZREM&lt;/code&gt;)만 되고 서버 장애로 새 샤드 추가(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZADD&lt;/code&gt;)가 실패하면 유저가 순위표에서 증발하는 문제가 발생한다.&lt;/p&gt;

&lt;p&gt;이를 방지하기 위해 애플리케이션은&lt;br /&gt;
(1) 트랜잭션 보장 로직을 작성하거나&lt;br /&gt;
(2) 실패 시 DB 백업 데이터 기반의 Retry 메커니즘을 구비해야 하며&lt;br /&gt;
2차 캐시의 점수를 기준으로 샤드 위치를 항상 동기화해주어야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;고정 파티션에서의 조회 동작 원리&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;상위 10명 조회&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;가장 높은 점수 구간을 담당하는 최상위 샤드(예: 901~1,000점 샤드)의 Sorted Set에서 상위 10명을 가져오면 되므로 \(O(\log N)\)으로 매우 빠르게 처리된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;특정 유저의 전체 순위 계산&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저가 속한 샤드 내부에서의 상대적 순위에 더해, &lt;strong&gt;나보다 높은 점수를 커버하는 상위 샤드들의 전체 유저 수&lt;/strong&gt;를 모두 더해야 최종 전체 순위가 계산된다.&lt;/li&gt;
      &lt;li&gt;각 샤드의 전체 유저 수(키 개수)는 Redis 의 &lt;a href=&quot;https://redis.io/docs/latest/commands/INFO/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INFO keyspace&lt;/code&gt;&lt;/a&gt; 명령을 통해 \(O(1)\) 시간에 즉시 조회할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;redis의-info-keyspace-란&quot;&gt;💡Redis의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INFO keyspace&lt;/code&gt; 란?&lt;/h4&gt;

&lt;p&gt;Redis의 시스템 상태를 점검하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INFO&lt;/code&gt; 명령의 옵션 중 하나이다.&lt;br /&gt;
현재 DB에 저장된 전체 키의 개수, 만료 설정된 키 개수 등의 통계 데이터를 \(O(1)\) 시간에 출력해 주는 모니터링 명령어이다.&lt;/p&gt;

&lt;p&gt;각 샤드에 몇 명의 유저 키가 존재하는지 즉시 파악할 때 사용한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;redis의-고정-파티션범위-샤딩은-redis-cluster를-사용하지-않는걸까&quot;&gt;💡Redis의 고정 파티션(범위 샤딩)은 Redis Cluster를 사용하지 않는걸까?&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Redis Cluster는 레디스가 알아서 키를 해시 슬롯에 분배해 주는 ‘하드웨어/인프라 주도 샤딩’이고,&lt;br /&gt;
고정 파티션은 개발자가 점수 구간에 맞춰 직접 서버를 지정하는 ‘애플리케이션 주도 샤딩’이기 때문에 Redis Cluster를 사용하지 않는다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;그렇다. 고정 파티션은 표준 Redis Cluster 기능을 이용하지 않는다.&lt;br /&gt;
대신 애플리케이션이 직접 관리하는 &lt;strong&gt;독립된 여러 개의 Redis 인스턴스(Standalone/&lt;a href=&quot;https://assu10.github.io/dev/2022/09/03/redis-architecture/#21-%EB%85%B8%EB%93%9C-%EC%88%98--of-nodes-per-cluster&quot;&gt;Sentinel&lt;/a&gt;&lt;/strong&gt;)를 연결해서 사용한다.&lt;br /&gt;
이유는 아래와 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;샤딩 기준의 불일치(Key vs Value)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Redis Cluster의 샤딩 방식:&lt;/strong&gt; Key 이름을 가지고 연산한다. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CRC16(key) % 16384&lt;/code&gt; 즉, 키 이름만 보고 어느 노드로 보낼지 레디스가 자동으로 결정한다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;고정 파티션의 샤딩 방식:&lt;/strong&gt; 키 이름이 아니라 &lt;strong&gt;데이터 내부의 점수(Score/Value)&lt;/strong&gt;를 기준으로 샤드를 나눈다.&lt;/li&gt;
      &lt;li&gt;레디스 클러스터는 ‘이 유저의 점수가 150점이니 2번 노드로 보낸다’하는 점수 기반 자동 라우팅 기능이 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 이동 주체의 차이&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Redis Cluster:&lt;/strong&gt; 노드가 추가되거나 빠질 때 ‘해시 슬롯’ 단위로 데이터를 자동으로 리샤딩(데이터 이동)한다.&lt;/li&gt;
      &lt;li&gt;고정 파티션: 유저가 게임을 해서 점수가 올랐을 때, 상위 점수 샤드로 유저를 옮기는 것은 레디스가 아니라 애플리케이션이 직접 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZREM&lt;/code&gt;(기존 샤드에서 삭제) + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZADD&lt;/code&gt;(신규 샤드에 추가)해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;클러스터의 ‘Cross-Slot’ 제약 조건&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;레디스 클러스터는 서로 다른 노드에 있는 키들을 한 번에 조작하거나 묶어서 연산하는 것을 엄격하게 제한한다.&lt;/li&gt;
      &lt;li&gt;고정 파티션처럼 애플리케이션이 샤드 간 데이터를 자유롭게 옮기고 통계를 집계하려면, 레디스 클러스터보다는 독립된 Redis 인스턴스 여럿을 앱 서버가 직접 컨트롤하는 구조가 훨씬 안전하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;그럼 실무에서는 고정 파티션을 어떻게 구성할까?&lt;br /&gt;
Redis Cluster 대신 아래와 같은 형태로 인프라를 구성한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[ 애플리케이션 서버 (샤딩 로직 보유) ]
       │
       ├─► [ Redis Shard 1 ] (Standalone / 0 ~ 100점 전용)
       ├─► [ Redis Shard 2 ] (Standalone / 101 ~ 200점 전용)
       └─► [ Redis Shard 3 ] (Standalone / 201 ~ 300점 전용)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;서버 구성:&lt;/strong&gt; 각 샤드는 서로의 존재는 모르는 독립된 Redis 서버&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;고가용성 대책:&lt;/strong&gt; 각 샤드가 다운되는 것에 대비해, 샤드마다 Redis Sentinel이나 Master-Replica 복제 구조를 개별적으로 붙여서 안정성을 확보&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;323-해시-파티션hash-partition-방식과-scatter-gather-패턴&quot;&gt;3.2.3. 해시 파티션(Hash Partition) 방식과 Scatter-Gather 패턴&lt;/h3&gt;

&lt;p&gt;고정 파티션 방식은 플레이어의 점수가 특정 대역(예: 하위 점수 구간)에 과도하게 몰려있을 때 특정 샤드로 트래픽이 몰리는 단점이 있다.&lt;br /&gt;
반면, 해시 파티션은 &lt;strong&gt;Redis Cluster&lt;/strong&gt;를 도입하여 유저의 점수 분포와 상관없이 여러 노드에 데이터를 균등하게 샤딩하는 접근법이다.&lt;/p&gt;

&lt;p&gt;Redis Cluster는 &lt;a href=&quot;https://assu10.github.io/dev/2026/07/02/architecture-consistent-hashing/&quot;&gt;안정 해시(Consistent Hashing)&lt;/a&gt; 대신 &lt;a href=&quot;https://stackoverflow.com/questions/36203532/why-redis-cluster-only-have-16384-slots&quot;&gt;&lt;strong&gt;총 16,384개의 해시 슬롯&lt;/strong&gt;&lt;/a&gt;을 기반으로 샤딩을 수행한다.&lt;br /&gt;
Key가 들어올 때마다 &lt;a href=&quot;https://en.wikipedia.org/wiki/Cyclic_redundancy_check&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CRC16(key) % 16384&lt;/code&gt;&lt;/a&gt; 연산을 실행하여 해당 키가 속할 해시 슬롯과 노드를 결정한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/hash.png&quot; alt=&quot;해시 파티션&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;노드 분산 예시(3개 노드 클러스터)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;노드 1: 해시 슬롯 [0, 5500] 담당&lt;/li&gt;
      &lt;li&gt;노드 2: 해시 슬롯 [5551, 11000] 담당&lt;/li&gt;
      &lt;li&gt;노드 3: 해시 슬롯 [11001, 16383] 담당&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;redis-clster는-왜-하필-16384214개-슬롯을-사용할까&quot;&gt;💡Redis Clster는 왜 하필 16,384(\(2^{14}\))개 슬롯을 사용할까?&lt;/h4&gt;

&lt;p&gt;Redis Cluster 노드들은 서로 상태를 주기적으로 교환하는 Heartbeat 패킷을 보낸다.&lt;br /&gt;
이 때 슬롯 상태를 Bitmap 으로 실어 보내는데, 16,384개 비트는 딱 &lt;strong&gt;2KB&lt;/strong&gt;크기로 매우 적은 네트워크 오버헤드만 발생한다.&lt;br /&gt;
슬롯 수가 65,536개 등으로 커지면 패킷 크기가 커져 네트워크 병목이 생긴다.&lt;br /&gt;
또한 클러스터 최대 권장 노드 수(1,000여 개)에 가장 이상적인 슬롯 개수가 16,384개이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;crc16cyclic-redundancy-check-이란&quot;&gt;💡CRC16(Cyclic Redundancy Check) 이란?&lt;/h4&gt;

&lt;p&gt;순환 중복 검사(Cyclic Redundancy Check) 알고리즘으로, 임의의 문자열 키를 입력받아 16비트 정수값을 생성하는 해시 함수이다.&lt;br /&gt;
Redis는 키 문자열을 입력받아 0~65,536 사이의 해시값을 고르게 생성하며, 이를 16,384로 나눈 나머지(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CRC16(key) % 16,384&lt;/code&gt;)로 데이터가 위치할 해시 슬록을 균등하게 할당한다.&lt;/p&gt;

&lt;p&gt;여기서 65,536 이라는 숫자가 나온 이유는 CRC16이 &lt;strong&gt;16비트&lt;/strong&gt; 알고리즘이기 때문이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;16비트와 65,536의 관계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;컴퓨터는 모든 데이터를 0과 1로 처리한다. 자릿수(비트 수)가 늘어날 때마다 표현할 수 있는 가지수는 2배씩 늘어난다.
        &lt;ul&gt;
          &lt;li&gt;1비트 = \(2^1 = 2\)가지 (0, 1)&lt;/li&gt;
          &lt;li&gt;2비트 = \(2^2 = 4\)가지 (00, 01, 10, 11)&lt;/li&gt;
          &lt;li&gt;8비트 = \(2^8 = 256\)가지&lt;/li&gt;
          &lt;li&gt;16비트 = \(2^{16} = 65,536\)가지&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;컴퓨터는 숫자를 0부터 세기 때문에, 총 65,536개의 숫자를 표현하면 그 범위는 &lt;strong&gt;0~65,535&lt;/strong&gt;까지가 된다.&lt;/p&gt;

&lt;p&gt;그럼 왜 Redis는 65,536개를 다 안 쓰고 16,384개만 쓸까?&lt;/p&gt;

&lt;p&gt;CRC16이 만들어내는 해시값은 0~65,535까지 65,536개나 되는데, 레디스는 굳이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;% 16384&lt;/code&gt; 연산을 통해 해시 슬롯을 &lt;strong&gt;16,384(\(2^{14}\))&lt;/strong&gt;개로 줄여서 사용한다.&lt;br /&gt;
여기에는 레디스 개발자의 네트워크 최적화 고민이 들어있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Heartbeat 패킷 최적화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;레디스 클러스터 노드들은 서로 주기적으로 Heartbeat를 주고 받는다. 이 때 자신이 담당하는 슬롯 정보를 Bitmap 형태로 실어 보낸다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;65,536개 슬롯을 쓴다면:&lt;/strong&gt; 비트맵 크기가 \(65,536 \div 8 = 8\text{KB}\)가 된다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;16,384개 슬롯을 쓴다면:&lt;/strong&gt; 비트맵 크기가 \(16,384 \div 8 = 2\text{KB}\)로 4배나 작아진다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;즉, CRC16 알고리즘 특성상 연산 결과는 &lt;strong&gt;16비트(\(2^{16} = 65,536\)가지)&lt;/strong&gt;로 나오지만, 레디스는 네트워크 통신 비용을 줄이기 위해 이를 16,384(\(2^{14}\))의 
슬롯으로 압축해서 사용하는 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1) 점수 갱신 연산&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;사용자의 점수를 갱신할 때는 사용자 ID를 기반으로 슬롯을 계산(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CRC16(user_id) % 16384&lt;/code&gt;로 찾을 수 있음)하여, 해당 슬롯을 담당하는 특정 샤드에만 접속한 뒤 
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZINCRBY&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZADD&lt;/code&gt; 명령을 수행하면 된다.&lt;br /&gt;
쓰기 작업은 단일 샤드에서 \(O(\log N)\)으로 매우 신속하게 처리된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2) 상위 10명 순위표 검색(Scatter-Gather 패턴)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;해시 파티션 환경에서는 유저 데이터가 점수와 상관없이 전 샤드에 무작위 분산되어 있다.&lt;br /&gt;
따라서 상위 10명의 플레이어를 검색하는 과정을 다소 복잡해진다.&lt;/p&gt;

&lt;p&gt;이 때는 모든 샤드에 각각 10명 데이터를 요청한 뒤, 애플리케이션 서버에서 이 결과를 한데 모아 다시 정렬하는 &lt;a href=&quot;https://assu10.github.io/dev/2026/07/05/architecture-s3-like-object-storage/#483-object-%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B7%9C%EB%AA%A8-%ED%99%95%EC%9E%A5-sharding&quot;&gt;Scatter-Gather&lt;/a&gt; 
접근법을 사용해야 한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/distri.png&quot; alt=&quot;Scatter-Gather 방안&quot; /&gt;&lt;/p&gt;

&lt;p&gt;모든 샤드에 유저 목록을 질의하는 절차를 병렬로 수행하면 네트워크 지연 시간을 일부 줄일 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;3) 해시 파티션 및 Scatter-Gather 패턴의 구조적 문제점&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;하지만 이 방식은 실시간 게임 순위표 시스템에서 다음과 같은 치명적인 한계를 가진다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;상위 K개 결과 반환 시 지연 시간 증가&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;상위 K명의 결과를 얻기 위해 각 샤드에서 K개씩, 총 S(샤드 수) * K개의 데이터를 읽어와 애플리케이션 메모리에서 재정렬해야 하므로 읽기 지연 시간이 대폭 증가한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tail Latency(가장 느린 파티션 병목)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;모든 샤드에 병렬로 질의를 던지더라도, 결과 집계는 &lt;strong&gt;가장 느리게 응답하는 파티션의 처리가 끝날 때까지 대기&lt;/strong&gt;해야 한다.&lt;/li&gt;
      &lt;li&gt;시스템 전체 응답 속도가 최악의 성능을 내는 노드에 구속된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;특정 사용자의 전체 순위 산출 불가&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저가 속한 샤드 내에서의 순위는 알 수 있지만, 다른 샤드에 나보다 점수가 높은 유저가 몇 명이나 존재하는지 알 방법이 없다.&lt;/li&gt;
      &lt;li&gt;특정 유저의 정확한 글로벌 순위를 산출하려면 결국 모든 샤드의 전체 데이터를 뒤져야 하므로 Sorted Set의 장점이 사라진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이러한 문제 때문에 실시간으로 정밀한 순위를 보여주어야 하는 본 설계안에서는 해시 파티션 대신 &lt;strong&gt;고정 파티션(점수 범위 샤딩)&lt;/strong&gt; 방안을 최종 채택한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;실무에서는-고정-파티션과-해시-파티션-중-무엇을-더-많이-사용할까&quot;&gt;💡실무에서는 고정 파티션과 해시 파티션 중 무엇을 더 많이 사용할까?&lt;/h4&gt;

&lt;p&gt;일반적인 분산 세션/데이터 캐싱 환경에서는 운영과 노드 증설(Resharding)이 자동화되어 있는 &lt;strong&gt;해시 파티션(Redis Cluster)&lt;/strong&gt;을 90% 이상 사용한다.&lt;/p&gt;

&lt;p&gt;하지만 &lt;strong&gt;리더보드, 실시간 랭킹 시스템처럼 ‘전체 데이터 간 정렬 및 순위 조회’가 핵심인 특수 목적 시스템&lt;/strong&gt;에서는 Scatter-Gather 방식의 한계(Tail Latency, 전체 순위 측정 불가) 
때문에 애플리케이션 레벨에서 직접 제어하는 &lt;strong&gt;고정 파티션(범위 샤딩)&lt;/strong&gt;을 채택하는 것이 실무 표준이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;324-레디스-노드-크기-조정-및-벤치마킹&quot;&gt;3.2.4. 레디스 노드 크기 조정 및 벤치마킹&lt;/h3&gt;

&lt;p&gt;Redis 노드를 구성할 때는 스냅숏(RDB) 생성 및 &lt;a href=&quot;https://assu10.github.io/dev/2022/09/03/redis-architecture/#6-copy-on-write&quot;&gt;Copy-on-Write&lt;/a&gt; 버퍼를 고려하여 
쓰기 작업이 많은 환경에서는 &lt;strong&gt;메모리를 이론치보다 2배 정도 넉넉하게 할당&lt;/strong&gt;해야 안전하다.&lt;/p&gt;

&lt;p&gt;하드웨어 수용량을 측정하기 위해 Redis가 기본 제공하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redis-benchmark&lt;/code&gt; 도구를 활용한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;redis-benchmark란&quot;&gt;💡&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redis-benchmark&lt;/code&gt;란?&lt;/h4&gt;

&lt;p&gt;Redis 서버의 가공할 응답 속도와 TPS를 측정하는 공식 성능 테스트 유틸리티이다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redis-benchmark -h localhost -p 6379 -c 50 -n 100000&lt;/code&gt; 처럼 실행하여 50개의 동시 클라이언트가 10만 건의 요청을 보낼 때의 지연 시간 분포와 
초당 처리량을 리포트로 확인한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-대안-nosql-데이터베이스dynamodb&quot;&gt;3.3. 대안: NoSQL 데이터베이스(DynamoDB)&lt;/h2&gt;

&lt;p&gt;Redis 대신 관리 부담이 적고 안정적인 Scale-out을 지원하는 NoSQL 데이터베이스(Amazon DynamoDB, Cassadra, MongoDB 등)를 리더보드의 메인 저장소로 
활용하는 방안도 훌륭한 대안이 될 수 있다.&lt;/p&gt;

&lt;p&gt;실시간 리더보드 구축을 위해 NoSQL을 선택할 때는 다음 두 가지 조건이 필수적이다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;높은 쓰기 연산 최적화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;초당 수천 건 이상의 점수 갱신 요청을 병목 없이 처리할 수 있어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;파티션 내 자동 정렬 기능&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;동일한 파티션에 저장된 데이터를 점수(Score) 기준으로 효율적으로 정렬하여 읽을 수 있어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/dynamo.png&quot; alt=&quot;DynamoDB 기반 솔루션&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Amazon DynamoDB는 이러한 조건을 만족하는 대포적인 완전 관리형 NoSQL 데이터베이스이다.&lt;br /&gt;
DynamoDB는 PK가 아닌 다른 Attribute로도 데이터를 효율적으로 조회할 수 있도록 &lt;a href=&quot;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html&quot;&gt;&lt;strong&gt;GSI(Global Secondary Index)&lt;/strong&gt;&lt;/a&gt;를 제공한다.&lt;br /&gt;
GSI는 원본 테이블의 속성들을 활용해 구성되지만, 원본 테이블과는 완전히 별개의 새로운 PK를 정의하여 전혀 다른 관점에서 데이터를 질의할 수 있게 해준다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;331-비정규화-테이블의-한계와-핫-파티션hot-partition-문제&quot;&gt;3.3.1. 비정규화 테이블의 한계와 핫 파티션(Hot Partition) 문제&lt;/h3&gt;

&lt;p&gt;체크 게임의 월간 순위표를 설계한다고 가정해보자.&lt;br /&gt;
단순하게 순위표와 유저 정보를 비정규화하여 아래와 같이 단일 테이블로 저장할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/old.png&quot; alt=&quot;순위표 및 사용자 테이블의 비정규화&quot; /&gt;&lt;/p&gt;

&lt;p&gt;하지만 데이터가 수백만 건으로 늘어나면 상위 점수를 찾기 위해 전체 테이블을 훑어야 하는 테이블 풀스캔이 발생하여 읽기 성능이 급격히 고갈된다.&lt;/p&gt;

&lt;p&gt;이 문제를 피하려면 DynamoDB의 파티션 키와 정렬 키(=클러스터 키) 구조를 활용해야 한다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_name#{year-month}&lt;/code&gt; 를 파티션 키로, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;score&lt;/code&gt;를 정렬 키로 지정하면 특정 달의 점수를 정렬된 상태로 모아둘 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/partition.png&quot; alt=&quot;파티션 키 및 정렬 키&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/07/04/architecture-distributed-email-service/#63-nosql-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8%EB%A7%81-%ED%8C%8C%ED%8B%B0%EC%85%98-%ED%82%A4%EC%99%80-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%ED%82%A4-%EC%84%A4%EA%B3%84&quot;&gt;6.3. NoSQL 데이터 모델링: 파티션 키와 클러스터 키 설계&lt;/a&gt; 참고&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;그러나 이 스키마는 핫 파티션이라는 치명적인 문제를 야기한다.&lt;br /&gt;
DynamoDB는 내부적으로 파티션 키를 기준으로 물리적 노드에 데이터를 분산하는데, 모든 유저가 이번 달(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2026-08&lt;/code&gt;) 데이터를 갱신하다 보면 특정 노드 하나에만 전체 
트래픽이 몰려 디스크 I/O 병목이 발생하게 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;gsiglobal-secondary-index란&quot;&gt;💡GSI(Global Secondary Index)란?&lt;/h4&gt;

&lt;p&gt;DynamoDB의 기본 테이블은 최초 지정한 PK로만 조회가 가능하다.&lt;br /&gt;
하지만 다른 컬럼을 기준으로 정렬 조회하고 싶을 때, 원본 테이블의 데이터를 바탕으로 &lt;strong&gt;‘새로운 파티션 키와 정렬 키를 가지는 보조 인덱스 테이블’&lt;/strong&gt;을 백그라운드에서 
동기화하여 만들어주는 기능이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;gsi는-어떻게-파티션-키와-정렬-키를-사용하는-걸까&quot;&gt;💡GSI는 어떻게 파티션 키와 정렬 키를 사용하는 걸까?&lt;/h4&gt;

&lt;p&gt;GSI는 쉽게 말해 &lt;strong&gt;원본 테이블의 데이터를 바탕으로 DynamoDB가 백그라운드에서 실시간으로 만들어주는 자동 복제(미러링) 인덱스 테이블&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;GSI 매핑과 작동 과정은 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;원본 테이블 데이터 저장(Write)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;애플리케이션이 원본 테이블에 유저 데이터(예: user_id: 1234, score: 950, shard_id: game#2026-08#p0)를 넣는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;GSI 비동기 복제 및 자동 정렬(DynamoDB 내부 동작)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;DynamoDB가 백그라운드에서 원본 데이터 변경을 감지하고, 지정해 둔 GSI로 데이터를 자동으로 복제한다.&lt;/li&gt;
      &lt;li&gt;이 때 GSI에 설정된 규칙에 따라
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;파티션 키(shard_id):&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game#2026-08#p0&lt;/code&gt;값을 기준으로 특정 물리 노드에 데이터를 모은다.&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;정렬 키(score):&lt;/strong&gt; 같은 파티션(p0) 안에서 점수(950)을 기준으로 디스크상에서 점수 순서대로 자동 정렬하여 보관한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;GSI를 통한 고속 조회(Read)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;개발자가 GSI로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game#2026-08#p0 파티션에서 score 내림차순으로 상위 10개만 줘&lt;/code&gt;라는 Query 요청을 날린다.&lt;/li&gt;
      &lt;li&gt;GSI 파티션 내부에는 점수가 이미 순서대로 정렬되어 있으므로, DynamoDB는 전체를 뒤질 필요 없이 맨 위에 10개만 빼서 즉시 반환한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;332-쓰기-샤딩write-sharding-패턴과-파티션-개수-트레이드오프&quot;&gt;3.3.2. 쓰기 샤딩(Write Sharding) 패턴과 파티션 개수 트레이드오프&lt;/h3&gt;

&lt;p&gt;핫 파티션 문제를 완벽히 해결하기 위해 파티션 키에 무작위 샤드 번호(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id % 파티션 수&lt;/code&gt;)를 조합하여 데이터를 여러 파티션에 강제로 분산시키는 
&lt;strong&gt;쓰기 샤딩(Write Sharding)&lt;/strong&gt; 패턴을 적용한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;새로운 파티션 키 규격: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_name#{year-month}#p{partition_number}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/new.png&quot; alt=&quot;새로운 파티션 키&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이 때 GSI를 구성하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_name#{year-month}#p{partition_number}&lt;/code&gt;를 파티션 키로, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;score&lt;/code&gt;를 정렬 키로 매핑하면 샤드별로 점수가 정렬된 N개의 파티션이 만들어진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;파티션 개수 산정과 트레이드 오프&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;샤딩을 적용할 때 가장 중요한 질문은 ‘과연 몇 개의 파티션(N)을 두어야 하는가?’이다.&lt;br /&gt;
이는 예상되는 쓰기 볼륨(Write TPS)이나 DAU를 기준으로 산정해야 하며, &lt;strong&gt;파티션 부하와 읽기 복잡도 사이의 트레이드오프&lt;/strong&gt;를 철저히 고려해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;파티션 수를 줄일 경우&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;장점(읽기 단순화)&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;조회할 파티션 수가 적어지므로 파티션이 많을 때보다 &lt;strong&gt;Scatter-Gather 읽기 속도가 빨라지고 구현이 단순&lt;/strong&gt;해진다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점(쓰기 병목 위험)&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;파티션 하나당 담당하는 트래픽이 커져 특정 파티션에 부하가 쏠리는 &lt;strong&gt;핫 파티션(Hot Partition) 위험&lt;/strong&gt;이 커진다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;파티션 수를 늘릴 경우&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;장점(쓰기 분산)&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;데이터를 여러 파티션으로 고르게 분산시키므로 개별 노드의 쓰기 부하가 비약적으로 낮아진다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점(읽기 지연 증가)&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;상위 10명을 읽으려고 할 때 &lt;strong&gt;조회해야 할 파티션 수가 늘어나므로(Scatter-Gather)&lt;/strong&gt; 애플리케이션의 읽기 복잡도와 지연 시간이 증가한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/distri2.png&quot; alt=&quot;분산 수집&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;쓰기-샤딩write-sharding이란&quot;&gt;💡쓰기 샤딩(Write Sharding)이란?&lt;/h4&gt;

&lt;p&gt;쓰기 샤딩은 특정 파티션에 트래픽이 몰리는 핫 파티션(Hot Partition) 문제를 해결하기 위해, 파티션 키에 무작위 또는 계산된 접미사(샤드 번호)를 붙여 데이터를 
여러 파티션으로 강제 분산시키는 기법이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;기존 방식&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_name#2026-08&lt;/code&gt; 하나의 파티션 키로 모든 유저의 데이터가 몰림&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;쓰기 샤딩 방식&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id % 파티션 개수(N)&lt;/code&gt;을 계산하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_name#2026-08#p0&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_name#2026-08#p1&lt;/code&gt;.. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;game_name#2026-08#pN&lt;/code&gt;으로 트래픽을 N개로 찢어서 저장&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;쓰기 샤딩은 쓰기 성능 향상을 위해 읽기 성능 및 구현 복잡도를 대가로 지불하는 대표적인 트레이드오프 구조이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;핫 파티션 완벽 예방&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;쓰기 트래픽이 N개의 파티션으로 균등하게 분산되므로 단일 노드 I/O 병목 및 쓰기 처리량 제한(Throttling)을 방지&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;무제한 쓰기 확장성&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;데이터나 트래픽이 늘어나더라도 샤드 수(N)만 늘려주면 쓰기 성능을 수평적으로 확장할 수 있음&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;읽기 작업의 복잡도 및 비용 증가(Scatter-Gather)&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;전체 순위표(예: 전체 Top 100)를 조회할 때 하나의 파티션만 읽을 수 없다.&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;모든 샤드 N개를 각각 병렬 조회(Scatter)한 후, 애플리케이션 메모리에서 결과를 하나로 합치고 다시 정렬(Gather)&lt;/strong&gt;해야 한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;파티션 개수(N) 결정의 트레이드 오프&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;N이 너무 작은 경우&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;여전히 핫 파티션 위험이 남음&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;N이 너무 큰 경우&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;쓰기는 쾌적해지지만, 읽을 때 N번의 조회 요청(&lt;a href=&quot;https://assu10.github.io/dev/2025/05/27/fanout/&quot;&gt;Fan-out&lt;/a&gt;)이 발생하여 읽기 비용(RCU, Read Capacity Unit)와 응답 지연 시간이 비례해서 증가함&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;즉, 쓰기 샤딩은 &lt;strong&gt;쓰기 속도를 끌어올리는 대신, 전체 데이터를 읽어올 때 N배의 읽기 수고(Scatter-Gather)를 감수하는 전략&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;벤치마킹을-통해-파티션-수의-최적값을-파악하는-방법은&quot;&gt;💡벤치마킹을 통해 파티션 수의 최적값을 파악하는 방법은?&lt;/h4&gt;

&lt;p&gt;부하 테스트 툴(JMeter, Locust 등)을 이용해 파티션 수를 4개, 8개, 16개로 바꿔가며 테스트한다.&lt;br /&gt;
쓰기 요청 시 DynamoDB에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ThrottlingException&lt;/code&gt;(부하 초과 에러)이 발생하지 않는 최소한의 파티션 개수를 산출함으로써, 읽기 시 발생하는 Scatter-Gather 병목을 최소화하는 
최적 포인트를 찾는다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;333-nosql-환경에서의-상대적-순위-계산-백분위수percentile-활용&quot;&gt;3.3.3. NoSQL 환경에서의 상대적 순위 계산: 백분위수(Percentile) 활용&lt;/h3&gt;

&lt;p&gt;NoSQL 쓰기 샤딩을 적용하면 데이터가 여러 샤드 파티션으로 분산되므로, 특정 유저가 전체 중에서 정확히 몇 위인지 계산하기 위해 모든 샤드의 데이터를 합치는 것은 
심각한 읽기 지연을 유발한다.&lt;/p&gt;

&lt;p&gt;하지만 대규모 게임 서비스에서는 유저에게 ‘당신은 정확히 123위입니다.’ 라고 알려주는 것보다, ‘당신은 상위 10% 이내의 최상위 플레이어입니다.’ 라고 백분위수로 보여주는 
것만으로도 충분히 유저 경험(UX)와 승부욕을 제공할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;크론 작업을 통한 점수 분포 캐싱 메커니즘&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;전체 조건(점수 분포의 균등성)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저 규모와 트래픽이 충분히 커서 샤딩이 필요한 시스템이라면, 통계적으로 &lt;strong&gt;모든 샤드의 점수 분포는 거의 동일&lt;/strong&gt;하다라고 가정할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;주기적 크론 작업&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;주기적으로 실행되는 크론 배치 작업이 전체 샤드 중 1개 샤드의 점수 분포 데이터를 분석하여 각 백분위수 구간별 점수 기준표를 생성하고 이를 캐시에 보관한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;캐시된 백분위수 점수표 예시&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;10번째 백분위수 (상위 90%) = 점수 &amp;lt; 100점&lt;/li&gt;
      &lt;li&gt;20번째 백분위수 (상위 80%) = 점수 &amp;lt; 500점&lt;/li&gt;
      &lt;li&gt;…&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;90번째 백분위수 (상위 10%) = 점수 &amp;lt; 6,500점&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;상대적 순위 즉시 산출&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저가 자신의 순위를 조회할 때, 복잡한 크로스 샤드 연산 없이 캐시에 올려둔 백분위수 점수표와 유저의 현재 점수만 비교(\(O(1)\))하여, &lt;strong&gt;당신은 상위 10%(90번째 백분위수)입니다.&lt;/strong&gt;라는 결과를 지연없이 즉시 반환한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-운영-이슈-및-고도화-방안&quot;&gt;4. 운영 이슈 및 고도화 방안&lt;/h1&gt;

&lt;p&gt;실시간 리더보드를 실제로 운영할 때 필요한 &lt;strong&gt;조회 속도 최적화, 동점자 순위 처리&lt;/strong&gt;, 그리고 &lt;strong&gt;장애 복구 전략&lt;/strong&gt;을 정리한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-redis-hash를-활용한-조회-최적화-및-동점자-순위-판정-방안&quot;&gt;4.1. Redis Hash를 활용한 조회 최적화 및 동점자 순위 판정 방안&lt;/h2&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;redis-hash란&quot;&gt;💡Redis Hash란?&lt;/h3&gt;

&lt;p&gt;Redis Hash는 하나의 Key 내부에 여러 개의 &lt;strong&gt;필드-값 쌍&lt;/strong&gt;을 객체 형태로 저장하는 자료 구조이다.&lt;/p&gt;

&lt;p&gt;예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HSET user:profile:aaa name &quot;assu&quot; image &quot;profile.png&quot; score &quot;999&quot;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;일반 Key-Value 구조보다 메모리를 매우 효율적으로 사용하며, 유저의 프로필 객체 데이터를 저장하고 조회하는 데 가장 이상적이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Redis Hash는 리더보드 시스템에서 다음 두 가지 핵심 목적으로 활용된다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) 유저 프로필 정보 빠른 조회를 위한 캐시&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Redis Sorted Set에는 오직 유저 ID와 점수만 저장되어 있다.&lt;/li&gt;
  &lt;li&gt;순위표 화면에 유저 닉네임, 프로필 이미지, 칭호 등을 함께 보여주기 위해 &lt;strong&gt;Redis Hash에 유저 ID와 프로필 객체를 대응시켜 보관&lt;/strong&gt;한다.&lt;/li&gt;
  &lt;li&gt;이를 통해 DB(MySQL)를 전혀 조회하지 않고도 상위 10명의 상세 프로필을 메모리 상에서 \(O(1)\)로 즉시 합성하여 응답할 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2) 동점자 발생 시 선착순(타임스탬프) 기반 순위 판정&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;두 유저의 점수가 같을 경우, ‘누가 더 먼저 해당 점수에 도달했는가?’에 따라 순위를 매긴다.&lt;/li&gt;
  &lt;li&gt;Redis Hash에 유저 ID와 마지막 승리 경기 타임스탬프를 대응시켜 기록해둔다.&lt;/li&gt;
  &lt;li&gt;동점자 발생 시 기록된 타임스탬프 값이 더 오래된(먼저 점수를 획득한) 유저에게 더 높은 순위를 부여하는 보완 로직을 적용한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;42-시스템-장애-복구-전략&quot;&gt;4.2. 시스템 장애 복구 전략&lt;/h2&gt;

&lt;p&gt;Redis는 In-Memory 데이터베이스이므로 노드 다운이나 클러스터 장애 발생 시 데이터 소실 위험이 존재한다.&lt;br /&gt;
이를 위해 DB(MySQL)를 활용한 복구 체계를 구축한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;승리 이력의 영구 저장&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저가 경기에서 승리할 때마다 MySQL의 점수 테이블에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;points&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timestamp&lt;/code&gt;를 영구 기록한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장애 발생 시 자동 복구 스크립트 실행&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Redis 클러스터 장애로 순위표 데이터가 손상되면, MySQL에 저장된 당월 승리 로그 데이터를 순회하는 복구 스크립트를 작동시킨다.&lt;/li&gt;
      &lt;li&gt;유저별 승리 이력을 차례로 읽으며 레코드당 한 번씩 Redis 명령(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZINCRBY&lt;/code&gt;)을 replay함으로써, 손상된 실시간 순위표를 100% 완벽하게 복원해낸다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-요약-및-결론&quot;&gt;5. 요약 및 결론&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0801/mindmap.png&quot; alt=&quot;마인드맵&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;RDB의 한계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;관계형 데이터베이스(MySQL 등)는 B-Tree 인덱스 재배치와 전체 정렬 스캔 병목으로 인해 대규모 실시간 리더보드에 부적합하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Redis Sorted Set 활용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;스킵 리스트(Skip List) 구조를 기반으로 데이터 추가/조회를 \(O(\log N)\) 시간에 완수하므로 실시간 리더보드 구축의 가장 이상적인 솔루션이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;대규모 확장 기법&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;고정 파티션(점수 범위 샤딩)&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;정밀한 순위 계산이 필요한 대규모 트래픽 환경에 적합하다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;NoSQL 쓰기 샤딩&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;핫 파티션을 해소하고, 백분위수(Percentile) 방식을 결합하여 무제한 확장을 달성한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;고가용성 및 장애 복구&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Redis Hash를 통한 프로필 캐싱/동점자 처리와 MySQL 승리 로그 기반의 장애 복구 스크립트를 갖춤으로써 시스템 안정성을 극대화한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬, 산 람 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000211656186&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links_vol2.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Man-in-the-middle_attack&quot;&gt;Man-in-the-middle attack - Wikipedia&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assu10.github.io/dev/2022/07/30/redis-zset/&quot;&gt;Redis Sorted Set (ZSet) 상세 자료구조&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/redis/redis/blob/unstable/src/t_zset.c&quot;&gt;Redis Sorted Set C언어 소스코드 (GitHub)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://medium.com/@sandeep4.verma/building-real-time-leaderboard-with-redis-82c98aa47b9f&quot;&gt;Building Real-Time Leaderboard with Redis&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ko/blogs/database/building-a-real-time-gaming-leaderboard-with-amazon-elasticache-for-redis/&quot;&gt;Building a real-time gaming leaderboard with Amazon ElastiCache for Redis&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://levelup.gitconnected.com/how-we-created-a-real-time-leaderboard-for-a-million-users-555aaa3ccf7b&quot;&gt;How we created a real-time leaderboard for a million users&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ko/lambda/&quot;&gt;AWS Lambda 공식 페이지&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://cloud.google.com/functions&quot;&gt;Cloud Functions - Google Cloud&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://azure.microsoft.com/en-us/products/functions/&quot;&gt;Azure Functions 공식 페이지&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/commands/INFO/&quot;&gt;Redis INFO 명령어 공식 문서&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/36203532/why-redis-cluster-only-have-16384-slots&quot;&gt;Why Redis Cluster only have 16384 slots - Stack Overflow&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Cyclic_redundancy_check&quot;&gt;Cyclic redundancy check (CRC) - Wikipedia&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/07/05/architecture-s3-like-object-storage/#483-object-%ED%85%8C%EC%9D%B4%EB%B8%94-%EA%B7%9C%EB%AA%A8-%ED%99%95%EC%9E%A5-sharding&quot;&gt;S3-like Object Storage 테이블 규모 확장 (Sharding)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html&quot;&gt;AWS DynamoDB Global Secondary Index (GSI) 가이드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/07/04/architecture-distributed-email-service/#63-nosql-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%AA%A8%EB%8D%B8%EB%A7%81-%ED%8C%8C%ED%8B%B0%EC%85%98-%ED%82%A4%EC%99%80-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%ED%82%A4-%EC%84%A4%EA%B3%84&quot;&gt;NoSQL 데이터 모델링: 파티션 키와 클러스터 키 설계&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 01 Aug 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/08/01/architecture-real-time-gaming-leaderboard/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/08/01/architecture-real-time-gaming-leaderboard/</guid>
        
        <category>architecture</category>
        
        <category>system-design</category>
        
        <category>redis</category>
        
        <category>sorted-set</category>
        
        <category>nosql</category>
        
        <category>dynamodb</category>
        
        <category>scalability</category>
        
        <category>skiplist</category>
        
        <category>write-sharding</category>
        
        <category>serverless</category>
        
        <category>대규모시스템설계</category>
        
        <category>실시간순위표</category>
        
        <category>레디스</category>
        
        <category>아키텍처</category>
        
        <category>노에스큐엘</category>
        
        <category>다이나모디비</category>
        
        <category>스킵리스트</category>
        
        <category>쓰기샤딩</category>
        
        <category>서버리스</category>
        
        <category>시스템아키텍처</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(2) - S3와 유사한 분산 객체 저장소</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-저장소-시스템-101&quot;&gt;1. 저장소 시스템 101&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-블록-저장소block-storage&quot;&gt;1.1. 블록 저장소(Block Storage)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#12-파일-저장소file-storage&quot;&gt;1.2. 파일 저장소(File Storage)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#13-객체-저장소object-storage&quot;&gt;1.3. 객체 저장소(Object Storage)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#14-block-vs-file-vs-object&quot;&gt;1.4. Block vs File vs Object&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#15-객체-저장소-핵심-용어&quot;&gt;1.5. 객체 저장소 핵심 용어&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-문제-이해-및-설계-범위-확정&quot;&gt;2. 문제 이해 및 설계 범위 확정&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-요구사항-정의&quot;&gt;2.1. 요구사항 정의&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-대략적인-규모-추정&quot;&gt;2.2. 대략적인 규모 추정&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-개략적-설계안-메타데이터와-데이터의-분리&quot;&gt;3. 개략적 설계안: 메타데이터와 데이터의 분리&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-객체-저장소의-핵심-특성-불변성--key-value&quot;&gt;3.1. 객체 저장소의 핵심 특성: 불변성 &amp;amp; Key-Value&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-전체-시스템-컴포넌트-구조&quot;&gt;3.2. 전체 시스템 컴포넌트 구조&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-핵심-워크플로우&quot;&gt;3.3. 핵심 워크플로우&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#331-객체-업로드&quot;&gt;3.3.1. 객체 업로드&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#332-객체-다운로드&quot;&gt;3.3.2. 객체 다운로드&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-상세-설계&quot;&gt;4. 상세 설계&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-데이터-저장소-아키텍처-routing-placement-service-data-node&quot;&gt;4.1. 데이터 저장소 아키텍처: Routing, Placement Service, Data Node&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#42-placement-service-와-raft-합의-프로토콜-안정해시의-역할&quot;&gt;4.2. Placement Service 와 Raft 합의 프로토콜, 안정해시의 역할&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#합의-프로토콜과-raft란&quot;&gt;💡합의 프로토콜과 Raft란?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#43-데이터-다중화-흐름&quot;&gt;4.3. 데이터 다중화 흐름&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#431-placement-service와-안정-해시consistent-hashing의-역할&quot;&gt;4.3.1. Placement Service와 안정 해시(Consistent Hashing)의 역할&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#노드-재배치를-줄이는-안정-해시를-왜-다중화-그룹-계산에-사용할까&quot;&gt;💡노드 재배치를 줄이는 안정 해시를 왜 ‘다중화 그룹 계산’에 사용할까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#432-데이터-일관성과-latency-간의-트레이드오프&quot;&gt;4.3.2. 데이터 일관성과 Latency 간의 트레이드오프&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#44-데이터-노드의-동작-및-wal-기반-순차-쓰기-구조&quot;&gt;4.4. 데이터 노드의 동작 및 WAL 기반 순차 쓰기 구조&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#45-객체-위치-검색-및-로컬-db-선택-기준&quot;&gt;4.5. 객체 위치 검색 및 로컬 DB 선택 기준&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#b-tree-기반-rdb-vs-rocksdblsm-tree-비교&quot;&gt;💡B+ Tree 기반 RDB vs RocksDB(LSM-Tree) 비교&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#왜-하필-sqlite일까&quot;&gt;💡왜 하필 SQLite일까?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#46-데이터-저장-흐름&quot;&gt;4.6. 데이터 저장 흐름&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#47-데이터-내구성-다중화-vs-소거-코드&quot;&gt;4.7. 데이터 내구성: 다중화 vs 소거 코드&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#471-하드웨어-장애와-장애-도메인&quot;&gt;4.7.1. 하드웨어 장애와 장애 도메인&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#472-소거-코드erasure-coding&quot;&gt;4.7.2. 소거 코드(Erasure coding)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#4721-42-소거-코드의-데이터-복원-절차&quot;&gt;4.7.2.1. 4+2 소거 코드의 데이터 복원 절차&lt;/a&gt;
                &lt;ul&gt;
                  &lt;li&gt;&lt;a href=&quot;#남은-값들로-어떻게-d_3-d_4를-복원할까&quot;&gt;💡남은 값들로 어떻게 \(d_3, d_4\)를 복원할까?&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
              &lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#4722-84-소거-코드와-장애-도메인-결합&quot;&gt;4.7.2.2. 8+4 소거 코드와 장애 도메인 결합&lt;/a&gt;
                &lt;ul&gt;
                  &lt;li&gt;&lt;a href=&quot;#84-소거-코드에서-원본-데이터를-복원할-수-있는-최대-노드-장애-개수는-왜-4개일까&quot;&gt;💡8+4 소거 코드에서 원본 데이터를 복원할 수 있는 최대 노드 장애 개수는 왜 4개일까?&lt;/a&gt;&lt;/li&gt;
                &lt;/ul&gt;
              &lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#4723-저장-용량-오버헤드-비교&quot;&gt;4.7.2.3. 저장 용량 오버헤드 비교&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#다중화-vs-소거-코드-비교&quot;&gt;다중화 vs 소거 코드 비교&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#473-정확성-검증-checksum&quot;&gt;4.7.3. 정확성 검증: Checksum&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#소거-코드는-메모리나-네트워크-상에서-발생한-데이터-훼손을-복구하지-못하는-걸까&quot;&gt;💡소거 코드는 메모리나 네트워크 상에서 발생한 데이터 훼손을 복구하지 못하는 걸까?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#체크섬-알고리즘-최신-기술-트렌드&quot;&gt;🔥체크섬 알고리즘 최신 기술 트렌드&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#48-메타데이터-데이터-모델-및-규모-확장&quot;&gt;4.8. 메타데이터 데이터 모델 및 규모 확장&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#481-스키마-설계&quot;&gt;4.8.1. 스키마 설계&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#482-bucket-테이블-규모-확장&quot;&gt;4.8.2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket&lt;/code&gt; 테이블 규모 확장&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#483-object-테이블-규모-확장-sharding&quot;&gt;4.8.3. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object&lt;/code&gt; 테이블 규모 확장: sharding&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#49-버킷-내-객체-목록-확인&quot;&gt;4.9. 버킷 내 객체 목록 확인&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#491-디렉터리-계층-구조-시뮬레이션-및-cli-동작&quot;&gt;4.9.1. 디렉터리 계층 구조 시뮬레이션 및 CLI 동작&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#492-단일-데이터베이스-서버에서의-목록-조회-및-애플리케이션-파싱&quot;&gt;4.9.2. 단일 데이터베이스 서버에서의 목록 조회 및 애플리케이션 파싱&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#493-분산-데이터베이스샤딩-환경에서의-목록-조회-및-오프셋-추적의-난제&quot;&gt;4.9.3. 분산 데이터베이스(샤딩) 환경에서의 목록 조회 및 오프셋 추적의 난제&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#494-객체-저장소의-아키텍처-타협점trade-off&quot;&gt;4.9.4. 객체 저장소의 아키텍처 타협점(Trade-off)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#실무에서의-rdb-샤딩-시-페이징-기법은-무엇이-있을까&quot;&gt;💡실무에서의 RDB 샤딩 시 페이징 기법은 무엇이 있을까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#410-객체-버전-관리versioning&quot;&gt;4.10. 객체 버전 관리(Versioning)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#4101-객체-업로드put-흐름&quot;&gt;4.10.1. 객체 업로드(PUT) 흐름&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#4102-삭제-표식delete-marker을-통한-비파괴적-삭제-메커니즘&quot;&gt;4.10.2. 삭제 표식(Delete Marker)을 통한 비파괴적 삭제 메커니즘&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#411-대용량-업로드-최적화-multipart-upload와-etag&quot;&gt;4.11. 대용량 업로드 최적화: Multipart Upload와 ETag&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#4111-멀티파트-업로드-3단계-처리-파이프라인&quot;&gt;4.11.1. 멀티파트 업로드 3단계 처리 파이프라인&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#일반적인-백엔드의-multipartform-data-vs-객체-저장소의-multipart-upload-차이점&quot;&gt;💡일반적인 백엔드의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;multipart/form-data&lt;/code&gt; vs 객체 저장소의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Multipart Upload&lt;/code&gt; 차이점&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#일반-백엔드-개발에서-s3처럼-진짜-분할-업로드를-구현하려면&quot;&gt;💡일반 백엔드 개발에서 S3처럼 ‘진짜 분할 업로드’를 구현하려면?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#412-garbage-collection&quot;&gt;4.12. Garbage Collection&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#4121-garbage가-발생하는-주요-원인&quot;&gt;4.12.1. Garbage가 발생하는 주요 원인&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#4122-데이터-노드-내부의-compaction-메커니즘&quot;&gt;4.12.2. 데이터 노드 내부의 Compaction 메커니즘&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#백그라운드-데몬daemon이란&quot;&gt;💡백그라운드 데몬(Daemon)이란?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#객체-저장소의-발전-방향&quot;&gt;🔥객체 저장소의 발전 방향&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#오프로딩offloading이란&quot;&gt;💡오프로딩(Offloading)이란?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-저장소-시스템-101&quot;&gt;1. 저장소 시스템 101&lt;/h1&gt;

&lt;p&gt;아마존 S3(Simple Storage Service)와 같은 대규모 객체 저장소를 설계하기에 앞서, 3대 저장소 시스템의 기본 개념과 작동 원리를 파악해본다.&lt;/p&gt;

&lt;p&gt;저장소 시스템은 물리적/논리적 구조에 따라 크게 &lt;strong&gt;블록(Block) 저장소, 파일(File) 저장소, 객체(Object) 저장소&lt;/strong&gt;의 3가지 부류로 나뉜다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-블록-저장소block-storage&quot;&gt;1.1. 블록 저장소(Block Storage)&lt;/h2&gt;

&lt;p&gt;블록 저장소는 데이터를 가공되지 않은 정해진 크기의 원시 블록(Raw Block) 조각으로 나누어 관리하며, 서버에 &lt;strong&gt;Volume&lt;/strong&gt; 형태로 직접 할당하는 가장 유연하고 
최상위 성능을 제공하는 저장소 시스템이다.&lt;/p&gt;

&lt;p&gt;HDD(Hard Disk Drive)나 SSD(Solid State Drive)처럼 서버에 물리적으로 직접 연결되는 드라이브가 대표적인 블록 저장소의 형태이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동작 원리 및 접근 방식&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;서버는 제공받은 원시 블록은 ext4, NTFS 등의 파일 시스템으로 포맷하여 사용하거나, DB나 VM 엔진 같은 고성능 애플리케이션에 블록 제어권을 직접 넘긴다.&lt;/li&gt;
      &lt;li&gt;애플리케이션이 파일 시스템을 거치지 않고 디스크 블록에 직접 I/O를 수행할 수 있기 때문에 최상의 입출력 성능(IOPS)을 끌어낼 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;네트워크 연결 프로토콜&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;로컬 물리 드라이브에 국한되지 않고, 고속 전용 네트워크 프로토콜인 &lt;a href=&quot;https://en.wikipedia.org/wiki/Fibre_Channel&quot;&gt;FC(Fibre Channel)&lt;/a&gt;나 &lt;a href=&quot;https://en.wikipedia.org/wiki/ISCSI&quot;&gt;iSCSI&lt;/a&gt;를 통해 SAN(Storage Area Network) 형태로 멀리 떨어진 서버에 결합될 수도 있다.&lt;/li&gt;
      &lt;li&gt;서버 입장에서는 네트워크 전송 여부와 상관없이 내부에 장착된 물리 드라이브와 완전히 동일하게 인식된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;12-파일-저장소file-storage&quot;&gt;1.2. 파일 저장소(File Storage)&lt;/h2&gt;

&lt;p&gt;파일 저장소는 블록 저장소 레이어 위에 계층적 디렉터리 구조를 구현하며, 파일과 폴더 단위로 데이터를 다룰 수 있도록 높은 수준의 추상화를 제공하는 파일 시스템 중심의 저장소이다.&lt;/p&gt;

&lt;p&gt;파일 저장소는 우리가 일상적으로 사용하는 OS의 파일 탐색기와 가장 유사한 범용 저장소 솔루션이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동작 원리 및 접근 방식&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Server_Message_Block&quot;&gt;SMB/CIFS(Windows 계열)&lt;/a&gt;나 &lt;a href=&quot;https://en.wikipedia.org/wiki/Network_File_System&quot;&gt;NFS(Linux 계열)&lt;/a&gt;와 같은 파일 수준 네트워크 프로토콜을 이용해 &lt;strong&gt;NAS(Network Attached Storage)&lt;/strong&gt; 형태로 구현된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클라이언트 서버가 원시 블록 제어나 볼륨 포맷 같은 까다로운 드라이버 레이어 작업을 신경 쓸 필요 없이 손쉽게 네트워크 망에서 파일과 폴더를 읽고 쓸 수 있다.&lt;/li&gt;
      &lt;li&gt;조직 구성원 간의 공유 폴더나 중앙 집중식 문서 보관소로 적합하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;13-객체-저장소object-storage&quot;&gt;1.3. 객체 저장소(Object Storage)&lt;/h2&gt;

&lt;p&gt;객체 저장소는 디렉터리 구조를 없앤 수평적(Flat) 구조 내에 데이터와 메타데이터를 하나의 객체(Object)로 묶어 보관하며, RESTful API를 통해 접근하는 무한 확장이 가능한 저비용 저장소이다.&lt;/p&gt;

&lt;p&gt;객체 저장소는 데이터 영속성을 극대화하고 페타바이트(PB) 이상의 대규모 인프라를 지원하는 한편, 전체 구축 비용을 낮추기 위해 &lt;strong&gt;실시간 점진적 수직 성능을 의도적으로 희생&lt;/strong&gt;한 시스템이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동작 원리 및 접근 방식&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 수정이 자주 발생하지 않는 Cold Data, 데이터 아카이빙, 백업, 미디어 파일 보관에 주로 쓰인다.&lt;/li&gt;
      &lt;li&gt;계층적 디렉터리가 없으며, 오직 고유한 키(URI) 기반의 RESTful API(HTTP GET, PUT, DELETE 등)로 데이터를 교환한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;대표 제품&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;AWS S3(Simple Storage Service), MS Azure Blob Storage, Google Cloud Storage 등&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;14-block-vs-file-vs-object&quot;&gt;1.4. Block vs File vs Object&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/storage.png&quot; alt=&quot;저장소 유형&quot; /&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;구분&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;블록 저장소&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;파일 저장소&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;객체 저장소&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;저장 내용 변경 가능성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Y&lt;/strong&gt; (부분 덮어쓰기 가능)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Y&lt;/strong&gt; (부분 덮어쓰기 가능)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;N&lt;/strong&gt; (직접 부분 수정 불가, 객체 버전을 통한 전체 대체만 가능)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;비용&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;고비용&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;중~고비용&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;저비용&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;성능&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;중~고 혹은 최상 (최저 Latency)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;중~고&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;저~중 (상대적 수십~수백ms 지연)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;데이터 일관성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;강력한 일관성(Strong Consistency)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;강력한 일관성(Strong Consistency)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;a href=&quot;https://aws.amazon.com/ko/s3/consistency/&quot;&gt;강력한 일관성&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;데이터 접근 프로토콜&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Serial_Attached_SCSI&quot;&gt;SAS&lt;/a&gt; / iSCSI / FC&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;표준 파일 접근, CIFS/SMB, NFS&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;HTTP RESTful API&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;규모 확장성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;중 (단일 서버/SAN 한계)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;고 (NAS 노드 확장)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;최상 (무한 수평 확장 가능)&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;적합한 응용처&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;VM 디스크, 데이터베이스(RDB)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;범용 파일 시스템, 조직 내 파일 공유&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;이진 데이터, 대용량 비구조화 데이터&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;15-객체-저장소-핵심-용어&quot;&gt;1.5. 객체 저장소 핵심 용어&lt;/h2&gt;

&lt;p&gt;객체 저장소의 아키텍처와 API를 이해하기 위한 기본 용어 5가지이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;+-----------------------------------------------------------------------------------+
| 버킷 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Bucket: 전역 유일 논리 컨테이너&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                                                 |
|  |                                                                                |
|  +--&amp;gt; 객체 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Object 1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;: URI &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; /bucket-name/image.png                               |
|  |     ├── 데이터 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Payload&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;: Binary Data &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;01010011...]                             |
|  |     └── 메타데이터 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Metadata&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;: Key-Value Pairs &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;Content-Type: image/png, ...&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;     |
|  |                                                                                |
|  +--&amp;gt; 객체 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Object 2&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;: URI &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; /bucket-name/document.pdf &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Version: v2&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;              |
+-----------------------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;버킷(Bucket)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;객체들을 보관하는 최상위 논리적 컨테이너&lt;/li&gt;
      &lt;li&gt;전 세계 모든 사용자가 공유하는 시스템 내에서 버킷 이름은 전역적으로 유일해야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;객체(Object)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;버킷 안에 저장되는 개별 데이터 단위&lt;/li&gt;
      &lt;li&gt;실제 바이너리 데이터 파일인 페이로드와, 해당 객체의 속성/이름/권한 등을 설명하는 메타데이터(Key-Value 쌍)로 구성됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;버전(Version)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;동일한 버킷 내에서 한 객체의 변경 이력을 여러 버전으로 동시 유지할 수 있는 기능&lt;/li&gt;
      &lt;li&gt;버킷 단위로 활성화 여부를 설정하며, 실수로 인한 파일 삭제나 악의적인 덮어쓰기로부터 데이터를 안전하게 복구할 수 있는 방어 장치가 됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;URI(Uniform Resource Identifier)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;객체 저장소는 RESTful API를 기반으로 동작하므로 디렉터리 경로 대신 HTTP URL/URI 엔드포인트를 통해 각 객체를 고유하게 식별함&lt;/li&gt;
      &lt;li&gt;예) &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /my-bucket/images/logo.png&lt;/code&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;SLA(Service-Level Agreement)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;서비스 제공자와 클라이언트 간 맺어지는 서비스 품질 보장 계약 수준&lt;/li&gt;
      &lt;li&gt;예를 들어 &lt;a href=&quot;https://aws.amazon.com/ko/s3/sla/&quot;&gt;아마존 S3 Standard-IA(Standard-Infrequent Access) 저장소 클래스의 SLA 규격&lt;/a&gt;은 다음과 같은 수치를 지킨다.
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;데이터 내구성(Durability)&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;여러 가용성 구역(AZ)에 걸쳐 &lt;strong&gt;99.999999999(11-nine)&lt;/strong&gt; 수준의 객체 내구성 제공(1개 AZ 전체 소실 시에도 데이터 복원 보장)&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;서비스 가용성&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;연간 &lt;strong&gt;99.9%&lt;/strong&gt; 이상의 업타임 보장&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-문제-이해-및-설계-범위-확정&quot;&gt;2. 문제 이해 및 설계 범위 확정&lt;/h1&gt;

&lt;h2 id=&quot;21-요구사항-정의&quot;&gt;2.1. 요구사항 정의&lt;/h2&gt;

&lt;p&gt;실무 아키텍처 수립의 첫 단계는 요구사항을 명확히 하고 시스템의 규모를 가늠하는 것이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 어떤 기능을 지원해야 하는가?
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 아마존 S3와 유사한 객체 저장소로서 아래 핵심 기능을 지원해야 한다.
        &lt;ul&gt;
          &lt;li&gt;Bucket 생성&lt;/li&gt;
          &lt;li&gt;객체 업로드 및 다운로드&lt;/li&gt;
          &lt;li&gt;객체 버전 관리(Versioning)&lt;/li&gt;
          &lt;li&gt;버킷 내 목록 출력(&lt;a href=&quot;https://docs.aws.amazon.com/cli/latest/reference/s3/ls.html&quot;&gt;aws s3 ls&lt;/a&gt; CLI와 유사한 접두어 기반 조회)&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 데이터 크기는 어느 정도인가?
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 수 GB 이상의 아주 큰 객체부터 수 KB 정도의 다량의 소형 객체까지 다양하게 저장할 수 있어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 매년 시스템에 추가되는 데이터의 양은 어느 정도인가?
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 매년 약 100PB(Petabyte)의 데이터가 새롭게 추가된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 99.9999%의 데이터 내구성과 99.99%의 서비스 가용성을 보장해야 하는가?
    &lt;ul&gt;
      &lt;li&gt;
        &lt;h2 id=&quot;a-그렇다-높은-내구성과-가용성을-동시에-달성해야-한다&quot;&gt;&lt;strong&gt;A:&lt;/strong&gt; 그렇다. 높은 내구성과 가용성을 동시에 달성해야 한다.&lt;/h2&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;비기능 요구사항&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;데이터 규모&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;연간 100PB 신규 데이터 저장&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 내구성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;99.9999%(Six-Nine)&lt;/strong&gt; 이상의 데이터 손실 방지&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;서비스 가용성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;99.99%(Four-Nine)&lt;/strong&gt; 이상의 시스템 정상 작동 보장&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;저장소 효율성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;높은 수준의 안정성과 성능을 보장하되, 인프라 저장 비용(TCO, Total Cost of Ownership)을 최대한 낮추어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;(TCO, Total Cost of Ownership)&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;총소유비용&lt;br /&gt;
단순히 하드웨어나 소프트웨어를 구매할 때 드는 초기 도입 비용 뿐 아니라, 이를 구축/운영/유지보수/폐기할 때까지 전체 수명 주기 동안 발생하는 모든 직/간접 비용을 합산한 개념&lt;/p&gt;

  &lt;p&gt;여기서는 단순히 디스크나 스토리지의 구매 단가만 낮추는 것이 아니라, 전력 소모/데이터 압축 효율/관리 인력 공수/확장 시 발생하는 부대 비용 등 운영 전반의 모든 숨은 비용을 최적화해야 한다는 의미임&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-대략적인-규모-추정&quot;&gt;2.2. 대략적인 규모 추정&lt;/h2&gt;

&lt;p&gt;객체 저장소 시스템 설계에서는 디스크 용량과 초당 디스크 입출력 처리량(IOPS: Input/Output Operation Per Second)이 핵심 병목 구간이 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;디스크 용량 산정 조건&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;저장되는 객체의 크기가 아래의 분포를 따른다고 가정해보자.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;소형 객체(20%)&lt;/strong&gt;: 1MB 미만&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;중형 객체(60%)&lt;/strong&gt;: 1MB~64MB&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;대형 객체(20%)&lt;/strong&gt;: 64MB 이상&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;계산 편의성을 위해 중앙값(Median)을 사용한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;소형 객체(1MB 미만)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;0MB~1MB 대역의 중앙에 위치하는 &lt;strong&gt;0.5MB&lt;/strong&gt; 적용&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;중형 객체(1MB~64MB)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;1MB~64MB 대역의 중앙 부근인 &lt;strong&gt;32MB&lt;/strong&gt; 적용&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;대형 객체(64MB 이상)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;64MB부터 수 GB에 이르는 대형 객체 분포의 대표 중앙값으로 &lt;strong&gt;200MB&lt;/strong&gt; 적용&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;중앙값(Median)&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;통계학에서 중앙값은 데이터를 크기 순으로 나열했을 때 정확히 가운데 위치하는 값이다.&lt;br /&gt;
파일 크기처럼 1KB짜리 극소형 파일부터 수십 GB짜리 극대형 파일까지 폭넓게 섞여 있는 &lt;strong&gt;치우친(Skewed) 분포&lt;/strong&gt;에서는 단순 ‘평균(Mean)’을 사용하면 
극단적으로 큰 파일 때문에 전체 결과가 왜곡된다.&lt;br /&gt;
따라서 범주의 대표 특성을 가장 잘 반영하는 &lt;strong&gt;중앙값&lt;/strong&gt;을 기준 수치로 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;IOPS 가정&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SATA 인터페이스 규격의 7,200 rpm HDD 1대는 초당 100~150회의 임의 데이터 탐색(Random I/O)을 지원한다. (= &lt;strong&gt;100~150 IOPS&lt;/strong&gt;)&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;수용 가능한 객체 수 산출 계산&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;안정적인 하드웨어 운용을 위해 디스크의 40% 저장 공간 사용률(Storage Utilization)을 유지한다고 가정할 때, 100PB 공간에 수용 가능한 객체 수를 계산해보자.&lt;/p&gt;

\[100\text{ PB} = 100 \times \underbrace{1,000}_{(\text{PB } \rightarrow \text{ TB})} \times \underbrace{1,000}_{(\text{TB } \rightarrow \text{ GB})} \times \underbrace{1,000}_{(\text{GB } \rightarrow \text{ MB})} \text{ MB} = 10^{11}\text{ MB}\]

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;객체 1개당 평균 가중 용량&lt;/strong&gt;&lt;/p&gt;

\[(0.2 \times 0.5\text{MB}) + (0.6 \times 32\text{MB}) + (0.2 \times 200\text{MB}) = 0.1\text{MB} + 19.2\text{MB} + 40\text{MB} = 59.3\text{MB}\]

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;수용 가능한 총 객체 수&lt;/strong&gt;&lt;/p&gt;

\[\frac{10^{11}\text{MB} \times 0.4}{59.3\text{MB}} \approx 674,536,256 \approx \mathbf{6\text{억 } 8\text{천만 개}}\]

&lt;p&gt;모든 객체의 메타데이터 크기를 평균 1KB로 가정하면, 약 6억 8천만 개 객체의 메타데이터를 저장하기 위해 약 &lt;strong&gt;0.68TB&lt;/strong&gt;의 DB 저장 공간이 필요하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-개략적-설계안-메타데이터와-데이터의-분리&quot;&gt;3. 개략적 설계안: 메타데이터와 데이터의 분리&lt;/h1&gt;

&lt;h2 id=&quot;31-객체-저장소의-핵심-특성-불변성--key-value&quot;&gt;3.1. 객체 저장소의 핵심 특성: 불변성 &amp;amp; Key-Value&lt;/h2&gt;

&lt;p&gt;설계에 들어가기 앞서 객체 저장소가 가진 고유한 4가지 속성을 이해해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;객체 불변성(Object immutability)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;블록/파일 저장소와 달리 객체 저장소에 저장된 데이터는 부분 수정이 불가능하다. 데이터를 변경하려면 삭제 후 전체를 새 버전 객체로 대체해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;키-값 저장소(Key-Value Store)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;객체의 URI가 Key가 되고, 실제 바이너리 데이터가 Value가 되는 키-값 대응 구조이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-http highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;요청:
&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;GET&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;/bucket1/object1.txt&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;HTTP&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1.1&lt;/span&gt;

응답:
HTTP/1.1 200 OK
Content-Length: 1234

[해당 객체의 바이너리 데이터 1234 바이트]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;WORM(Write Once, Read Many)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 접근 패턴 측면에서 쓰기는 1회 발생하지만 읽기는 수없이 반복적으로 발생한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;다양한 객체 크기 동시 지원&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수 KB 소형 파일부터 수 GB 대형 파일까지 문제없이 저장할 수 있어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;WORM(Write Once, Read Many)&lt;/strong&gt;&lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;&lt;strong&gt;Write Once:&lt;/strong&gt; 데이터를 딱 한 번만 저장(생성)할 수 있다. 저장된 데이터는 지정된 보존 기간 동안 수정, 덮어쓰기, 삭제가 불가능하다.
      &lt;ul&gt;
        &lt;li&gt;이를 &lt;strong&gt;데이터 불변성(Immutability)&lt;/strong&gt;이라고 한다.&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;Read Many:&lt;/strong&gt; 수정이나 삭제는 안 되지만, 저장된 데이터를 조회하고 읽는 것은 자유롭다.&lt;/li&gt;
  &lt;/ul&gt;

  &lt;p&gt;&lt;strong&gt;객체 저장소에서 WORM을 쓰는 이유는?&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;랜섬웨어 방어
      &lt;ul&gt;
        &lt;li&gt;랜섬웨어가 시스템에 침투하더라도, WORM 설정이 적용된 객체 저장소의 데이터는 암호화하거나 강제 삭제 불가&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;법적 보존 의무 준수
      &lt;ul&gt;
        &lt;li&gt;금융, 의료, 법률 분야처럼 ‘거래 내역이나 진료 기록을 최소 5년간 원본 그대로 보존해야 한다’는 법적 규제가 있을 때 필수적으로 사용
          &lt;ul&gt;
            &lt;li&gt;(예: AWS S3의 Object Lock 기능이 대표적인 WORM 구현 형태)&lt;/li&gt;
          &lt;/ul&gt;
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;랜섬웨어(Ransomware)&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;몸값(Ransom)과 소프트웨어(Software)의 합성어로, 컴퓨터 내의 데이터를 볼모로 잡고 돈을 요구하는 악성 프로그램&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;UNIX 파일 시스템과의 구조적 유사성&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;객체 저장소의 아키텍처 철학은 &lt;strong&gt;UNIX 파일 시스템의 아키텍처와 매우 유사&lt;/strong&gt;하다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/unix.png&quot; alt=&quot;UNIX 파일 시스템과 객체 저장소&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;UNIX 파일 시스템은 파일 이름과 메타데이터를 &lt;a href=&quot;https://en.wikipedia.org/wiki/Inode&quot;&gt;inode&lt;/a&gt; 구조체에 저장하고, 실제 데이터는 디스크의 물리 데이터 블록에 별도로 보관한다.&lt;/li&gt;
  &lt;li&gt;아이노드에 저장된 파일 블록 포인터를 따라가면서 디스크를 읽는 구조이다.&lt;/li&gt;
  &lt;li&gt;따라서 로컬 파일을 읽을 때 우선 아이노드에 기록된 메타데이터를 읽어서 파일 블록 포인터 목록을 확보한 후 그 포인터를 따라가면서 데이터를 읽어야 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;객체 저장소 역시 이와 동일하게 메타데이터 저장소(inode 역할)와 데이터 저장소(하드디스크 역할)를 물리적으로 엄격히 분리한다.&lt;br /&gt;
단, 메타데이터 저장소에는 파일 블록 포인터 대신 네트워크를 통해 데이터 저장소에 보관된 객체를 요청할 때 필요한 식별자(ID)가 보관된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/bucket.png&quot; alt=&quot;버킷과 객체&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;메타데이터와 실제 데이터 분리의 장점&lt;/strong&gt;&lt;br /&gt;
불변(Immutable) 객체 데이터와 변경 가능한(Mutable) 메타데이터를 분리하면, 데이터 저장소와 메타데이터 저장소를 서로 독립적인 기술 스택으로 확장하고 구현 및 
최적화가 가능하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-전체-시스템-컴포넌트-구조&quot;&gt;3.2. 전체 시스템 컴포넌트 구조&lt;/h2&gt;

&lt;p&gt;아래는 S3 시스템의 개략적인 전체 컴포넌트 구조도이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/draft.png&quot; alt=&quot;개략적 설계안&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;로드밸런서&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자로부터 들어오는 RESTful API HTTP 요청을 여러 API 서버로 균등하게 분산&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;API 서비스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Stateless 서버 레이어로 쉽게 Scale-out이 가능하며, IAM 인증, 메타데이터 조회, 데이터 저장소 호출 등을 총괄 조율&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;IAM(Identity &amp;amp; Access Management)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;인증(Authentication), 접근 권한 부여(Authorization) 및 접근 제어(Access Control List) 등을 검증&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 저장소&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;실제 객체 바이너리 데이터를 저장하고 읽어오는 고성능 저장소 시스템&lt;/li&gt;
      &lt;li&gt;모든 데이터 연산은 객체 고유 ID(UUID)를 통해 수행됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;메타데이터 저장소&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;버킷 정보 및 객체 이름과 ID 간의 매핑 메타데이터를 보관&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-핵심-워크플로우&quot;&gt;3.3. 핵심 워크플로우&lt;/h2&gt;

&lt;h3 id=&quot;331-객체-업로드&quot;&gt;3.3.1. 객체 업로드&lt;/h3&gt;

&lt;p&gt;아래 그림은 bucket-to-share 라는 버킷 생성 후 script.txt를 업로드하는 흐름이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/upload.png&quot; alt=&quot;객체 업로드&quot; /&gt;&lt;/p&gt;

&lt;p&gt;① 클라이언트는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket-to-share&lt;/code&gt; 버킷을 생성하기 위해 HTTP PUT 요청을 보내며, 요청은 API 서비스로 전달된다.&lt;br /&gt;
② API 서비스는 IAM 서비스를 호출하여 사용자의 생성 권한을 확인한다.&lt;br /&gt;
③ 권한 검증 후 API 서비스는 메타데이터 저장소를 호출하여 버킷 정보를 생성 및 등록한다.&lt;br /&gt;
④ 버킷 생성 완료 후, 클라이언트는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;script.txt&lt;/code&gt; 객체를 업로드하기 위해 HTTP PUT 요청을 보낸다.&lt;br /&gt;
⑤ API 서비스는 IAM을 통해 해당 사용자에게 WRITE 권한이 있는지 재확인한다.&lt;br /&gt;
⑥ 검증이 완료되면 API 서비스는 HTTP PUT Body에 실린 객체 데이터를 데이터 저장소로 전송한다.&lt;br /&gt;
데이터 저장소는 데이터를 영속적으로 저장한 후 고유한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_id&lt;/code&gt;(UUID)를 반환한다.&lt;br /&gt;
⑦ API 서비스는 메타데이터 저장소를 호출하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_id(UUID)&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket_id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_name(script.txt)&lt;/code&gt; 매핑 항목을 새롭게 등록한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[객체 업로드 HTTP 요청 예시]&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-http highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;PUT&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;/bucket-to-share/script.txt&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;HTTP&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1.1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;Host&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;foo.s3example.org&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Sun, 12 Sept 2021 17:51:00 GMT&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;[권한 문자열]&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;Content-Type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;text/plain&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;Content-Length&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;4567&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;x-amz-meta-author&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Alex&lt;/span&gt;

[객체 데이터 4567 바이트]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;332-객체-다운로드&quot;&gt;3.3.2. 객체 다운로드&lt;/h3&gt;

&lt;p&gt;객체 저장소는 디렉터리 계층을 지원하지 않지만, 객체 이름에 슬래시(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;)를 포함하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket-to-share/script.txt&lt;/code&gt; 처럼 저장하면 계층 구조를 논리적으로 흉내낼 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[객체 다운로드 HTTP 요청 예시]&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-http highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nf&quot;&gt;GET&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;/bucket-to-share/script.txt&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;HTTP&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;1.1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;Host&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;foo.s3example.org&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Sun, 12 Sept 2021 18:30:01 GMT&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;Authorization&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;[권한 문자열]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/download.png&quot; alt=&quot;객체 다운로드&quot; /&gt;&lt;/p&gt;

&lt;p&gt;① 클라이언트가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /bucket-to-share/script.txt&lt;/code&gt; 요청을 보내면 로드밸런서가 이를 API 서비스로 포워딩한다.&lt;br /&gt;
② API 서비스는 IAM을 통해 사용자의 READ 권한을 검증한다.&lt;br /&gt;
③ 권한 확인이 완료되면, API 서비스는 메타데이터 저장소에서 해당 객체 이름에 매칭된 &lt;strong&gt;UUID&lt;/strong&gt;를 조회해온다.&lt;br /&gt;
④ API 서비스는 확보한 UUID를 가지고 데이터 저장소에 접근하여 실제 객체 데이터를 읽어온다.&lt;br /&gt;
⑤ API 서비스는 읽어온 데이터 페이로드를 HTTP GET 응답에 실어 클라이언트에 반환한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-상세-설계&quot;&gt;4. 상세 설계&lt;/h1&gt;

&lt;p&gt;앞서 살펴본 개략적 구조를 바탕으로, PB 데이터 규모에서 99.9999%의 내구성과 99.99%의 가용성을 달성하기 위한 데이터 저장소와 메타데이터 저장소의 상세 아키텍처에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-데이터-저장소-아키텍처-routing-placement-service-data-node&quot;&gt;4.1. 데이터 저장소 아키텍처: Routing, Placement Service, Data Node&lt;/h2&gt;

&lt;p&gt;API 서비스가 사용자 요청을 받으면 객체 저장 및 조회 연산은 내부 데이터 저장소로 전달된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/updown.png&quot; alt=&quot;객체 업로드/다운로드&quot; /&gt;&lt;/p&gt;

&lt;p&gt;데이터 저장소 내부 핵심 컴포넌트는 다음과 같이 3가지로 나뉜다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/datacomponent.png&quot; alt=&quot;데이터 저장소 컴포넌트&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;데이터 라우팅 서비스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;API 서비스와 데이터 노드 사이에서 HTTP RESTful 또는 &lt;a href=&quot;https://assu10.github.io/dev/2026/07/01/architecture-hotel-reservation-system/#34-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%84-%EA%B3%A0%EC%84%B1%EB%8A%A5-%ED%86%B5%EC%8B%A0-%EC%A0%84%EB%9E%B5-grpc&quot;&gt;gRPC&lt;/a&gt; 
프로토콜 인터페이스를 제공하는 Stateless 레이어&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;배치 서비스(Placement Service)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;가상 클러스터 지도(Virtual Cluster Map)를 관리하며, 데이터를 어느 데이터 노드 다중화 그룹에 배치할지 결정하는 중앙 컨트롤러&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 노드&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;실제로 HDD/SSD를 장착하고 바이너리 데이터를 영속적으로 저장 및 복제하는 노드들&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;42-placement-service-와-raft-합의-프로토콜-안정해시의-역할&quot;&gt;4.2. Placement Service 와 Raft 합의 프로토콜, 안정해시의 역할&lt;/h2&gt;

&lt;p&gt;Placement Service는 데이터 노드의 상태(Heartbeat)를 지속해서 모니터링하고 물리적 형상 정보인 가상 클러스터 지도를 최신 상태로 유지한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/virtual.png&quot; alt=&quot;가상 클러스터 지도&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Placement Service는 중요한 서비스이므로 Placement Service 클러스터는 Paxos 나 Raft 와 같은 합의 프로토콜을 사용할 것을 권장한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;합의-프로토콜과-raft란&quot;&gt;💡합의 프로토콜과 Raft란?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Raft의 개념 및 성격&lt;/strong&gt;&lt;br /&gt;
Raft는 이론적 추상 개념이 아니라, 분산 시스템에서 여러 노드가 &lt;strong&gt;하나의 동일한 상태(State)에 대해 합의(Consensus)&lt;/strong&gt;에 도달하도록 보장하기 위해 설계된 
&lt;strong&gt;실제 동작 알고리즘(Consensus Algorithm)&lt;/strong&gt;이다.&lt;br /&gt;
&lt;a href=&quot;https://en.wikipedia.org/wiki/Paxos_(computer_science)&quot;&gt;팩서스(Paxos)&lt;/a&gt; 알고리즘보다 이해 및 구현하기 쉽도록 개선된 방안이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;다수결(Quorum) 기반의 과반수 합의 원리&lt;/strong&gt;&lt;br /&gt;
5~7개의 노드로 Placement Service 클러스터를 구성하고 Raft 알고리즘을 적용하면, Leader 노드 선출과 클러스터 지도 변경 사항이
&lt;strong&gt;과반수(Quorum, \(\frac{N}{2} + 1\)) 노드의 동의&lt;/strong&gt;를 거쳐 로그로 동기화된다.&lt;br /&gt;
7개 노드 중 최대 3개 노드에 장애가 발생하더라도 4개 노드가 살아있으므로 서비스는 중단 없이 정상 작동한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;43-데이터-다중화-흐름&quot;&gt;4.3. 데이터 다중화 흐름&lt;/h2&gt;

&lt;p&gt;데이터 저장소가 객체를 받아 영속적으로 보관하는 기본 동작 절차는 다음과 같다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API 서비스 → 데이터 라우팅 → Placement Service 조회 → 주 노드 전송 → 부 노드 복제로 이어지는 다중화 메커니즘&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/flow.png&quot; alt=&quot;데이터를 영속적으로 보관하는 흐름&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;① API 서비스의 데이터 전달&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;API 서비스는 클라이언트로부터 받은 객체 데이터를 데이터 저장소 내의 &lt;strong&gt;데이터 라우팅 서비스&lt;/strong&gt;로 포워딩&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;② Placement Service 질의&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 라우팅 서비스는 해당 객체에 &lt;strong&gt;UUID&lt;/strong&gt;를 할당하고, &lt;strong&gt;Placement Service&lt;/strong&gt;에 해당 객체를 보관할 데이터 노드 위치를 질의함&lt;/li&gt;
      &lt;li&gt;Placement Service는 가상 클러스터 지도를 확인하여 데이터를 보관할 &lt;strong&gt;Primary 데이터 노드&lt;/strong&gt; 반환&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;③ 주 데이터 노드로 데이터 전송&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 라우팅 서비스는 저장할 데이터를 UUID와 함께 주 데이터 노드에 &lt;strong&gt;직접&lt;/strong&gt; 전송&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;④ 지역 저장 및 다중화 복제&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;주 데이터 노드는 데이터를 자신의 노드에 지역적으로(Locally) 저장하는 한편, 2개의 &lt;strong&gt;Secondary 데이터 노드&lt;/strong&gt;에 다중화 복제를 진행함&lt;/li&gt;
      &lt;li&gt;주 데이터 노드는 모든 부 데이터 노드에 성공적으로 다중화가 완료되면 데이터 라우팅 서비스에 완료 응답 보냄&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;⑤ UUID 반환&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 라우팅 서비스는 객체의 UUID(객체 ID)를 API 서비스로 반환하며 연산을 마침&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;431-placement-service와-안정-해시consistent-hashing의-역할&quot;&gt;4.3.1. Placement Service와 안정 해시(Consistent Hashing)의 역할&lt;/h3&gt;

&lt;p&gt;위 흐름의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;② Placement Service 질의&lt;/code&gt;에서 Placement Service는 UUID를 입력으로 받아 해당 객체가 저장된 다중화 그룹(Primary + Secondary 노드들)을 도출해낸다.&lt;/p&gt;

&lt;p&gt;이 계산 결과는 항상 결정적(Deterministic)이어야 하며, 노드가 새롭게 추가되거나 장애로 제거되더라도 전체 시스템에 미치는 영향을 최소화해야 한다.&lt;br /&gt;
이를 위해 &lt;strong&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/07/02/architecture-consistent-hashing/&quot;&gt;안정 해시&lt;/a&gt;&lt;/strong&gt; 기술을 활용한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;노드-재배치를-줄이는-안정-해시를-왜-다중화-그룹-계산에-사용할까&quot;&gt;💡노드 재배치를 줄이는 안정 해시를 왜 ‘다중화 그룹 계산’에 사용할까?&lt;/h4&gt;

&lt;p&gt;안정 해시 키 분배 방식은 단순 노드 추가/삭제 시 데이터 이동을 최소화하는 기능 외에도, &lt;strong&gt;대규모 분산 저장소에서 다중화 그룹을 중앙 DB 없이 결정론적으로 찾기 위한 핵심 메커니즘&lt;/strong&gt;으로 사용된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;결정론적 다중화 그룹 지정&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;UUID를 해시 링 위에 매핑한 뒤, 링을 따라 시계 방향으로 가장 먼저 만나는 노드를 &lt;strong&gt;Primary 노드&lt;/strong&gt;로 지정&lt;/li&gt;
      &lt;li&gt;그 후 계속 시계 방향으로 순회하며 서로 다른 물리적 Rack이나 가용성 구역(AZ)에 위치한 노드 2개를 추가로 선택하여 &lt;strong&gt;Secondary 노드&lt;/strong&gt;로 지정&lt;/li&gt;
      &lt;li&gt;이를 통해 별도의 거대한 위치 매핑 테이블 없이도 UUID 만으로 동일한 복제 노드 그룹을 항상 찾아낼 수 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;100PB 규모에서의 데이터 리밸런싱 폭탄 방지&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;새로운 데이터 노드가 증설되거나 특정 노드가 다운되었을 때, 전통적인 Modulo 해싱을 사용하면 전체 100PB의 데이터 배치가 통째로 뒤흔들림&lt;/li&gt;
      &lt;li&gt;안정 해시를 적용하면 &lt;strong&gt;노드가 변동되어도 오직 변동된 해시 구간에 속한 최소한의 데이터만 이동&lt;/strong&gt;하면 되므로 전체 클러스터의 데이터 재배치(Rebalancing) 부하를 극도로 낮출 수 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;432-데이터-일관성과-latency-간의-트레이드오프&quot;&gt;4.3.2. 데이터 일관성과 Latency 간의 트레이드오프&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;④ 지역 저장 및 다중화 복제&lt;/code&gt;의 과정을 보면, 주 데이터 노드는 &lt;strong&gt;모든 부 데이터 노드에 복제 작업이 성공한 후에야&lt;/strong&gt; 완료 응답을 반환한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;데이터 라우팅 서비스] ──&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1. 데이터 전송&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;──&amp;gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;주 데이터 노드]
                                                │
                                                ├──&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;2. 복제 전송&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;──&amp;gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;부 데이터 노드 1] &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;완료&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                                                │
                                                └──&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;3. 복제 전송&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;──&amp;gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;부 데이터 노드 2] &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;느림... 완료&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                                                │
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;데이터 라우팅 서비스] &amp;lt;──&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;4. 최종 완료 응답&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;────┘ &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;가장 느린 부 노드가 끝날 때까지 대기&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점: 강력한 일관성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;응답을 받는 시점에 모든 데이터 노드가 동일한 최신 사본을 보유하므로, 언제 어디서 데이터를 읽어도 강력한 데이터 일관성이 보장됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점: 높은 Latency&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;복제 네트워크 전송 중 &lt;strong&gt;가장 느린 부 데이터 노드의 작업이 끝날 때까지 대기&lt;/strong&gt;해야 하므로, 클라이언트가 체감하는 쓰기 응답 지연은 가장 길어짐&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;데이터를 여러 데이터 노드에 복제할 때, ‘클라이언트에 성공 완료 응답을 언제 보낼 것인가’에 따라 데이터 일관성과 응답 지연 시간 사이의 명확한 트레이드오프가 발생한다.&lt;/p&gt;

&lt;p&gt;아래는 데이터 일관성과 지연 시간 사이의 트레이드오프를 보여준다.
&lt;img src=&quot;/assets/img/dev/2026/0705/relation.png&quot; alt=&quot;데이터 일관성과 지연 시간 사이의 타협적 관계&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;첫 번째 선택지: 3개 노드 전체 저장 완료 후 응답&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터를 주 데이터 노드 및 2개의 부 데이터 노드 전부(총 3개 노드)에 보관하고 나서야 성공적으로 저장되었다고 간주&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;데이터 일관성&lt;/strong&gt; 측면에서는 가장 강력하고 최선이지만, 복제 과정에서 가장 느린 부 노드의 작업이 끝날 때까지 대기해야 하므로 &lt;strong&gt;응답 지연 시간&lt;/strong&gt;이 가장 높다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;두 번째 선택지: 주 노드+부 노드 1개 저장 완료 후 응답&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터를 주 데이터 및 2개의 부 데이터 노드 중 &lt;strong&gt;최소 1개&lt;/strong&gt;에 성공적으로 보관하면 성공으로 간주&lt;/li&gt;
      &lt;li&gt;데이터 일관성과 응답 지연 시간 사이에서 &lt;strong&gt;중간 정도의 합리적인 성능&lt;/strong&gt;을 제공&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;세 번째 선택지: 주 노드 저장 완료 즉시 응답&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터를 &lt;strong&gt;주 데이터에만 보관&lt;/strong&gt;하고 나면 즉시 성공했다고 간주&lt;/li&gt;
      &lt;li&gt;응답 지연 시간이 가장 낮아 &lt;strong&gt;쓰기 속도는 가장 빠르지만&lt;/strong&gt;, 비동기 복제가 완료되기 전에 주 노드에 장애가 생기면 데이터 손실 위험이 있어 &lt;strong&gt;데이터 일관성 측면에서는 가장 취약&lt;/strong&gt;함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;두 번째와 세 번째 선택지 모두 궁극적 일관성(Eventual Consistency)의 한 형태&lt;/strong&gt;로 볼 수 있다.&lt;br /&gt;
클라이언트에 응답이 전달된 직후 짧은 시간 동안은 노드 간 데이터 불일치가 존재할 수 있지만, 약간의 시차가 지난 후 백그라운드 복제 프로세스를 통해 &lt;strong&gt;결국(Eventually) 모든 데이터 노드가 동일한 최신 상태로 동기화&lt;/strong&gt;
되는 특징을 가진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;44-데이터-노드의-동작-및-wal-기반-순차-쓰기-구조&quot;&gt;4.4. 데이터 노드의 동작 및 WAL 기반 순차 쓰기 구조&lt;/h2&gt;

&lt;p&gt;데이터 노드는 주기적으로 Placement Service에 Heartbeat 메시지를 전송한다. 15초 이상 응답이 없는 노드는 Down 상태로 표시된다.&lt;/p&gt;

&lt;p&gt;데이터 저장 시 소형 객체를 개별 파일로 직접 디스크에 저장하면 두 가지 치명적 한계가 발생한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;디스크 블록 낭비&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;파일 시스템의 기본 블록 크기(보통 4KB)보다 작은 파일(예: 1KB)을 저장해도 블록 1개를 온전히 차지하므로 디스크 공간이 낭비됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;inode 소진&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;OS 디스크 포맷 시 생성되는 아이노드 수량이 소진되어 공간이 남아도 더 이상 파일을 생성할 수 없게 되며, OS의 메타데이터 캐싱 성능이 급격히 저하된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 문제를 해결하기 위해 여러 작은 객체를 하나의 큰 디스크 파일에 일렬로 연이어 기록(&lt;a href=&quot;https://assu10.github.io/dev/2026/06/12/architecture-distributed-message-queue-architecture/#412-%EC%84%A0%ED%83%9D%EC%A7%80-2-%EC%93%B0%EA%B8%B0-%EC%9A%B0%EC%84%A0-%EB%A1%9C%EA%B7%B8wal-write-ahead-log%EC%B1%84%ED%83%9D&quot;&gt;WAL(Write-Ahead Log) 방식&lt;/a&gt;)한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/small.png&quot; alt=&quot;작은 객체들을 한 파일에 저장하는 방안&quot; /&gt;&lt;/p&gt;

&lt;p&gt;용량 임계치에 도달한 파일은 읽기 전용 파일로 변경하고 새로운 파일을 만들며, 읽기 전용 파일로 변경된 파일은 오직 읽기 요청만 처리한다.&lt;/p&gt;

&lt;p&gt;읽기-쓰기 파일에 대한 쓰기 연산은 순차적으로 이루어져야 한다.&lt;br /&gt;
위 그림에서 보듯이 객체는 파일에 일렬로 저장되는데, 이를 유지하려면 여러 CPU 코어가 쓰기 연산을 병렬로 진행하더라도 객체 내용이 뒤섞이는 일은 없어야 한다.&lt;br /&gt;
파일에 객체를 기록하기 위해 자기 순서를 기다려야 한다는 뜻이기도 한데, 많은 코어를 가진 서버 시스템의 경우 대역폭이 심각하게 줄어든다는 문제가 있다.&lt;br /&gt;
즉, 여러 CPU 코어가 단 하나의 읽기-쓰기 파일에 동시에 데이터를 기록하려고 하면 디스크 Lock 경합이 발생하여 병렬 대역폭이 급격히 떨어진다.&lt;br /&gt;
따라서 &lt;strong&gt;CPU 코어마다 전담 읽기-쓰기 파일(예: Core-1 전용 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/a&lt;/code&gt;, Core-2 전용 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/b&lt;/code&gt;)을 각각 할당&lt;/strong&gt;하여 병렬 디스크 I/O 처리 성능을 최대한으로 끌어올린다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;45-객체-위치-검색-및-로컬-db-선택-기준&quot;&gt;4.5. 객체 위치 검색 및 로컬 DB 선택 기준&lt;/h2&gt;

&lt;p&gt;하나의 큰 파일에 수만 개의 작은 객체가 묶여 있다면, 특정 UUID를 가진 객체의 위치는 어떻게 빠르게 찾을 수 있을까?&lt;/p&gt;

&lt;p&gt;이를 위해 각 데이터 노드마다 디스크 내 객체 오프셋 정보를 관리하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_mapping&lt;/code&gt; 테이블을 유지한다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;필드&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;설명&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_id&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;객체의 고유 UUID (PK)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file_name&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;객체가 보관된 읽기 전용/쓰기 데이터 파일 경로&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_offset&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;파일 내 객체 데이터가 시작하는 바이트 위치&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_size&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;객체의 바이트 단위 크기&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;위 정보를 저장할 때는 RocksDB 와 같은 파일 기반의 Key-Value Store 를 이용하는 방법과 RDBMS를 이용하는 방법이 있는데 RDBMS 가 더 나은 선택이다.&lt;br /&gt;
그 중에서도 SQLite가 이런 경우 안성맞춤이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;b-tree-기반-rdb-vs-rocksdblsm-tree-비교&quot;&gt;💡B+ Tree 기반 RDB vs RocksDB(LSM-Tree) 비교&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/facebook/rocksdb&quot;&gt;RocksDB&lt;/a&gt;는 LSM-Tree 구조 기반으로 쓰기 속도가 뛰어나지만 읽기 시 여러 &lt;a href=&quot;https://www.igvita.com/2012/02/06/sstable-and-log-structured-storage-leveldb/&quot;&gt;SSTable(Sorted String Table)&lt;/a&gt;을 
확인해야 하므로 읽기 지연이 발생할 수 있다.&lt;br /&gt;
반면, &lt;a href=&quot;https://en.wikipedia.org/wiki/B%2B_tree&quot;&gt;B+Tree&lt;/a&gt; 구조의 RDBMS는 인덱스 노드를 통한 &lt;strong&gt;랜덤 읽기 연산 성능이 압도적&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;객체 저장소 데이터는 &lt;strong&gt;1회 작성 후 수없이 읽히는(WORM)&lt;/strong&gt; 패턴이므로 읽기 성능이 우수한 RDB 구조가 훨씬 유리하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;왜-하필-sqlite일까&quot;&gt;💡왜 하필 SQLite일까?&lt;/h3&gt;

&lt;p&gt;데이터 노드의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_mapping&lt;/code&gt; 정보는 &lt;strong&gt;해당 데이터 노드가 자체 보유한 local 파일의 위치 정보&lt;/strong&gt;에 불과하므로, 
&lt;strong&gt;다른 데이터 노드와 네트워크를 통해 굳이 매핑 데이터를 공유할 필요가 전혀 없다.&lt;/strong&gt;&lt;br /&gt;
따라서 별도의 heavyweight DB 서버(MySQL 등)를 띄우는 대신, 데이터 노드 내부 프로세스에 소켓 통신 오버헤드 없이 임베디드 형태로 가볍게 동작하는 
&lt;strong&gt;파일 기반 &lt;a href=&quot;https://www.sqlite.org/index.html&quot;&gt;SQLite RDB&lt;/a&gt;가 완벽한 최적의 솔루션&lt;/strong&gt;이 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;46-데이터-저장-흐름&quot;&gt;4.6. 데이터 저장 흐름&lt;/h2&gt;

&lt;p&gt;주 데이터 노드가 데이터를 넘겨받았을 때, &lt;strong&gt;디스크 파일 시스템 수준에서 데이터를 실제로 어떻게 보관하는지&lt;/strong&gt;에 대해 알아본다.&lt;br /&gt;
&lt;a href=&quot;#43-데이터-다중화-흐름&quot;&gt;4.3. 데이터 다중화 흐름&lt;/a&gt;에서 Primary 데이터 노드가 데이터를 전달받았을 때 디스크 내부에서 일어나는 과정(Step 3~4)을 좀 더 상세하게 본다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;주 데이터 노드가 1개의 객체를 할당받았을 때, 디스크 내 대용량 파일 Append 순차 쓰기와 SQLite object_mapping 인덱싱이 수행되는 노드 내부 단위&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/enhance.png&quot; alt=&quot;데이터 저장 흐름&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;① 데이터 노드 서비스 수신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;API 서비스(또는 라우팅 서비스)로부터 데이터를 받음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;② WAL 스타일 파일 Append&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;디스크의 읽기-쓰기 전담 파일(예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/c&lt;/code&gt;) 맨 끝에 데이터를 순차적으로 연이어 덧붙임&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;③ 로컬 DB 갱신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;노드 내부의 SQLite &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_mapping&lt;/code&gt; 테이블에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(object_id, file_name, start_offset, object_size)&lt;/code&gt; 레코드 추가&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;④ 결과 반환&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 노드 서비스가 성공 응답 및 UUID를 상위 레이어로 반환&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;여기서 UUID는 데이터 노드가 데이터를 디스크에 저장하는 과정인 위 단계에서 생성되는 것이 아니라, 그 이전 단계인 API 서비스 또는 데이터 라우팅 서비스에서 미리 생성된다.&lt;br /&gt;
Placement Service에 ‘이 데이터를 어느 노드 그룹에 저장해야 할까?’라고 질의할 때 UUID의 해시값(안정 해시)을 기준으로 Primary/Secondary 데이터 노드를 결정해야 하기 때문이다.&lt;br /&gt;
따라서 데이터 노드가 데이터를 넘겨받은 시점에는 이미 UUID가 확정된 상태이며, 데이터 노드는 전달받은 UUID와 데이터 페이로드를 디스크 파일 및 SQLite 메타데이터에 
기록하기만 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;47-데이터-내구성-다중화-vs-소거-코드&quot;&gt;4.7. 데이터 내구성: 다중화 vs 소거 코드&lt;/h2&gt;

&lt;p&gt;Six-Nine(99.9999%) 수준 이상의 데이터 내구성을 제공하는 저장소 시스템을 구축하려면 모든 장비 장애 시나리오를 살피고 데이터를 안전하게 보관해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;471-하드웨어-장애와-장애-도메인&quot;&gt;4.7.1. 하드웨어 장애와 장애 도메인&lt;/h3&gt;

&lt;p&gt;기록 매체의 종류와 관계없이 HDD나 SSD의 장애는 피할 수 없다.&lt;br /&gt;
내구성을 높이는 가장 검증된 방법은 데이터를 여러 대의 드라이브에 복제하여 특정 드라이브에서 장애가 생겨도 전체 데이터 가용성에 영향이 없도록 하는 것이다.&lt;br /&gt;
여기서는 기본적으로 데이터를 3중 복제한다.&lt;/p&gt;

&lt;p&gt;회전식 HDD 드라이브의 연간 장애율(AFR, Annualized Failure Rate)을 &lt;a href=&quot;https://www.backblaze.com/blog/cloud-storage-durability/&quot;&gt;0.81%&lt;/a&gt;라고 가정해보자.&lt;br /&gt;
데이터를 3중 복제하면 대략적인 내구성은 다음과 같이 계산된다.&lt;/p&gt;

\[1 - 0.0081^3 \approx 0.999999 \quad (99.9999\%)\]

&lt;p&gt;완전한 내구성 평가를 위해서는 여러 장애 도메인의 영향을 복합적으로 고려해야 한다.&lt;br /&gt;
&lt;strong&gt;장애 도메인&lt;/strong&gt;이란 중요한 물리적/논리적 구획에 문제가 생겼을 때 부정적인 영향을 공유하는 범위를 말한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;+-------------------------------------------------------------------------+
| Data Center &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;AZ 1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                                                      |
|  +-----------------------------------+  +----------------------------+  |
|  | Rack 1                            |  | Rack 2                     |  |
|  |  &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Server 1] &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Server 2] &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Server 3] |  |  &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Server 4] &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Server 5]     |  |
|  |  &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;공유: 전원 supply, 네트워크 SW&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; |  |                            |  |
|  +-----------------------------------+  +----------------------------+  |
+-------------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Rack 장애 도메인&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 센터 내의 서버들은 보통 19인치 &lt;a href=&quot;https://en.wikipedia.org/wiki/19-inch_rack&quot;&gt;Rack&lt;/a&gt;에 설치된다.&lt;/li&gt;
      &lt;li&gt;하나의 랙에 장착된 모든 서버는 전원 분배 장치(PDU, Power Distribution Unit)와 네트워크 스위치를 공유하므로 해당 랙 전체가 하나의 장애 도메인이 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;서버 노드 장애 도메인&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;서버 장비 내 컴포넌트들은 마더보드, CPU, RAM 등을 공유하므로 해당 서버 노드 자체가 독립된 장애 도메인에 속한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;가용성 구역(AZ)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;다른 데이터 센터와 물리적 전원 및 네트워크 인프라를 공유하지 않는 독립적인 데이터 센터 단위를 말한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/multi.png&quot; alt=&quot;여러 데이터센터를 활용한 데이터 다중화&quot; /&gt;&lt;/p&gt;

&lt;p&gt;데이터를 여러 가용성 구역과 다양한 랙에 분산 복제해 두어야 단일 AZ 소실 같은 대규모 재난 상황에서도 데이터 파손을 막을 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;472-소거-코드erasure-coding&quot;&gt;4.7.2. 소거 코드(Erasure coding)&lt;/h3&gt;

&lt;p&gt;데이터를 3중 다중화하면 99.9999% 내구성을 달성할 수 있지만, 200%에 달하는 막대한 저장 공간 오버헤드가 생긴다.&lt;br /&gt;
원본 1GB에 더해 &lt;strong&gt;추가 복제본 2GB&lt;/strong&gt;(1GB * 2)가 더 필요하므로, 원본 대비 추가로 드는 공간 오버헤드는 &lt;strong&gt;200%&lt;/strong&gt;가 된다. (총 필요 용량은 300%)&lt;/p&gt;

&lt;p&gt;이 비용을 획기적으로 낮추면서 내구성을 올리는 방안이 바로 &lt;a href=&quot;https://en.wikipedia.org/wiki/Erasure_code&quot;&gt;소거 코드&lt;/a&gt;이다.&lt;br /&gt;
소거 코드는 원본 데이터를 작은 단위로 분할하여 서로 다른 서버에 배치하는 한편, 데이터 일부가 소실되었을 때 복구하기 위한 &lt;strong&gt;패리티(Parity)&lt;/strong&gt; 정보를 수학적으로 
계산하여 중복성을 확보한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;4721-42-소거-코드의-데이터-복원-절차&quot;&gt;4.7.2.1. 4+2 소거 코드의 데이터 복원 절차&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/delete.png&quot; alt=&quot;소거 코드를 통한 데이터 복구&quot; /&gt;&lt;/p&gt;

&lt;p&gt;① 원본 데이터를 4개의 같은 크기 단위(\(d_1, d_2, d_3, d_4\))로 분할한다.&lt;br /&gt;
\(d_1, d_2, d_3, d_4\)의 데이터는 모두 다른 내용의 데이터이다.&lt;br /&gt;
원본 파일 바이너리를 동일한 바이트 크기로 4등분한 서로 다른 조각이다.&lt;br /&gt;
예를 들어 ‘일이삼사’라는 텍스트 파일이 있다면 \(d_1=\text{일}\), \(d_2=\text{이}\), \(d_3=\text{삼}\), \(d_4=\text{사}\) 형태로 분할된다.&lt;/p&gt;

&lt;p&gt;② &lt;a href=&quot;https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction&quot;&gt;리드-솔로몬 수학 공식(Reed-Solomon Code)&lt;/a&gt;을 적용하여 패리티 \(p_1, p_2\)를 계산한다.&lt;br /&gt;
이해를 돕기 위해 단순화된 선형 방정식 예시를 들어보면 아래와 같다.&lt;br /&gt;
\(p_1 = d_1 + 2d_2 - d_3 + 4d_4\)&lt;br /&gt;
\(p_2 = -d_1 + 5d_2 + d_3 - 3d_4\)&lt;br /&gt;
여기서 데이터는 ‘일,이, 삼,사’같은 문자나 파일 데이터인데 어떻게 +2, *4 같은 수학 수식이 가능한걸까?&lt;br /&gt;
컴퓨터 내의 모든 데이터는 결국 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;로 구성된 &lt;strong&gt;바이너리 바이트(0~255 숫자)&lt;/strong&gt; 값이다. 예를 들어 ‘일’이라는 글자의 UTF-8 바이트 표기값은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xEC&lt;/code&gt;(236)와 같은 숫자 형태이다.&lt;br /&gt;
소거 코드는 갈루아 체(Galois Field \(GF(2^8)\))의 대수학 수식을 이용하여 각 바이트 숫자에 연산을 직접 적용한다.&lt;/p&gt;

&lt;p&gt;③ 데이터 노드 장치 장애로 인해 \(d_3\)과 \(d_4\) 데이터 조각이 소실된다.&lt;/p&gt;

&lt;p&gt;④ 남은 데이터 (\(d_1, d_2\)) 및 패리티(\(p_1, p_2\))값과 연립 방정식을 결합하면 소실된 \(d_3, d_4\)를 완벽히 복원할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h5 id=&quot;남은-값들로-어떻게-d_3-d_4를-복원할까&quot;&gt;💡남은 값들로 어떻게 \(d_3, d_4\)를 복원할까?&lt;/h5&gt;

&lt;p&gt;미지수가 \(d_3, d_4\) 2개이고, 알려진 수식 \(p_1, p_2\)가 2개인 &lt;strong&gt;2원 1차 연립방정식&lt;/strong&gt; 문제가 된다.&lt;br /&gt;
\(d_3 - 4d_4 = d_1 + 2d_2 - p_1\)&lt;br /&gt;
\(-d_3 + 3d_4 = -d_1 + 5d_2 - p_2\)&lt;/p&gt;

&lt;p&gt;알려진 값 \(d_1, d_2, p_1, p_2\)를 대입하여 두 식을 더하면 \(d_4\)가 도출되고, 순차적으로 \(d_3\)도 완벽히 역산하여 복원 가능하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;4722-84-소거-코드와-장애-도메인-결합&quot;&gt;4.7.2.2. 8+4 소거 코드와 장애 도메인 결합&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/delete2.png&quot; alt=&quot;(8+4) 소거 코드&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림의 각 요소는 아래를 의미한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;최상단 네모들&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;분할 되기 전의 원본 객체 파일들&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;중간 톱니바퀴&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;패리티를 계산하는 수학적 인코더 엔진&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;12개의 중간 도형들(마름모, 원)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;원본 데이터 8조각(\(d_1 ... d_8\)) + 패리티 4조각(\(p_1 ... p_4\)) = 총 12개의 데이터 블록&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;화살표 및 장애 도메인&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;만들어진 12개 조각을 &lt;strong&gt;서로 다른 12개의 독립된 장애 도메인(랙/AZ)&lt;/strong&gt;에 각 1개씩 분산 배치하는 모습&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;8+4 소거코드를 사용하였으므로 원본 데이터는 8조각으로 분할하였고, 4개의 패리티를 계산한다.&lt;br /&gt;
그 결과 만들어진 12조각의 데이터는 전부 같은 크기로 12개의 장애 도메인에 분산한다.&lt;br /&gt;
소거 코드의 수식 덕에 최대 4개 노드에 장애가 동시에 발생하더라도 원본 데이터를 복원할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h5 id=&quot;84-소거-코드에서-원본-데이터를-복원할-수-있는-최대-노드-장애-개수는-왜-4개일까&quot;&gt;💡8+4 소거 코드에서 원본 데이터를 복원할 수 있는 최대 노드 장애 개수는 왜 4개일까?&lt;/h5&gt;

&lt;p&gt;패리티 블록이 4개라는 것은 수학적으로 &lt;strong&gt;독립된 4개의 방정식&lt;/strong&gt;을 가지고 있다는 뜻이다.&lt;br /&gt;
연립방정식의 해를 구하려면 미지수의 개수가 방정식의 개수보다 작거나 같아야 하므로, 소실될 수 있는 최대 조각(미지수)의 수도 &lt;strong&gt;최대 4개&lt;/strong&gt;로 제한된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;데이터를 다중화할 경우 데이터 라우터는 객체 데이터를 하나의 건강한 노드에서 읽으면 충분했지만, 소거 코드를 사용하면 최대 8개의 건강한 노드에서 데이터를 가져와야 한다.&lt;br /&gt;
원본 데이터를 8조각으로 쪼개어 보관했기 때문이다.&lt;br /&gt;
패리티 연산 없이 정상적인 상황에서 원본 객체 하나를 조립해 내려면, &lt;strong&gt;8개의 서로 다른 노드에 저장된 원본 조각 \(d_1 ... d_8\)을 전부 한꺼번에 읽어와야만&lt;/strong&gt; 하나의 파일로 완성할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;4723-저장-용량-오버헤드-비교&quot;&gt;4.7.2.3. 저장 용량 오버헤드 비교&lt;/h4&gt;

&lt;p&gt;소거 코드의 구조적 단점이다.&lt;/p&gt;

&lt;p&gt;응답 지연이 높아지는 대신 내구성은 향상되고, 저장소 비용은 낮아지지만, 객체 저장소는 저장 비용이 대부분이므로 이런 타협적 측면은 고려할 가치가 있다.&lt;/p&gt;

&lt;p&gt;소거 코드를 사용하면 추가로 어느 정도의 저장 용량이 필요할까?&lt;br /&gt;
2개 데이터 블록에 하나의 패리티 블록이 필요하므로 오버헤드는 50%이고, 3중 복제 다중화 방안을 채택하는 경우에는 200% 이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/storage1.png&quot; alt=&quot;다중화가 요구하는 추가 용량 vs 소거 코드가 요구하는 추가 용량&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림에서 다중화는 총 3GB, 소거 코드는 총 1.5GB가 필요하다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;3중 복제&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;원본 1GB + 복제본 2GB = 3GB&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(4+2) 소거코드&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 4 * 0.25GB(=1GB) + 패리티 2 * 0.25GB(=0.5GB) = 1.5GB (50% 오버헤드)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&quot;https://www.backblaze.com/blog/cloud-storage-durability/&quot;&gt;백블레이즈(Backblaze) 사의 통계&lt;/a&gt;에 따르면 8+4 소거 코드를 적용할 경우 
무려 99.999999999%(11-nine) 수준의 내구성을 달성할 수 있다.&lt;/p&gt;

&lt;h3 id=&quot;다중화-vs-소거-코드-비교&quot;&gt;다중화 vs 소거 코드 비교&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;구분&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;다중화 (Replication)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;소거 코드 (Erasure Coding)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;내구성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;99.9999% (3중 복제)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;99.999999999% (8+4 소거 코드) &lt;strong&gt;[우월]&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;저장소 효율성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;200% 오버헤드&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;50% 오버헤드 &lt;strong&gt;[우월]&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;계산 자원&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;추가 연산 없음 &lt;strong&gt;[우월]&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;패리티 계산 및 복원에 높은 CPU 자원 소모&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;쓰기 성능&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;추가 계산 없어 쓰기 성능이 높고, 지연 시간 낮음 &lt;strong&gt;[우월]&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;디스크 기록 전 패리티 연산으로 지연 시간 증가&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;읽기 성능&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;단일 건강 노드에서 즉시 읽기 &lt;strong&gt;[우월]&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;8개 노드 동시 조회 및 장애 시 복원 연산으로 지연 증가&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;요약하면 응답 지연이 중요한 애플리케이션은 다중화 방안을 권장하고, 저장소 비용이 중요하면 소거 코드를 권장한다.&lt;br /&gt;
소거 코드는 비용 효율과 내구성 측면에서는 매력적이지만 데이터 노드 설계 측면에서 까다로우므로 여기서는 다중화 방안에 중점을 두고 설명한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;473-정확성-검증-checksum&quot;&gt;4.7.3. 정확성 검증: Checksum&lt;/h3&gt;

&lt;p&gt;소거 코드를 적용하면 적은 저장 공간 오버헤드로도 디스크 하드웨어 장애에 대비한 높은 데이터 내구성을 얻을 수 있다.&lt;br /&gt;
특정 디스크에 물리적 고장이 생기면 이를 노드 장애로 간주하고, 정상 상태인 다른 노드들의 패리티 데이터를 모아 연립방정식 계산으로 소실된 블록을 복구한다.&lt;/p&gt;

&lt;p&gt;하지만 100PB 이상의 대규모 분산 저장소 환경에서는 디스크 단위의 완전한 정전이나 파손 외에도, &lt;strong&gt;네트워크 전송 중의 비트 뒤틀림(Bit Flip)이나 메모리(RAM) 상의 
데이터가 알 수 없는 이유로 오염되는 ‘무소음 데이터 오염(Silent Data Corruption / Bit Rot)’&lt;/strong&gt; 문제가 빈번하게 발생한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;소거-코드는-메모리나-네트워크-상에서-발생한-데이터-훼손을-복구하지-못하는-걸까&quot;&gt;💡소거 코드는 메모리나 네트워크 상에서 발생한 데이터 훼손을 복구하지 못하는 걸까?&lt;/h4&gt;

&lt;p&gt;결론부터 말하면, &lt;strong&gt;소거 코드는 ‘어느 블록이 훼손되었는지’ 위치를 미리 알려주지 않으면 메모리 오염을 스스로 감지하거나 복구할 수 없다.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;소거 코드의 한계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;소거 코드는 ‘3번 노드의 데이터 조각 \(d_3\)이 유실되었다’라는 명확한 불량 판정이 전제되어야 수학적 역산이 작동한다.&lt;/li&gt;
      &lt;li&gt;데이터가 조용히 뒤틀린 상태에서 소거 코드가 이를 정상 데이터로 착각하고 읽어들이면, 오염된 데이터를 그대로 클라이언트에게 전달하거나 잘못된 패리티 연산을 수행하게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;체크섬과의 협업&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;따라서 데이터를 디스크나 메모리에서 읽어올 때 &lt;strong&gt;체크섬&lt;/strong&gt;을 대조하여 &lt;strong&gt;‘3번 노드의 \(d_3\) 조각이 오염되었다’라고 손상 여부를 먼저 감지해주어야 한다.&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;불량 블록임이 확인되어야 비로소 소거 코드 재구성 연산이 발동하여 다른 정상 노드의 패리티 조각들로부터 깨끗한 원본 \(d_3\)를 복원해낼 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;체크섬의 생성과 검증 원리&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;데이터 훼손 문제는 프로세스 경계 및 전송 구간마다 데이터 정합성 검증을 위한 &lt;a href=&quot;https://en.wikipedia.org/wiki/Checksum&quot;&gt;체크섬(Checksum)&lt;/a&gt;을 두어 해결한다.&lt;br /&gt;
체크섬은 원본 데이터를 입력받아 에러 유무를 판별할 수 있도록 생성한 작고 고유한 해시 데이터 블록이다.&lt;/p&gt;

&lt;p&gt;데이터를 저장하거나 전송할 때 원본 데이터 기반의 체크섬을 미리 계산해둔다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/checksum1.png&quot; alt=&quot;체크섬 생성&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이후 데이터를 가져오거나 네트워크로 수신할 때, 전달받은 데이터로 체크섬을 다시 계산하여 기존 체크섬과 대조한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;재계산한 체크섬이 원본 체크섬과 다를 경우&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메모리, 디스크, 혹은 네트워크 전송 과정에서 데이터 바이너리가 손상된 것으로 확정&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;두 체크섬이 일치할 경우&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;입력값이 1비트만 달라져도 완전히 다른 해시 결과를 내놓는 체크섬 알고리즘 특성상, 데이터가 온전히 보존되었다고 간주
        &lt;ul&gt;
          &lt;li&gt;(해시 충돌 가능성이 극도로 낮아 현실적으로 100% 온전하다고 판단)&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/checksum2.png&quot; alt=&quot;체크섬 계산&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;체크섬 알고리즘과 데이터 노드 저장 위치&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;체크섬 계산 방식으로는 &lt;a href=&quot;https://en.wikipedia.org/wiki/MD5&quot;&gt;MD5&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/SHA-1&quot;&gt;SHA-1&lt;/a&gt;, &lt;a href=&quot;https://en.wikipedia.org/wiki/HMAC&quot;&gt;HMAC&lt;/a&gt; 등 다양한 해시 알고리즘이 존재한다.&lt;br /&gt;
여기서는 계산 방식이 간단하고 빠르게 동작하는 &lt;strong&gt;MD5&lt;/strong&gt;를 예시로 사용한다.&lt;/p&gt;

&lt;p&gt;본 설계안에서 체크섬은 데이터 파일의 맨 끝(Tail)에 덧붙여 보관한다.&lt;br /&gt;
데이터 노드가 디스크 내 전담 파일(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/c&lt;/code&gt;)을 읽기 전용(Read-only) 상태로 전환하기 직전, 파일 전체 데이터에 대한 MD5 체크섬을 계산하여 파일의 마지막 위치에 덧붙여 기록한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/checksum3.png&quot; alt=&quot;데이터 노드에 체크섬 추가&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;체크섬-알고리즘-최신-기술-트렌드&quot;&gt;🔥체크섬 알고리즘 최신 기술 트렌드&lt;/h4&gt;

&lt;p&gt;여기서는 이해하기 쉬운 MD5 알고리즘을 제시하지만, 실제 AWS S3나 Ceph 같은 최신 대규모 프로덕션 객체 저장소에서는 CPU 하드웨어 가속 명령어(SSE4.2/AVX)를 지원하는 
&lt;strong&gt;CRC32C&lt;/strong&gt;나 &lt;strong&gt;xxHash&lt;/strong&gt; 알고리즘을 주로 사용한다.&lt;br /&gt;
MD5 대비 CPU 자원을 수 배 이상 절약하면서도 초당 수 GB의 데이터를 실시간으로 검증할 수 있기 때문이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;(8+4) 소거 코드 + 체크섬 결합 읽기 파이프라인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;(8+4) 소거 코드와 체크섬 검증 메커니즘을 동시 적용한 환경에서, 클라이언트가 객체 데이터를 요청할 때 처리되는 전체 읽기 흐름은 다음과 같다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;데이터 및 체크섬 조회&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 노드는 디스크 전담 파일에서 요청받은 데이터 조각과 함께 파일 끝에 저장되어 있던 원본 체크섬을 조회&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;실시간 체크섬 검증&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;읽어온 데이터 조각으로 체크섬을 즉시 다시 계산하여 저장된 원본 체크섬과 대조&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;체크섬 일치:&lt;/strong&gt; 데이터 오염이 없는 상태이므로 다음 단계로 진행&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;체크섬 불일치:&lt;/strong&gt; 비트 오염 등으로 데이터가 망가진 것이므로 해당 조각을 즉시 폐기하고, &lt;strong&gt;다른 정상 장애 도메인(노드)들에서 패리티 조각들을 읽어와 소거 코드 역산 연산으로 원본 데이터 조각을 복구&lt;/strong&gt;함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;8개 조각 검증 완료&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;원본 객체를 구성하는 8개 데이터 조각에 대해 1~2번 검증 및 복구 과정을 성공적으로 마칠 때까지 반복&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;최종 원본 객체 전달&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;정합성이 완벽히 검증된 8개 조각을 하나의 완성된 원본 파일로 재조립하여 클라이언트에게 최종 반환&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;48-메타데이터-데이터-모델-및-규모-확장&quot;&gt;4.8. 메타데이터 데이터 모델 및 규모 확장&lt;/h2&gt;

&lt;p&gt;객체 저장소의 메타데이터는 객체 이름, 크기, 생성일, 소유자 ID, 액세스 권한, 그리고 실제 객체 데이터 조각이 데이터 노드의 어느 파일/오프셋에 보관되어 있는지에 대한 위치 정보를 관리한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;481-스키마-설계&quot;&gt;4.8.1. 스키마 설계&lt;/h3&gt;

&lt;p&gt;메타데이터 저장소는 객체 이름 기반의 ID 조회, 객체 생성/삭제, 그리고 특정 버킷 내의 객체 목록 조회를 효율적으로 처리할 수 있어야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket&lt;/code&gt; 테이블:&lt;/strong&gt; 버킷 레코드는 사용자가 생성한 저장 영역 단위의 메타데이터를 보관한다.
    &lt;ul&gt;
      &lt;li&gt;bucket_name: 전역적으로 유일한 버킷 이름 문자열(PK)&lt;/li&gt;
      &lt;li&gt;bucket_id: 시스템 내부에서 버킷을 식별하기 위해 사용하는 UUID&lt;/li&gt;
      &lt;li&gt;owner_id: 해당 버킷을 소유한 사용자 또는 계정의 식별자(IAM 권한 검증용)&lt;/li&gt;
      &lt;li&gt;enable_versioning: 해당 버킷의 객체 버전 관리(Versioning) 기능 활성화 여부&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object&lt;/code&gt; 테이블:&lt;/strong&gt; 객체 메타 데이터를 보관한다.
    &lt;ul&gt;
      &lt;li&gt;bucket_id: 객체가 속한 버킷의 ID&lt;/li&gt;
      &lt;li&gt;object_name: 버킷 내에서 객체 경로/이름 문자열&lt;/li&gt;
      &lt;li&gt;object_id: 객체 고유의 UUID&lt;/li&gt;
      &lt;li&gt;object_version: 버전 관리를 위해 부여되는 버전을 식별하는 값(&lt;a href=&quot;https://assu10.github.io/dev/2026/07/04/architecture-distributed-email-service/#timeuuid-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85%EC%9D%B4-%EC%A0%95%EB%A0%AC%EC%97%90-%ED%95%84%EC%88%98%EC%9D%B8-%EC%9D%B4%EC%9C%A0&quot;&gt;TIMEUUID&lt;/a&gt;)&lt;/li&gt;
      &lt;li&gt;file_name: 데이터 노드 내부의 디스크 파일명(예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/c&lt;/code&gt;)&lt;/li&gt;
      &lt;li&gt;start_offset: 해당 파일 내에서 객체 데이터가 시작되는 디스크 바이트 오프셋 위치&lt;/li&gt;
      &lt;li&gt;object_size: 객체의 바이트 단위 크기&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;482-bucket-테이블-규모-확장&quot;&gt;4.8.2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket&lt;/code&gt; 테이블 규모 확장&lt;/h3&gt;

&lt;p&gt;버킷의 총개수는 전체 사용자 수에 비례하며, 사용자당 생성하는 버킷 수가 많지 않기 때문에 상대적으로 용량이 작다.&lt;/p&gt;

&lt;p&gt;예를 들어 &lt;strong&gt;100만 명의 사용자가 각각 10개의 버킷을 생성하고 버킷 레코드당 1KB를 사용&lt;/strong&gt;한다고 가정하면, 전체 데이터 용량은 약 1GB(1,000,000명 * 10개 버킷 * 1KB)에 불과하다.&lt;br /&gt;
따라서 단일 RDB 서버만으로도 전체 버킷 메타데이터 저장 공간을 충분히 커버할 수 있다.&lt;/p&gt;

&lt;p&gt;다만, 버킷 정보 조회가 빈번히 일어나는 읽기 트래픽을 분산하기 위해 &lt;strong&gt;Primary DB - Read Replica DB 구조&lt;/strong&gt;로 다중화하여 확장성을 확보한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;483-object-테이블-규모-확장-sharding&quot;&gt;4.8.3. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object&lt;/code&gt; 테이블 규모 확장: sharding&lt;/h3&gt;

&lt;p&gt;object 테이블은 수십억 개 이상의 레코드를 보관해야 하므로 단일 DB 서버로는 용량과 I/O 성능을 감당할 수 없다.&lt;br /&gt;
따라서 여러 DB 노드로 데이터를 분산하는 샤딩이 필수적이다.&lt;/p&gt;

&lt;p&gt;샤딩 키는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash(bucket_name + object_name)&lt;/code&gt; 순서쌍으로 하는 것을 권장한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket_id&lt;/code&gt;로 샤딩할 경우의 문제&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 대형 기업 고객이 하나의 버킷에 수십억 개의 객체를 저장하면, 해당 bucket_id가 속한 단일 샤드 DB 서버에 트래픽과 용량이 쏠리는 &lt;strong&gt;핫스팟(Hotspot) 문제&lt;/strong&gt;가 발생&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_id&lt;/code&gt;로 샤딩할 경우의 문제&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;샤드는 고르게 분산되지만, REST API 요청인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /bucket-name/object-name&lt;/code&gt; 형태의 URI 조회 시 어떤 샤드에 해당 객체가 있는지 알 방법이 없어 모든 샤드를 전수 조사(Scatter-Gather)해야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 버킷 이름과 객체 이름을 조합한 해시값을 샤딩 키로 쓰면, &lt;strong&gt;특정 대용량 버킷 안의 객체들이 여러 샤드 서버로 아주 고르게 분산 저장&lt;/strong&gt;될 뿐만 아니라, 
URI 조회 시에도 해시값만 계산하여 단 하나의 샤드 DB로 즉시 조회가 가능해진다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Scatter-Gather&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;분산 시스템에서 일단 모두에게 다 물어보고(Scatter), 응답을 하나로 모으는(Gather) 쿼리 처리 방식&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;49-버킷-내-객체-목록-확인&quot;&gt;4.9. 버킷 내 객체 목록 확인&lt;/h2&gt;

&lt;p&gt;객체 저장소는 본질적으로 디렉터리라는 물리적 개념이 존재하지 않는 수평적 평면 구조(Flat Key-Value Store)이다.&lt;br /&gt;
모든 객체는 단지 고유한 Key 이름을 가질 뿐이다.&lt;/p&gt;

&lt;p&gt;하지만 사용자는 파일 시스템의 디렉터리 구조에 익숙하므로, 객체 저장소는 접두어와 구분자(주로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;)를 활용하여 계층적 디렉터리 구조를 시뮬레이션하여 보여주는 기능을 제공한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;491-디렉터리-계층-구조-시뮬레이션-및-cli-동작&quot;&gt;4.9.1. 디렉터리 계층 구조 시뮬레이션 및 CLI 동작&lt;/h3&gt;

&lt;p&gt;AWS S3 CLI 명령어인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aws s3 ls&lt;/code&gt;는 평면적인 객체 키 목록을 디렉터리처럼 구조화하여 출력한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 1. 버킷 최상위 목록 조회 (구분자 &apos;/&apos; 적용)&lt;/span&gt;
aws s3 &lt;span class=&quot;nb&quot;&gt;ls &lt;/span&gt;s3://my-bucket/

&lt;span class=&quot;c&quot;&gt;# 2. 특정 접두어(Prefix) 하위의 목록 조회&lt;/span&gt;
aws s3 &lt;span class=&quot;nb&quot;&gt;ls &lt;/span&gt;s3://my-bucket/a/

&lt;span class=&quot;c&quot;&gt;# 3. 특정 접두어 하위의 모든 객체를 재귀적으로 조회&lt;/span&gt;
aws s3 &lt;span class=&quot;nb&quot;&gt;ls &lt;/span&gt;s3://my-bucket/a/ &lt;span class=&quot;nt&quot;&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이해를 돕기 위해 버킷 내에 다음과 같은 4개의 객체(Key)가 평면적으로 저장되어 있다고 가정해보자.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;KR/cities/seoul.txt&lt;/li&gt;
  &lt;li&gt;KR/cities/suwon.txt&lt;/li&gt;
  &lt;li&gt;NY/cities/ny.txt&lt;/li&gt;
  &lt;li&gt;federal.txt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 버킷을 대상으로 AWS S3 CLI 명령어를 수행하면 아래와 같이 표시된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aws s3 ls s3://my-bucket/&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;KR/&lt;/li&gt;
      &lt;li&gt;NY/&lt;/li&gt;
      &lt;li&gt;federal.txt&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aws s3 ls s3://my-bucket/KR/&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;cities/&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aws s3 ls s3://my-bucket/KR/ --recursive&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;KR/cities/seoul.txt&lt;/li&gt;
      &lt;li&gt;KR/cities/suwon.txt&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;492-단일-데이터베이스-서버에서의-목록-조회-및-애플리케이션-파싱&quot;&gt;4.9.2. 단일 데이터베이스 서버에서의 목록 조회 및 애플리케이션 파싱&lt;/h3&gt;

&lt;p&gt;단일 RDB 환경에서의 SQL은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LIKE&lt;/code&gt; 연산자와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ORDER BY&lt;/code&gt;를 사용하여 접두어 조회를 효율적으로 처리할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;-- 버킷 &apos;123&apos; 내에서 &apos;KR/&apos; 접두어로 시작하는 객체 목록 조회&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;object_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;object_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;updated_at&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;object&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bucket_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;123&apos;&lt;/span&gt; 
  &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;object_name&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;LIKE&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;KR/%&apos;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;object_name&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ASC&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;DB 질의 결과로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KR/cities/seoul.txt&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KR/cities/suwon.txt&lt;/code&gt; 가 반환되었을 때, 애플리케이션 레이어는 다음과 같은 가상 폴더를 만들어낸다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;접두어 제거:&lt;/strong&gt;  요청 조건이었던 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KR/&lt;/code&gt; 부분을 문자열에서 잘라냄
    &lt;ul&gt;
      &lt;li&gt;KR/cities/seoul.txt → cities/seoul.txt&lt;/li&gt;
      &lt;li&gt;KR/cities/suwon.txt → cities/suwon.txt&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;구분자 스캔:&lt;/strong&gt; 남은 문자열에서 첫 번째 슬래시까지의 위치를 찾음
    &lt;ul&gt;
      &lt;li&gt;cities/seoul.txt → 첫 슬래시까지 잘라내어 cities/ 추출&lt;/li&gt;
      &lt;li&gt;cities/suwon.txt → 첫 슬래시까지 잘라내어 cities/ 추출&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;중복 제거:&lt;/strong&gt; 추출된 동일한 문자열 cities/ 를 하나로 합쳐 클라이언트에게 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PRE cities/&lt;/code&gt;(가상 디렉터리) 항목으로 최종 응답&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;493-분산-데이터베이스샤딩-환경에서의-목록-조회-및-오프셋-추적의-난제&quot;&gt;4.9.3. 분산 데이터베이스(샤딩) 환경에서의 목록 조회 및 오프셋 추적의 난제&lt;/h3&gt;

&lt;p&gt;앞서 다루었듯, 메타데이터 DB는 단일 버킷에 트래픽이 쏠리는 핫스팟을 방지하기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash(bucket_name + object_name)&lt;/code&gt; 순서쌍을 샤딩 키로 활용하여 레코드를 
여러 샤드 DB 서버에 분산 저장한다.&lt;/p&gt;

&lt;p&gt;이로 인해 &lt;strong&gt;동일한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KR/&lt;/code&gt; 접두어를 공유하는 객체들이라 하더라도 해시값에 의해 완전히 다른 샤드 DB 서버로 흩어지게 된다.&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[샤드 DB 1] ──&amp;gt; &apos;KR/cities/seoul.txt&apos;  (해시값: 0x2A...)
[샤드 DB 2] ──&amp;gt; &apos;NY/cities/ny.txt&apos;     (해시값: 0x9B...)
[샤드 DB 3] ──&amp;gt; &apos;KR/cities/suwon.txt&apos;  (해시값: 0x5F...)
[샤드 DB 4] ──&amp;gt; &apos;federal.txt&apos;          (해시값: 0x1C...)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;분산 DB에서 페이징 나눔이 어려운 이유(샤드별 오프셋 추적)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;객체가 여러 샤드에 나뉘어 있으므로, 각 샤드가 조건에 맞아 반환하는 객체 수는 제각각이다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;어떤 샤드에는 한 페이지를 꽉 채울 10개의 객체가 존재&lt;/li&gt;
  &lt;li&gt;어떤 샤드에는 2~3개만 존재&lt;/li&gt;
  &lt;li&gt;어떤 샤드에는 아예 조건에 맞는 객체가 없음&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 애플리케이션 코드는 &lt;strong&gt;모든 샤드에 질의를 보낸 후(Scatter), 반환된 결과를 받아 메모리에서 하나로 취합 및 정렬(Gather &amp;amp; Sort)한 뒤 10개만 추려내어 
클라이언트에 반환&lt;/strong&gt;해야 한다.&lt;/p&gt;

&lt;p&gt;문제는 이번 페이지에 포함되지 못하고 남은 객체들은 &lt;strong&gt;다음 페이지 요청 시 다시 고려&lt;/strong&gt;되어야 한다는 점이다.&lt;br /&gt;
즉, &lt;strong&gt;샤드마다 읽어들인 위치(오프셋)가 서로 달라지게 된다.&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[페이지 1 요청 결과]
- 샤드 1: 5개 읽음 (다음 시작 오프셋: 5)
- 샤드 2: 1개 읽음 (다음 시작 오프셋: 1)
- 샤드 3: 4개 읽음 (다음 시작 오프셋: 4)
- 샤드 4: 0개 읽음 (다음 시작 오프셋: 0)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Scatter-Gather 및 OFFSET 페이징의 성능 폭망 문제&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;특정 버킷이 전체 목록을 알파벳순(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ORDER BY object_name&lt;/code&gt;)으로 나열하려면, 애플리케이션은 모든 샤드 DB에 흩어져 있는 결괏값을 읽어온(Scatter-Gather) 후 
병합 정렬(Merge Sort)해야만 한다.&lt;/p&gt;

&lt;p&gt;이 분산 환경에서 전통적인 RDB의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OFFSET 100000 LIMIT 10&lt;/code&gt; 방식의 페이징을 적용할 경우 다음과 같은 치명적인 성능 저하가 발생한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Scatter-Gather 오버헤드&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;모든 샤드 DB 각각에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OFFSET 100000 LIMIT 10&lt;/code&gt; 질의를 전송해야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;읽기 데이터의 기하급수적 증가&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;샤드가 10개라면 각 샤드가 100,010개씩, 총 1,000,100개의 레코드를 메모리로 읽어와서 정렬해야 100,000번째 페이지의 10개 데이터를 건져낼 수 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;네트워크 및 CPU 병목&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;\(O(N \times (\text{OFFSET} + \text{LIMIT}))\) 형태의 컴퓨팅 비용이 발생하여 페이징 깊이가 깊어질수록 시스템 전체가 마비됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;494-객체-저장소의-아키텍처-타협점trade-off&quot;&gt;4.9.4. 객체 저장소의 아키텍처 타협점(Trade-off)&lt;/h3&gt;

&lt;p&gt;대규모 객체 저장소 설계는 &lt;strong&gt;시스템의 무한한 규모 확장성과 데이터 내구성 최적화에 최우선 순위&lt;/strong&gt;를 둔다.&lt;br /&gt;
상대적으로 객체 목록 출력 명령의 성능을 보장하는 것은 우선순위가 낮다.&lt;/p&gt;

&lt;p&gt;실제로 AWS S3를 포함한 시중의 상용 객체 저장소가 제공하는 객체 목록 출력 기능의 반응 속도와 처리량은 단일 DB 조회 대비 최적과는 거리가 멀다.&lt;br /&gt;
데이터 보관 비용과 내구성을 극대화하는 대신, 목록 조회 성능을 일정 부분 양보하는 아키텍처적 타협을 선택했기 때문이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;실무에서의-rdb-샤딩-시-페이징-기법은-무엇이-있을까&quot;&gt;💡실무에서의 RDB 샤딩 시 페이징 기법은 무엇이 있을까?&lt;/h4&gt;

&lt;p&gt;실무에서는 샤드별 오프셋을 직접 추적하는 방식을 가급적 피하며, 아래 4가지 대안 기법을 사용한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;1. 커서(Keyset) 기반 페이징:&lt;/strong&gt; 오프셋 대신 ‘마지막 읽은 키’ 조건절을 전달하여 인덱스 스캔 유도&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2. 샤딩 미들웨어 도입:&lt;/strong&gt; 미들웨어를 두어서 DB 프록시 레이어에서 오프셋 추적을 자동화&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;3. 검색 엔진(Elasticsearch) 연동:&lt;/strong&gt; RDB는 CUD만 담당하고, 페이징 조회는 검색 엔진으로 이관&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;4. 목록 전용 비정규화 테이블:&lt;/strong&gt; 목록 전용 테이블을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket_id&lt;/code&gt; 기준으로 샤딩하여 단일 DB 샤드에서 정렬 및 페이징 일괄 처리&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1. 커서 기반 페이징(Keyset / Cursor-based Pagination): 가장 보편적&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;샤드별 오프셋 숫자(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OFFSET 100&lt;/code&gt;)로 추적하는 대신, ‘이전 페이지에서 마지막으로 읽은 객체 이름(Last Evaluated Key)’만을 커서로 전달&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;-- 애플리케이션은 각 샤드에 동일한 &apos;커서 조건&apos;만 던지면 됨&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;object_name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;object_size&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;object&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;object_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;KR/cities/seoul.txt&apos;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;-- 이전 페이지의 마지막 키&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;object_name&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;ASC&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;LIMIT&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 방법을 사용하면 애플리케이션은 샤드별 오프셋 숫자를 계산할 필요가 없다.&lt;br /&gt;
단지 클라이언트가 들고 온 ‘마지막 키 문자열’ 하나만 모든 샤드에 조건절(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE key &amp;gt; cursor&lt;/code&gt;)로 던진 후, 각 샤드에서 올라온 상위 10개만 정렬하면 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2. 샤딩 미들웨어 도입: DB 레이어에 위임&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;애플리케이션 코드가 샤딩을 인식하지 않도록 &lt;strong&gt;Vitess, Citus, Apache ShardingSphere&lt;/strong&gt; 같은 샤딩 미들웨어를 프록시로 둔다.&lt;/p&gt;

&lt;p&gt;이 방법을 사용하면 애플리케이션은 마치 단일 RDB에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT * FROM object ORDER BY name LIMIT 10 OFFSET 20&lt;/code&gt;을 던지는 것처럼 개발한다.&lt;br /&gt;
샤드 분산, 샤드별 오프셋 추적, 병합 정렬은 미들웨어가 내부 메모리에서 알아서 처리해준다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;3. CUD와 Read의 분리&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RDB 샤딩은 서비스 데이터 저장 및 수정 용도로만 쓰고, 복잡한 페이징 및 목록 조회는 &lt;strong&gt;Elasticsearch / OpenSearch&lt;/strong&gt; 같은 분산 검색 엔진으로 읽기(Read)를 이관한다.&lt;/p&gt;

&lt;p&gt;이 방법은 CDC(Debezium 등)를 통해 RDB 데이터를 검색 엔진으로 비동기 동기화한다.&lt;br /&gt;
검색 엔진은 &lt;a href=&quot;https://assu10.github.io/dev/2026/07/04/architecture-distributed-email-service/#%EC%97%AD%EC%9D%B8%EB%8D%B1%EC%8A%A4inverted-index%EC%9D%98-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0&quot;&gt;역인덱스(Inverted Index)&lt;/a&gt; 구조를 가지므로 샤딩 분산 환경에서도 수억 건의 페이징 조회를 수 ms 내에 처리한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;4. 목록 전용 비정규화 테이블 운용&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;목록 출력이 빈번하게 일어나는 서비스라면, &lt;strong&gt;목록 조회 전용 테이블을 별도로 만들어 단일 샤드로 모이도록 샤딩 키를 다르게 설계&lt;/strong&gt;한다.&lt;/p&gt;

&lt;p&gt;주 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object&lt;/code&gt; 테이블은 핫스팟 방지를 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash(bucket_name + object_name)&lt;/code&gt;로 샤딩하더라도, &lt;strong&gt;목록 전용 비정규화 테이블은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket_id&lt;/code&gt;만을 샤딩 키&lt;/strong&gt;로 설정한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[주 메타데이터 DB: hash(bucket_name + object_name) 샤딩]
- 샤드 1: KR/cities/seoul.txt
- 샤드 2: NY/cities/ny.txt
- 샤드 3: KR/cities/suwon.txt
(분산 저장되어 핫스팟 방지)

[목록 전용 비정규화 DB: bucket_id 샤딩]
- 샤드 1 (버킷 &apos;123&apos; 전용): 
   ├── KR/cities/seoul.txt
   ├── KR/cities/suwon.txt
   └── federal.txt
(단일 샤드에 모여 있어 한 번의 단일 DB 질의로 페이징 완료)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점: 구현 단순화 및 성능 극대화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;버킷 내 모든 객체 목록이 &lt;strong&gt;단 1대의 데이터베이스 샤드 서버에 집중 보관&lt;/strong&gt;되므로, 샤드 간 병합 정렬이나 샤드별 오프셋 추적 로직 없이 단일 RDB의 일반적인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ORDER BY + LIMIT/OFFSET&lt;/code&gt; 문 만으로 목록 조회 처리 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점: 동기화 비용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;객체가 업로드되거나 삭제될 때 Primary 테이블과 비정규화 목록 테이블 두 곳 모두에 데이터를 반영해야 하므로 이중 쓰기나 이벤트 기반 최종 일관성 동기화 파이프라인을 추가로 구축해야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;410-객체-버전-관리versioning&quot;&gt;4.10. 객체 버전 관리(Versioning)&lt;/h2&gt;

&lt;p&gt;객체 버전 관리는 동일한 버킷 안에서 &lt;strong&gt;동일한 객체 이름(Key)을 가진 여러 버전의 사본을 보관하고 관리하는 기능&lt;/strong&gt;이다.&lt;br /&gt;
실수로 객체를 삭제하거나 덮어썼을 때 이전 상태로 복구할 수 있는 무결성 장치이다.&lt;/p&gt;

&lt;p&gt;버전 관리 기능이 활성화되면 객체 저장소는 해당 문서의 이전 버전을 메타데이터 저장소에 그대로 유지하며, 기존 레코드를 덮어쓰거나 이전 버전에 삭제 표시를 남기지 않고 독립된 레코드로 보존한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;4101-객체-업로드put-흐름&quot;&gt;4.10.1. 객체 업로드(PUT) 흐름&lt;/h3&gt;

&lt;p&gt;버전 기능이 활성화된 클라이언트가 객체를 업로드(PUT)할 때, 시스템 내부에서는 데이터 오버라이드를 방지하기 위해 아래와 같은 5단계 절차가 진행된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/object.png&quot; alt=&quot;객체 버전&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;① 클라이언트 요청 전송&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클라이언트가 script.txt 객체를 업로드하기 위해 API 서비스로 HTTP PUT 요청을 보낸다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;② 인증 및 권한 검증&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;API 서비스는 사용자 신원을 확인한 후, 해당 사용자가 대상 버킷에 Write 권한이 있는지 검증한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;③ 데이터 저장소 영속화 및 UUID 발급&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;권한 검증 결과 이상이 없으면, API 서비스는 객체 데이터를 데이터 저장소에 보낸다.&lt;/li&gt;
      &lt;li&gt;데이터 저장소는 새 객체를 생성하여 디스크에 영속적으로 저장한 후, API 서비스에 새 데이터 조각의 고유 UUID를 반환한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;④ 메타데이터 저장소 호출&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;API 서비스는 메타데이터 저장소를 호출하여 새 객체의 메타데이터 정보를 보관 요청한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;⑤ &lt;a href=&quot;https://assu10.github.io/dev/2026/07/04/architecture-distributed-email-service/#timeuuid-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%83%80%EC%9E%85%EC%9D%B4-%EC%A0%95%EB%A0%AC%EC%97%90-%ED%95%84%EC%88%98%EC%9D%B8-%EC%9D%B4%EC%9C%A0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TIMEUUID&lt;/code&gt;&lt;/a&gt; 기반 버전 레코드 추가&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메타데이터 저장소의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object&lt;/code&gt; 테이블에는 버전 기능을 지원하기 위한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_version&lt;/code&gt; 컬럼이 존재한다.(이 컬럼은 버킷의 버전 기능이 활성화되어 있을 때 사용)&lt;/li&gt;
      &lt;li&gt;기존 레코드를 덮어쓰는 대신, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bucket_id&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_name&lt;/code&gt;은 동일하지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_id&lt;/code&gt;(3단계에서 반환받은 새 UUID)와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_version&lt;/code&gt;은 새로운 값인 레코드를 테이블에 INSERT 한다.&lt;/li&gt;
      &lt;li&gt;이 때, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_version&lt;/code&gt;은 새로운 레코드가 테이블에 추가될 때 자동으로 생성되는 TIMEUUID 값이다.&lt;/li&gt;
      &lt;li&gt;동일한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_name&lt;/code&gt;을 갖는 항목들 가운데 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_version&lt;/code&gt;에 기록된 TIMEUUID 값이 가장 큰 레코드가 해당 객체의 최신 버전이 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/meta.png&quot; alt=&quot;메타데이터와 버전 정보&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;4102-삭제-표식delete-marker을-통한-비파괴적-삭제-메커니즘&quot;&gt;4.10.2. 삭제 표식(Delete Marker)을 통한 비파괴적 삭제 메커니즘&lt;/h3&gt;

&lt;p&gt;버전 관리 환경에서는 객체 삭제 요청이 들어오더라도 물리적 데이터나 메타데이터를 지우지 않는다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/meta2.png&quot; alt=&quot;삭제 표시 삽입을 통한 객체 삭제&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;① Delete Marker 추가&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;객체를 삭제할 때 버킷 안의 모든 기존 버전을 그대로 둔 채, &lt;strong&gt;단순히 삭제 표식이라는 특별한 레코드를 최신 버전으로 추가&lt;/strong&gt;한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;② 현재 버전 승격&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;삭제 표식 또한 객체의 새로운 버전이므로, 테이블에 삽입되는 순간 해당 객체의 새로운 ‘현재 버전’이 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;③ GET 요청 시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;404 NOT FOUND&lt;/code&gt; 응답&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;삭제 표식이 현재 버전인 상태에서 클라이언트가 해당 객체를 조회하면, 저장소 API 서비스는 삭제 표식을 확인하고 객체가 존재하지 않는 것으로 취급하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;404 NOT FOUND&lt;/code&gt; 오류를 반환한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;411-대용량-업로드-최적화-multipart-upload와-etag&quot;&gt;4.11. 대용량 업로드 최적화: Multipart Upload와 ETag&lt;/h2&gt;

&lt;p&gt;대용량 객체를 단일 HTTP 요청으로 업로드하는 것은 네트워크 대역폭 및 시스템 안정성 측면에서 매우 큰 위험을 부담한다.&lt;/p&gt;

&lt;p&gt;단일 스트림 업로드 방식은 네트워크에 순간적인 패킷 유실이나 타임아웃이 발생할 경우 &lt;strong&gt;수십 GB의 파일 전체를 처음부터 다시 업로드&lt;/strong&gt;해야 하며, 단일 TCP 연결 
대역폭의 한계로 인해 디스크 쓰기 속도를 속도감 있게 활용하지 못한다는 치명적인 단점이 존재한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;4111-멀티파트-업로드-3단계-처리-파이프라인&quot;&gt;4.11.1. 멀티파트 업로드 3단계 처리 파이프라인&lt;/h3&gt;

&lt;p&gt;이러한 문제를 해결하기 위해 대용량 객체를 작은 파트 단위로 나누어 병렬로 전송하는 &lt;strong&gt;멀티파트 업로드&lt;/strong&gt; 메커니즘을 제공한다.&lt;/p&gt;

&lt;p&gt;멀티 파트 업로드는 클라이언트와 저장소 API 간의 아래와 같은 3단계 상호작용을 통해 이루어진다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[클라이언트] ───(1. InitiateMultipartUpload)───&amp;gt; [API 서비스]
            &amp;lt;───(UploadID 반환: &quot;upload_89f2&quot;)───
                                
[클라이언트] ───(2. Part 1 Upload + UploadID)───&amp;gt; [API 서비스] (ETag 1 반환)
            ───(2. Part 2 Upload + UploadID)───&amp;gt; [API 서비스] (ETag 2 반환)
            ───(2. Part 3 Upload + UploadID)───&amp;gt; [API 서비스] (ETag 3 반환)
                        (병렬 전송)
                                
[클라이언트] ───(3. CompleteMultipartUpload)───&amp;gt; [API 서비스]
                 (UploadID + ETag 목록 전달)       (파일 결합 및 영속화)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/multipart.png&quot; alt=&quot;멀티파트 업로드&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;1단계: 업로드 시작(Initiate Multipart Upload)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;① 클라이언트가 대상 버킷과 객체 이름을 지정하여 멀티파트 업로드 개시 요청을 보낸다.&lt;/li&gt;
      &lt;li&gt;② 서버는 이 세션을 식별하기 위한 고유 트랜잭션 키인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UploadID&lt;/code&gt;를 발급하여 클라이언트에 응답한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2단계: 파트별 독립 및 병렬 업로드(Upload Parts)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클라이언트는 원본 대용량 파일을 지정된 크기(예: 8MB~100MB)의 파트 조각으로 나눈다.&lt;/li&gt;
      &lt;li&gt;③ 각 파트에 1,2,3.. 순서로 파트 번호를 부여하고, &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UploadID&lt;/code&gt;와 파트 번호를 포함하여 여러 파트를 독립적인 HTTP 요청으로 병렬로 전송&lt;/strong&gt;한다.&lt;/li&gt;
      &lt;li&gt;④ 서버는 파트 데이터를 수신하여 저장소에 영속화한 후, 해당 파트 바이너리인 MD5 해시값인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ETag&lt;/code&gt;를 생성하여 클라이언트에 응답한다.&lt;/li&gt;
      &lt;li&gt;업로드 도중 특정 파트 전송이 실패하더라도, 전체 파일을 재전송할 필요 없이 &lt;strong&gt;실패한 해당 파트만 재전송&lt;/strong&gt;하면 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;3단계: 업로드 완료 및 객체 결합(Complete Multipart Upload)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;⑤ 모든 파트의 전송이 완료되면 클라이언트는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UploadID&lt;/code&gt;, 그리고 파트 번호와 수신한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ETag&lt;/code&gt; 목록을 배열 형태로 담아 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CompleteMultipartUpload&lt;/code&gt; 요청을 전송한다.&lt;/li&gt;
      &lt;li&gt;⑥ 서버는 클라이언트가 제출한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ETag&lt;/code&gt; 목록과 실제 저장된 파트 조각들의 정합성을 최종 검증한 후, 분산 저장된 조각들을 하나의 완성된 객체 메타데이터로 연결 조립한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;⚠️멀티파트 업로드의 구조적 한계: 조립 후 버려지는 파트 조각 문제&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;멀티파트 업로드 접근법에서 반드시 고려해야 할 문제점은 ‘객체 조립이 완료된 후에는 디스크에 따로 남아있는 파트 조각들이 더 이상 쓸모가 없어진다’는 사실이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;독립 파일 저장&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;2단계 진행 시 데이터 노드 디스크에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Part 1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Part 2&lt;/code&gt; … 가 각각 독립된 임시 파일 형태로 기록된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;조립 완료 후 참조 중단&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;3단계에서 서버가 조각들을 하나의 원본 객체로 조립하고 나면, 디스크에 보관되어 있던 개별 파트 조각 파일들은 원본 객체 메타데이터와의 연결이 끊어지게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;용량 고갈 위험&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이 쓸모없어진 파트 조각들을 제때 지워주지 않으면 원본 객체 용량 외에 추가로 파트 조각 용량이 디스크를 중복 점유하게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 객체 조립이 끝난 후 디스크 공간을 확보하기 위해, 쓸모없어진 파트 조각들을 백그라운드에서 주기적으로 삭제해 주는 &lt;strong&gt;GC(Garbage Collection)&lt;/strong&gt; 메커니즘이 
필수적으로 연동되어야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;일반적인-백엔드의-multipartform-data-vs-객체-저장소의-multipart-upload-차이점&quot;&gt;💡일반적인 백엔드의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;multipart/form-data&lt;/code&gt; vs 객체 저장소의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Multipart Upload&lt;/code&gt; 차이점&lt;/h4&gt;

&lt;p&gt;결론적으로 말하자면, 둘은 이름만 비슷할 뿐 완전히 다른 개념이다.&lt;br /&gt;
백엔드 개발에서 일어나는 혼동을 바로잡기 위해 두 방식의 차이점을 다음과 같이 비교 정리할 수 있다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;구분&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;HTTP Standard multipart/form-data (일반 웹 백엔드)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;S3 / 객체 저장소의 Multipart Upload (S3 API)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;개념&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;단일 HTTP 요청 바디 안에서 텍스트 데이터와 파일 데이터를 &lt;strong&gt;구분자(boundary)&lt;/strong&gt;로 나누어 보내는 MIME 표준&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;하나의 대용량 파일 자체를 여러 개(N개)의 독립된 HTTP 요청 조각으로 분할하여 전송하는 API 프로토콜&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;파일 분할 여부&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;파일을 조각내지 않음 (1GB 파일도 단 1개의 HTTP 요청으로 통째로 전송)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;파일을 여러 개(예: 8MB 단위)의 청크(Chunk) 조각으로 실제로 분할하여 전송&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;전송 방식&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;단일 TCP 연결 스트림 전송&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;여러 HTTP 요청을 통한 병렬(Parallel) 전송&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;중단 시 복구&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;실패 시 1GB 전체를 처음부터 다시 전송&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;실패한 특정 파트 조각(8MB)만 재전송 가능 (Resumable Upload)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;HTTP &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;multipart/form-data&lt;/code&gt;&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;단일 HTTP 요청 내부에서 Form 데이터와 파일 데이터를 구분자(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;boundary&lt;/code&gt;)로 나누어 보낼 뿐, 단일 파일 바이너리를 청크 단위로 분할하여 전송하지 않는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;객체 저장소 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Multipart Upload&lt;/code&gt;&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;대용량 파일 자체를 8MB 이상의 파트 조각들로 분할하여 독립적인 HTTP 요청으로 병렬 전송한다.&lt;/li&gt;
      &lt;li&gt;실패 시 해당 파트만 재전송하면 되므로 대역폭 효율과 안정성이 뛰어나다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;일반-백엔드-개발에서-s3처럼-진짜-분할-업로드를-구현하려면&quot;&gt;💡일반 백엔드 개발에서 S3처럼 ‘진짜 분할 업로드’를 구현하려면?&lt;/h4&gt;

&lt;p&gt;일반 웹 백엔드(Spring, Node.js..)에서 S3처럼 끊어 올리기(Resumable Upload)나 대용량 파일 자동 분할 업로드를 구현하고 싶다면, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;multipart/form-data&lt;/code&gt;만으로는 
불가능하며 다음과 같은 방법을 사용한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;프론트엔드 JavaScript &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;File.slice()&lt;/code&gt; 활용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;JS의 Blob API인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file.slice(start, end)&lt;/code&gt;를 이용해 클라이언트 브라우저에서 직접 8MB 단위로 파일을 자른 뒤, 백엔드 REST API에 파트 번호와 함께 N번의 API 요청을 보낸다.&lt;/li&gt;
      &lt;li&gt;백엔드는 임시 디스크나 DB에 쪼개진 파트를 저장해 두었다가 마지막에 하나로 결합(File Stream Append)한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;오픈소스 프로토콜 도입(TUS Protocol)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;대용량 분할/이어올리기 표준 오픈소스 프로토콜인 &lt;a href=&quot;https://tus.io/&quot;&gt;tus.io&lt;/a&gt; 같은 라이브러리를 프론트/백엔드에 도입하여 자동화한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AWS SDK 자동화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;AWS SDK, Google Cloud SDK, S3 CLI 도구 등은 파일 크기가 특정 임계치(기본 8MB~16MB)를 초과하면 &lt;strong&gt;클라이언트단 라이브러리가 메모리상에서 파일을 청크(Chunk) 단위로 자동 분할하여 병렬 전송하고, 완료 결합 요청까지 자동으로 수행&lt;/strong&gt;한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;412-garbage-collection&quot;&gt;4.12. Garbage Collection&lt;/h2&gt;

&lt;p&gt;객체 저장소는 무한에 가까운 용량 확장을 목표로 하므로, 가용 저장 공간을 효율적으로 관리하는 것이 핵심 과제이다.&lt;/p&gt;

&lt;p&gt;시간이 흐름에 따라 데이터 노드의 디스크에는 논리적으로 지워진 데이터, 더 이상 참조되지 않는 구버전 데이터, 그리고 조립이 끝났거나 중단된 멀티파트 파트 조각 등 
쓸모없는 ‘쓰레기 데이터’가 지속적으로 누적된다.&lt;/p&gt;

&lt;p&gt;이러한 유효하지 않은 데이터를 백그라운드에서 감지하고 회수하여 디스크 공간을 재확보하는 프로세스가 바로 GC(Garbage Collection)이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;4121-garbage가-발생하는-주요-원인&quot;&gt;4.12.1. Garbage가 발생하는 주요 원인&lt;/h3&gt;

&lt;p&gt;대규모 객체 저장소 환경에서 GC의 처리 대상이 되는 쓰레기 데이터들은 주로 다음 경로를 통해 발생한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;논리적으로 삭제(Delete Marker)되거나 덮어써진 객체 및 구버전 사본&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 특정 버전 ID를 지정하여 영구 삭제 요청을 보내거나, 수명주기 규칙에 의해 보존기간이 만료된 데이터 조각들은 즉시 GC의 대상이 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;멀티파트 조립 완료 후 남아있는 파트 조각들&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#411-대용량-업로드-최적화-multipart-upload와-etag&quot;&gt;4.11. 대용량 업로드 최적화: Multipart Upload와 ETag&lt;/a&gt;에서 다루었듯이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CompleteMultipartUpload&lt;/code&gt; 완료 후 원본 객체가 생성되고 나면 디스크에 개별적으로 보관되어 있던 수많은 파트 조각 파일들은 즉시 참조가 끊어져 쓰레기 데이터가 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;업로드 도중 취소되거나 중단된 채 방치된 멀티파트 업로드 파트 조각들&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;완료되지 못하고 방치된 고아(Orphan) 파트 조각들 또한 주기적 GC 대상이 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;다중 노드에 분산 보관 중인 사용되지 않는 데이터 사본들&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;삭제된 객체에 대응하는 분산 복제본이나 패리티 조각들이 여러 노드에 남아있는 경우이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;객체 저장소는 데이터 내구성을 위해 데이터를 여러 노드에 나누어 보관하므로, GC 시 &lt;strong&gt;모든 관련 노드의 사본을 동시 회수&lt;/strong&gt;해야 하는 분산 동기화 과제가 존재한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[다중 노드 환경에서의 객체 삭제 / GC 범위]

1. 다중 복제(Replication) 방식
   [주 저장소 노드 (Primary)]  ──(GC 수행)──&amp;gt; 삭제 / 공간 회수
   [부 저장소 노드 (Secondary)] ──(GC 수행)──&amp;gt; 삭제 / 공간 회수 (동시 회수)

2. (8+4) 소거 코드(Erasure Coding) 방식
   [데이터 노드 1 ~ 8]   ──(GC 수행)──&amp;gt; 8개 원본 데이터 조각 삭제
   [패리티 노드 9 ~ 12]  ──(GC 수행)──&amp;gt; 4개 패리티 데이터 조각 삭제
   ------------------------------------------------------------------
   ★ 단 1개의 객체 삭제 시 -&amp;gt; 총 12개 노드 전체에서 데이터 회수 수행
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;다중 복제 데이터의 사본 회수&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터를 다중화하여 저장하는 방식에서는 객체가 삭제 대상으로 판정되면, &lt;strong&gt;Primary Node뿐만 아니라 Secondary Nodes에 할당된 데이터 복사본까지 모두 찾아서 지워야&lt;/strong&gt; 한다.&lt;/li&gt;
      &lt;li&gt;백그라운드 GC 데몬은 메타데이터의 위치 정보를 조회하여 모든 Secondary 저장소 노드에 공간 회수 명령을 발행한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(8+4) 소거 코드 데이터의 사본 회수&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;(8+4) 소거 코드를 사용하는 환경에서는 객체 하나가 8개의 데이터 조각과 4개의 패리티 조각으로 쪼개져 총 12개의 서로 다른 장애 도메인(노드)에 분산 저장된다.&lt;/li&gt;
      &lt;li&gt;따라서 &lt;strong&gt;객체 하나를 영구 삭제할 때, GC 데몬은 12개 노드 전체를 대상으로 해당 객체의 조각 파일들을 추적하여 지워야&lt;/strong&gt; 완벽한 공간 회수가 이루어진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;4122-데이터-노드-내부의-compaction-메커니즘&quot;&gt;4.12.2. 데이터 노드 내부의 Compaction 메커니즘&lt;/h3&gt;

&lt;p&gt;데이터 노드는 디스크 I/O 성능 극대화를 위해 &lt;strong&gt;순차 쓰기 전용 Append-Only&lt;/strong&gt; 구조로 파일(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/c&lt;/code&gt; 등)을 관리한다.&lt;br /&gt;
파일 중간에 위치한 특정 객체나 파트 조각만 콕 집어서 물리적으로 지우는 것은 파일 시스템 특성상 불가능하다.&lt;/p&gt;

&lt;p&gt;따라서 각 데이터 노드의 백그라운드 GC 데몬은 &lt;strong&gt;파일 압축(Compaction)&lt;/strong&gt; 방식을 채택하여 쓰레기 데이터를 정리한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0705/gar.png&quot; alt=&quot;Garbage Collection 정리(Compaction) 메커니즘&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;유효 객체 복사 및 불량 객체 건너뛰기&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;GC 프로세스는 기존 파일(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/b&lt;/code&gt;)의 객체들을 새로운 파일(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/d&lt;/code&gt;)로 순차 복사한다.&lt;/li&gt;
      &lt;li&gt;이 과정에서 &lt;strong&gt;삭제된 객체임을 알리는 Flag 값이 true인 ‘객체 2’와 ‘객체 5’는 복사하지 않고 건너뛴다.&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_mapping&lt;/code&gt; 테이블 갱신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유효한 모든 객체의 복사가 완료되면, GC는 데이터 노드 로컬 SQLite의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_mapping&lt;/code&gt; 테이블(및 중앙 메타데이터 DB)을 갱신한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;속성 변경 유무&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;예를 들어 복사되어 살아남은 &lt;strong&gt;‘객체 3’의 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_id&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object_size&lt;/code&gt; 값은 변하지 않고 그대로 유지&lt;/strong&gt;된다.&lt;/li&gt;
      &lt;li&gt;그러나 디스크 상의 저장 위치가 바뀌었으므로 &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file_name&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_offset&lt;/code&gt;값은 새 위치(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/d&lt;/code&gt; 및 신규 오프셋)를 가리키도록 수정&lt;/strong&gt;된다.
        &lt;ul&gt;
          &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file_name&lt;/code&gt;이 변경되는 이유는 객체의 디스크 내 물리적 저장 위치(파일)가 실제로 교체되었기 때문이다.&lt;/li&gt;
          &lt;li&gt;원래 ‘객체 3’은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/b&lt;/code&gt;라는 파일 안의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_offset = 5000&lt;/code&gt; 바이트 위치에 저장되어 있었다고 해보자.&lt;/li&gt;
          &lt;li&gt;하지만 GC 프로세스가 쓰레기 데이터를 솎아내고 유효 객체들만 새로운 파일인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/d&lt;/code&gt;로 이관 복사하였다.&lt;/li&gt;
          &lt;li&gt;따라서 ‘객체 3’이 실제로 존재하는 디스크 파일 경로가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/b&lt;/code&gt;에서 &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/data/d&lt;/code&gt;&lt;/strong&gt;로 완전히 바뀌게 되었다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;원자적 트랜잭션 보장&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 일관성을 보장하기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;file_name&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_offset&lt;/code&gt;에 대한 갱신 연산은 반드시 &lt;strong&gt;동일한 단일 트랜잭션 안에서 원자적으로 수행&lt;/strong&gt;하는 것이 바람직하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;백그라운드-데몬daemon이란&quot;&gt;💡백그라운드 데몬(Daemon)이란?&lt;/h4&gt;

&lt;p&gt;데몬은 사용자가 직접 제어하지 않아도 보이지 않는 백그라운드에서 묵묵히 돌아가는 프로세스를 뜻한다.&lt;/p&gt;

&lt;p&gt;리눅스나 서버 환경에서 이름 끝에 &lt;strong&gt;d&lt;/strong&gt;가 붙어있는 프로그램들은 대부분 데몬이다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;httpd&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;웹 요청을 기다렸다가 처리해주는 웹 서버 데몬&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dockerd&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;도커 컨테이너들을 백그라운드에서 관리해주는 도커 데몬&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crond&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;예약된 시간이 되면 알아서 작업을 실행해 주는 스케줄러 데몬&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;객체 저장소의 메인 프로세스(API 서비스)는 사용자들의 다운로드/업로드 요청을 처리하느라 바쁘다.&lt;br /&gt;
만일 메인 프로세스가 GC까지 직접 한다면 사용자 응답 속도가 크게 느려질 것이다.&lt;/p&gt;

&lt;p&gt;따라서 &lt;strong&gt;메인 프로세스와 완전히 분리된 GC 데몬을 별도로 띄워 놓는다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이 데몬 프로세스는 백그라운드에서 조용히 디스크를 스캔하며, 유효하지 않은 쓰레기 조각들을 주워 담아 폐기한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;객체-저장소의-발전-방향&quot;&gt;🔥객체 저장소의 발전 방향&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;NVMe-oF (NVMe over Fabrics) 및 캔자스/하드웨어 Offloading&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;과거에는 데이터 노드의 CPU가 erasure coding 연산과 체크섬 계산을 부담했지만, 최근 AWS나 GCP 등의 최신 인프라에서는 SmartNIC 및 dedicated 인공지능/DSP 가속 칩을 통해 erasure coding 연산을 하드웨어 오프로딩하여 CPU 병목을 최소화한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;S3 Express One Zone (초저지연 객체 저장소)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;2024년 이후 AWS가 선보인 S3 Express One Zone과 같이, 밀리초(ms) 단위의 지연 시간을 요구하는 AI/ML 학습 워크로드를 위해 초저지연 전용 싱글 AZ 객체 저장소 계층이 등장했다.&lt;/li&gt;
      &lt;li&gt;기존의 3개 AZ 분산 복제 대신 디렉터리 노드(Directory Bucket) 구조와 고성능 NVMe 캐싱 층을 결합하여 초당 수만 건의 Small File Read/Write 성능을 제공한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;오프로딩offloading이란&quot;&gt;💡오프로딩(Offloading)이란?&lt;/h4&gt;

&lt;p&gt;한 곳에 집중된 과부하나 작업 부담을 다른 곳으로 넘겨주는 행위를 말한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;IT
    &lt;ul&gt;
      &lt;li&gt;CPU 같은 핵심 처리 장치의 과부하를 막기 위해 특정 연산이나 프로세스를 보조 장치(GPU, NPU 등)나 클라우드 서버로 이관하여 처리하는 기술&lt;/li&gt;
      &lt;li&gt;예: 스마트폰의 복잡한 AI 연산을 전용 NPU나 클라우드로 보내 연산 속도를 높이고 배터리를 아끼는 것&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;인지 심리학
    &lt;ul&gt;
      &lt;li&gt;뇌의 기억 부담을 줄이기 위해 스마트폰 메모장, 달력 앱, 알람 등에 할 일을 기록해두는 행위&lt;/li&gt;
      &lt;li&gt;뇌의 짐을 외부에 덜어냄으로써 더 중요한 생각에 집중할 수 있게 돕는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬, 산 람 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000211656186&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links_vol2.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bytebytego.com/guides/explain-the-top-6-use-cases-of-object-stores/&quot;&gt;ByteByteGo - Object Storage Use Cases &amp;amp; Architecture&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ceph.io/en/news/blog/2009/the-rados-distributed-object-store/&quot;&gt;Ceph RADOS - Distributed Object Store Architecture&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://min.io/docs/minio/linux/index.html&quot;&gt;MinIO High Performance Object Storage Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/whitepapers/latest/s3-optimizing-performance-best-practices/s3-optimizing-performance-best-practices.html&quot;&gt;AWS Whitepaper - Optimizing Amazon S3 Performance&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.usenix.org/conference/osdi14/technical-sessions/presentation/muralidhar&quot;&gt;Facebook f4: Facebook’s Warm BLOB Storage System (USENIX)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 05 Jul 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/07/05/architecture-s3-like-object-storage/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/07/05/architecture-s3-like-object-storage/</guid>
        
        <category>architecture</category>
        
        <category>system-design</category>
        
        <category>object-storage</category>
        
        <category>distributed-systems</category>
        
        <category>erasure-coding</category>
        
        <category>storage-engine</category>
        
        <category>garbage-collection</category>
        
        <category>metadata-sharding</category>
        
        <category>placement-service</category>
        
        <category>시스템설계</category>
        
        <category>객체저장소</category>
        
        <category>분산시스템</category>
        
        <category>소거코드</category>
        
        <category>쓰레기수집</category>
        
        <category>아키텍처</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(2) - 분산 이메일 서비스</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-요구사항-정의와-규모-추정&quot;&gt;1. 요구사항 정의와 규모 추정&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-비즈니스-및-기능구조-도출&quot;&gt;1.1. 비즈니스 및 기능구조 도출&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#12-비기능-요구사항&quot;&gt;1.2. 비기능 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#13-개략적인-규모-추정&quot;&gt;1.3. 개략적인 규모 추정&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#메타데이터-저장-시-발신인과-수신인의-데이터를-따로-계산하지-않는-이유는&quot;&gt;💡메타데이터 저장 시 발신인과 수신인의 데이터를 따로 계산하지 않는 이유는?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-이메일-프로토콜과-전통적-메일-서버&quot;&gt;2. 이메일 프로토콜과 전통적 메일 서버&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-이메일-프로토콜&quot;&gt;2.1. 이메일 프로토콜&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#pop-vs-imap&quot;&gt;💡POP vs IMAP&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-dns-mx-레코드&quot;&gt;2.2. DNS MX 레코드&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-mime&quot;&gt;2.3. MIME&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#24-전통적-메일-서버-아키텍처&quot;&gt;2.4. 전통적 메일 서버 아키텍처&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#241-전통적-파일-시스템-저장-구조-maildir&quot;&gt;2.4.1. 전통적 파일 시스템 저장 구조: Maildir&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-분산-메일-서버-아키텍처-개략-설계&quot;&gt;3. 분산 메일 서버 아키텍처 개략 설계&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-restful-api&quot;&gt;3.1. RESTful API&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-분산-메일-서버-아키텍처&quot;&gt;3.2. 분산 메일 서버 아키텍처&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#첨부-파일-저장소로-카산드라cassandra-nosql이-부적합한-이유&quot;&gt;💡첨부 파일 저장소로 카산드라(Cassandra) NoSQL이 부적합한 이유&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-이메일-전송-워크플로우와-메시지-큐의-역할&quot;&gt;4. 이메일 전송 워크플로우와 메시지 큐의 역할&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-타-도메인-발송-시의-메시지-큐-규모-제어-및-백오프-전략&quot;&gt;4.1. 타 도메인 발송 시의 메시지 큐 규모 제어 및 백오프 전략&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-이메일-수신-워크플로우와-실시간-알림websocket-처리&quot;&gt;5. 이메일 수신 워크플로우와 실시간 알림(WebSocket) 처리&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#메일-처리-워커의-검증-작업스팸-바이러스-등을-통과하지-못한-이메일들은-어떻게-처리될까&quot;&gt;💡메일 처리 워커의 검증 작업(스팸, 바이러스 등)을 통과하지 못한 이메일들은 어떻게 처리될까?&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-메타데이터-db-선정-및-nosql-데이터-모델링&quot;&gt;6. 메타데이터 DB 선정 및 NoSQL 데이터 모델링&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#61-이메일-메타데이터의-특성&quot;&gt;6.1. 이메일 메타데이터의 특성&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#62-올바른-데이터베이스-선정&quot;&gt;6.2. 올바른 데이터베이스 선정&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#621-rdbms&quot;&gt;6.2.1. RDBMS&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#rdbms-blob-저장의-구조적-병목-원인&quot;&gt;💡RDBMS BLOB 저장의 구조적 병목 원인&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#정형structured-blob-vs-비정형unstructured-blob&quot;&gt;💡정형(Structured) BLOB vs 비정형(Unstructured) BLOB&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#622-분산-객체-저장소aws-s3-등&quot;&gt;6.2.2. 분산 객체 저장소(AWS S3 등)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#623-분산-nosql-데이터베이스구글-빅테이블-카산드라-등&quot;&gt;6.2.3. 분산 NoSQL 데이터베이스(구글 빅테이블, 카산드라 등)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#624-강한-일관성&quot;&gt;6.2.4. 강한 일관성&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#625-결론&quot;&gt;6.2.5. 결론&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#증분-백업incremental-backup이란&quot;&gt;💡증분 백업(Incremental Backup)이란?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#63-nosql-데이터-모델링-파티션-키와-클러스터-키-설계&quot;&gt;6.3. NoSQL 데이터 모델링: 파티션 키와 클러스터 키 설계&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#클러스터-키의-상세-메커니즘&quot;&gt;💡클러스터 키의 상세 메커니즘&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#user_id-단일-파티션-키가-유발하는-핫스팟-및-wide-partition-문제&quot;&gt;💡&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt; 단일 파티션 키가 유발하는 ‘핫스팟 및 Wide Partition’ 문제&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#64-nosql-질의-패턴&quot;&gt;6.4. NoSQL 질의 패턴&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#timeuuid-데이터-타입이-정렬에-필수인-이유&quot;&gt;💡TIMEUUID 데이터 타입이 정렬에 필수인 이유&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#65-읽음안읽음-메일-필터링과-nosql-테이블-비정규화&quot;&gt;6.5. 읽음/안읽음 메일 필터링과 NoSQL 테이블 비정규화&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#allow-filtering&quot;&gt;💡&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALLOW FILTERING&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#nosql이-파티션-키와-클러스터-키를-활용하여-쿼리를-처리하는-예시&quot;&gt;💡NoSQL이 파티션 키와 클러스터 키를 활용하여 쿼리를 처리하는 예시&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#66-이메일-스레드threads-대화-타래-재구성-원리&quot;&gt;6.6. 이메일 스레드(Threads) 대화 타래 재구성 원리&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#67-요약&quot;&gt;6.7. 요약&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-스팸-메일함을-피하기-위한-이메일-전송-가능성-극대화-전략&quot;&gt;7. 스팸 메일함을 피하기 위한 이메일 전송 가능성 극대화 전략&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#71-이메일-인증-프로토콜-spf-dkim-dmarc&quot;&gt;7.1. 이메일 인증 프로토콜: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SPF&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DKIM&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DMARC&lt;/code&gt;&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#spf와-dkim이-둘-다-적용되어-있을-때-하나만-실패해도-메일이-차단될까&quot;&gt;💡SPF와 DKIM이 둘 다 적용되어 있을 때, 하나만 실패해도 메일이 차단될까?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#72-ip-평판-관리와-발송-ip-범주화-전략&quot;&gt;7.2. IP 평판 관리와 발송 IP 범주화 전략&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#73-ip-warming&quot;&gt;7.3. IP Warming&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#ip-warming-기간-동안-발송-속도와-볼륨-제어는-be-내에서-어떻게-구현할까&quot;&gt;💡IP Warming 기간 동안 발송 속도와 볼륨 제어는 BE 내에서 어떻게 구현할까?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#74-스팸-필터-레이더-우회-및-피드백-루프-운영&quot;&gt;7.4. 스팸 필터 레이더 우회 및 피드백 루프 운영&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#8-고성능-검색-엔진-설계&quot;&gt;8. 고성능 검색 엔진 설계&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#81-이메일-검색-특성-극단적인-write-heavy&quot;&gt;8.1. 이메일 검색 특성: 극단적인 Write-Heavy&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#82-검색-기능을-지원하기-위한-두-가지-선택지&quot;&gt;8.2. 검색 기능을 지원하기 위한 두 가지 선택지&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#821-방안-1-데이터-저장소nosql-내부의-내장-검색-기능-활용&quot;&gt;8.2.1. 방안 1: 데이터 저장소(NoSQL) 내부의 내장 검색 기능 활용&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#데이터베이스에-내장된-전용-검색-솔루션을-이용한다는-것은-정확히-무엇일까&quot;&gt;💡데이터베이스에 내장된 전용 검색 솔루션을 이용한다는 것은 정확히 무엇일까?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#왜-일반-nosqlrdmbs-질의로는-검색이-불가능할까&quot;&gt;💡왜 일반 NoSQL/RDMBS 질의로는 검색이 불가능할까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#822-방안-2-독립된-전문-검색-엔진elasticsearch-도입&quot;&gt;8.2.2. 방안 2: 독립된 전문 검색 엔진(Elasticsearch) 도입&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#83-elasticsearch-기반-분산-검색-레이어-설계&quot;&gt;8.3. Elasticsearch 기반 분산 검색 레이어 설계&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#831-역인덱스-아키텍처와-유저-사서함-격리&quot;&gt;8.3.1. 역인덱스 아키텍처와 유저 사서함 격리&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#832-kafka를-활용한-비동기-인덱싱-파이프라인&quot;&gt;8.3.2. Kafka를 활용한 비동기 인덱싱 파이프라인&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#페타바이트-급-검색-엔진에서-인덱스가-오염되거나-스키마가-되었을-때-재색인하는-부하는-어떻게-제어할까&quot;&gt;💡페타바이트 급 검색 엔진에서 인덱스가 오염되거나 스키마가 되었을 때 재색인하는 부하는 어떻게 제어할까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#833-주-저장소nosql과-elasticsearch-간의-데이터-정합성-동기화&quot;&gt;8.3.3. 주 저장소(NoSQL)과 Elasticsearch 간의 데이터 정합성 동기화&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#주-이메일-저장소와-elasticsearch-간의-실시간-동기화-맞추기&quot;&gt;💡주 이메일 저장소와 Elasticsearch 간의 실시간 동기화 맞추기&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#역인덱스inverted-index의-기본-구조&quot;&gt;💡역인덱스(Inverted Index)의 기본 구조&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#84-맞춤형-검색-엔진과-디스크-io-병목-해결&quot;&gt;8.4. 맞춤형 검색 엔진과 디스크 I/O 병목 해결&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#841-lsm-트리-도입&quot;&gt;8.4.1. LSM 트리 도입&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#842-변하지-않는-이메일과-자주-변하는-폴더-정보의-분리-메커니즘&quot;&gt;8.4.2. 변하지 않는 이메일과 자주 변하는 폴더 정보의 분리 메커니즘&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#lsm-트리-구조-안에서-데이터를-어떻게-두-개의-파트로-쪼개는-걸까&quot;&gt;💡LSM 트리 구조 안에서 데이터를 어떻게 두 개의 파트로 쪼개는 걸까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#85-elasticsearch-vs-맞춤형-검색-엔진&quot;&gt;8.5. Elasticsearch vs 맞춤형 검색 엔진&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#86-요약&quot;&gt;8.6. 요약&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#9-가용성-향상&quot;&gt;9. 가용성 향상&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#91-단일-마스터single-primary-복제-모델을-채택하는-이유&quot;&gt;9.1. 단일 마스터(Single-Primary) 복제 모델을 채택하는 이유&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#92-cap-정리-관점에서의-트레이드오프&quot;&gt;9.2. CAP 정리 관점에서의 트레이드오프&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#split-brain-이란&quot;&gt;💡Split-Brain 이란?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#10-추가-고려사항&quot;&gt;10. 추가 고려사항&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#101-결함-감지-및-무중단-장애-복구failover&quot;&gt;10.1. 결함 감지 및 무중단 장애 복구(Failover)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#1011-gossip-protocol을-통한-상태-관리&quot;&gt;10.1.1. Gossip Protocol을 통한 상태 관리&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#102-글로벌-인프라를-위한-보안-compliance-및-데이터-격리&quot;&gt;10.2. 글로벌 인프라를 위한 보안, Compliance 및 데이터 격리&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#1021-gdpr-및-데이터-주권data-sovereignty-대응&quot;&gt;10.2.1. GDPR 및 데이터 주권(Data Sovereignty) 대응&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#1022-이중-암호화-표준-도입&quot;&gt;10.2.2. 이중 암호화 표준 도입&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#103-모니터링-및-메트릭-핵심-지표&quot;&gt;10.3. 모니터링 및 메트릭 핵심 지표&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#1031-종단-간-지연e2e-delivery-latency과-backpressure-관측&quot;&gt;10.3.1. 종단 간 지연(E2E Delivery Latency)과 Backpressure 관측&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#11-요약&quot;&gt;11. 요약&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-요구사항-정의와-규모-추정&quot;&gt;1. 요구사항 정의와 규모 추정&lt;/h1&gt;

&lt;p&gt;대규모 분산 이메일 서비스 설계란 전 세계 10억 명 이상의 사용자가 어떤 상황에서도 데이터 유실 없이 실시간으로 메일을 송수신할 수 있도록 고가용성, 고확장성, 강력한 
데이터 일관성을 보장하는 분산 인프라를 구축하는 아키텍처 기술이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-비즈니스-및-기능구조-도출&quot;&gt;1.1. 비즈니스 및 기능구조 도출&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;규모 및 타깃 유저&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 얼마나 많은 사람들이 이용하는 제품인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 글로벌 10억 명의 대규모 사용자를 타깃으로 설정한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;설계 기능 범위&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 인증부터 스팸 검증까지 아래의 모든 기능을 포함해야 하는가?
        &lt;ul&gt;
          &lt;li&gt;회원가입 및 인증&lt;/li&gt;
          &lt;li&gt;이메일 발송/수신&lt;/li&gt;
          &lt;li&gt;모든 이메일 조회(메일함 조회)&lt;/li&gt;
          &lt;li&gt;읽음 여부에 따른 이메일 필터링&lt;/li&gt;
          &lt;li&gt;제목, 발신인, 본문 내용 기반의 검색 기능&lt;/li&gt;
          &lt;li&gt;스팸 및 바이러스 방지 필터링&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 대규모 아키텍처 핵심 플로우에 집중하기 위해 &lt;strong&gt;유지보수 및 인증 시스템 설계는 제외&lt;/strong&gt;하고 진행한다. 나머지 메일 송수신, 필터링, 고속 검색, 스팸 차단은 핵심 설계 범위에 포함한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;접근 프로토콜 및 환경&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 사용자는 메일 서버에 어떻게 연결되는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 전통적으로는 SMTP, POP, IMAP 등 전용 프로토콜을 사용해 접속해 왔고 여전히 널리 쓰이지만 다소 구식인 면이 있다. 여기서는 현대적인 웹 기반 인터페이스와의 연동을 고려하여 &lt;strong&gt;HTTP/HTTPS 프로토콜&lt;/strong&gt;을 메인 소통 창구로 채택한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;첨부파일 지원 여부&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 이메일에 첨부 파일도 함께 전송할 수 있어야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 그렇다. 멀티미디어 및 문서 첨부 기능을 지원해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;12-비기능-요구사항&quot;&gt;1.2. 비기능 요구사항&lt;/h2&gt;

&lt;p&gt;10억 명의 대용량 트래픽과 데이터 적재를 견디기 위해 아키텍처가 반드시 충족해야 할 4가지 비기능적 속성이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;안정성(Reliablilty)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자의 이메일 데이터는 어떠한 경우에도 &lt;strong&gt;유실되어서는 안 된다.&lt;/strong&gt; 데이터 무결성이 최우선이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;가용성(Availability)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;장애 발생 시에도 서비스가 지속되도록 사용자 데이터를 여러 노드에 걸쳐 자동 복제(Replication)해야 한다.&lt;/li&gt;
      &lt;li&gt;일부 컴포넌트나 데이터 센터에 장애가 나더라도 시스템은 정상적으로 계속 동작해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;규모 확장성(Scalability)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자 수와 메일 트래픽이 아무리 늘어나도 시스템의 전반적인 Latency나 성능이 저하되지 않아야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;유연성 및 유연한 확장성(Flexibility &amp;amp; Extensibility)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;기존의 전통적 이메일 프로토콜(POP, IMAP)은 제공 기능이 매우 제한적이다.&lt;/li&gt;
      &lt;li&gt;스레딩(Threading), 실시간 레이블링 등 현대적인 기능을 유연하게 추가할 수 있도록 새 컴포넌트의 결합이 용이한 &lt;strong&gt;맞춤형 프로토콜 및 마이크로서비스 아키텍처 지향성&lt;/strong&gt;을 가진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;13-개략적인-규모-추정&quot;&gt;1.3. 개략적인 규모 추정&lt;/h2&gt;

&lt;p&gt;이메일 서비스는 텍스트와 대용량 바이너리(첨부 파일)가 결합하여 &lt;strong&gt;막대한 저장 용량과 트래픽&lt;/strong&gt;을 요구하는 대표적인 하이엔드 시스템이다.&lt;br /&gt;
수치 분석을 통해 분산 데이터베이스 스토리지 스케일을 예측해보자.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;하이엔드(High-End) 시스템&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;기술적으로 아키텍처 관점에서의 하이엔드 시스템은 &lt;strong&gt;컴퓨팅 자원의 한계치에 도전할 만큼 극단적인 스케일의 요구사항(초고처리량, 초대용량, 초고가용성)을 동시에 만족해야 하는
최고 난이도의 엔터프라이즈급 시스템&lt;/strong&gt;을 의미함&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;기본 가상 조건&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;전체 사용자 수:&lt;/strong&gt; 10억 명(\(10^9\) 명)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;1인당 하루 평균 발송 메일 수:&lt;/strong&gt; 10건&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;1인당 하루 평균 수신 메일 수:&lt;/strong&gt; &lt;a href=&quot;https://resources.review42.com/how-many-emails-are-sent-per-day/&quot;&gt;40건&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;이메일 1건당 평균 메타데이터 크기:&lt;/strong&gt; 50KB (첨부 파일 제외, 헤더 및 본문 텍스트 포함)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;첨부 파일 포함 메일 비율:&lt;/strong&gt; 전체의 20%&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;첨부 파일의 평균 크기:&lt;/strong&gt; 500KB&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;시스템 트래픽 및 스토리지 계산&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;이메일 전송 QPS&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하루 24시간을 시스템 계산 편의상 \(10^5초\)(정확히는 86,400초)로 수렴하여 계산한다.&lt;/li&gt;
      &lt;li&gt;
\[\text{전송 QPS} = \frac{10^9 \text{ 명} \times 10 \text{ 건}}{10^5 \text{ 초}} = 100,000 \text{ QPS}\]
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;1년간 메타데이터 저장 공간 요구사항&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 1년간 수신하는 모든 메일의 메타데이터 누적 용량이다.&lt;/li&gt;
      &lt;li&gt;
\[10^9 \text{ 명} \times 40 \text{ 건/일} \times 365 \text{ 일} \times 50 \text{ KB} = 730 \text{ PB}\]
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;1년간 첨부 파일 저장 공간 요구사항&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메일 중 20%에만 포함되는 평균 500KB의 첨부 파일을 객체 저장소에 보관할 때 필요한 총 용량이다.&lt;/li&gt;
      &lt;li&gt;
\[10^9 \text{ 명} \times 40 \text{ 건/일} \times 365 \text{ 일} \times 20\% \times 500 \text{ KB} = 1,460 \text{ PB}\]
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이처럼 1년에 처리해야 할 데이터 용량만 &lt;strong&gt;730PB&lt;/strong&gt;, 첨부 파일 &lt;strong&gt;1,460PB&lt;/strong&gt;로 도합 2EB(엑사바이트)에 육박한다.&lt;br /&gt;
초당 100,000건에 달하는 Write 트래픽과 이러한 막대한 데이터를 단일 서버나 단순 RDBMS 스토리지로 감당하는 것은 불가능하다.&lt;/p&gt;

&lt;p&gt;따라서 이 시스템을 안정적으로 운영하기 위해서는 반드시 데이터를 쪼개어 저장하는 &lt;strong&gt;샤딩&lt;/strong&gt; 기술과, 데이터 유실을 막는 &lt;strong&gt;분산 데이터베이스 솔루션&lt;/strong&gt; 및 &lt;strong&gt;대규모 오브젝트 스토리지 계층&lt;/strong&gt;이 
아키텍처의 필수 전제 조건이 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;메타데이터-저장-시-발신인과-수신인의-데이터를-따로-계산하지-않는-이유는&quot;&gt;💡메타데이터 저장 시 발신인과 수신인의 데이터를 따로 계산하지 않는 이유는?&lt;/h3&gt;

&lt;p&gt;대규모 시스템 설계에서 스토리지를 산정할 때 가장 중요한 원칙 중 하나는 &lt;strong&gt;데이터의 중복 계산을 피하는 것&lt;/strong&gt;이다.&lt;br /&gt;
‘발송 데이터’와 ‘수신 데이터’를 각각 이중으로 더해 용량을 부풀리는 실수를 하면 안 된다.&lt;br /&gt;
이메일은 구조적으로 &lt;strong&gt;‘단일 발송(Write 한번), 멀티플 수신(Read/적재 여러 번)’&lt;/strong&gt;의 형태를 띤다.&lt;br /&gt;
철수가 한 건의 메일을 작성해 영희, 길동이 2명에게 보낸다면, 시스템 계층 내에서 실제로 생성 및 분산되어 저장되어야 할 메일함 인스턴스는 수신자들의 받은 편지함 스페이스(총 2카피)이다.&lt;/p&gt;

&lt;p&gt;따라서 여기서 상정하는 &lt;strong&gt;‘하루 평균 수신 40건’이라는 수치는 발신자가 보낸 액션이 시스템 전체 인바운드로 변환되어 최종 도달한 총량&lt;/strong&gt;을 의미한다.&lt;/p&gt;

&lt;p&gt;발신자의 ‘보낸 편지함’ 메타데이터는 수신자 측에 저장된 원본 원시 데이터를 포인터 형태로 참조하거나, 이미 이 수신 기준 총합 데이터 구조 내에 흡수되어 계산된 것이므로 
중복 계산을 방지하기 위해 ‘수신 용량’을 기준으로 통합 산정하는 것이 정확하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-이메일-프로토콜과-전통적-메일-서버&quot;&gt;2. 이메일 프로토콜과 전통적 메일 서버&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;이메일 프로토콜&lt;/strong&gt;이란 인터넷망을 통해 텍스트 및 멀티미디어 메시지를 안전하게 송수신하기 위해 정의된 표준 통신 규약이다.&lt;br /&gt;
메시지를 목적지 메일 서버로 보내는 &lt;strong&gt;SMTP&lt;/strong&gt;와, 사용자가 자신의 클라이언트로 메일을 가져오는 &lt;strong&gt;POP/IMAP&lt;/strong&gt;이 메커니즘의 핵심 축을 이룬다.&lt;/p&gt;

&lt;p&gt;대규모 분산 메일 아키텍처를 안정적으로 설계하기 위해서는 수십 년간 글로벌 메일 생태계를 지탱해 온 기본 프로토콜의 동작 원리와, 
과거 단일 장비 기반의 전통적 메일 서버가 가졌던 아키텍처적 한계를 정확하게 파악해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-이메일-프로토콜&quot;&gt;2.1. 이메일 프로토콜&lt;/h2&gt;

&lt;p&gt;이메일은 송신과 수신 프로토콜이 철저하게 분리되어 동작하는 구조를 갖고 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;SMTP(Simple Mail Transfer Protocol)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이메일을 한 서버에서 다른 서버로 보내는 &lt;strong&gt;글로벌 표준 송신 프로토콜&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;POP(Post Office Protocol)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;원격 메일 서버에 도착한 이메일을 클라이언트 단말로 다운로드하기 위해 사용하는 전통적인 수신 프로토콜&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;IMAP(Internet Mail Access Protocol)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;현대 이메일 서비스에서 가장 널리 쓰이는 표준 수신 프로토콜로, 사용자의 모바일, PC 등 다양한 단말과 원격 메일 서버 상태를 &lt;strong&gt;실시간으로 동기화&lt;/strong&gt;한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;HTTPS&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;기술적으로는 메일 전송 전용 프로토콜이 아니지만, 대화형 웹메일 인터페이스나 모바일 애플리케이션(예: &lt;a href=&quot;https://en.wikipedia.org/wiki/ActiveSync&quot;&gt;MS 아웃룩의 ActiveSync 프로토콜&lt;/a&gt;)이 메일 서버의 사서함에 접속하고 API 통신을 처리할 때 핵심 인프라로 이용된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;pop-vs-imap&quot;&gt;💡POP vs IMAP&lt;/h3&gt;

&lt;p&gt;POP은 사용자가 메일을 클릭하지 않아도 무조건 메시지가 다운로드되고, 메일 서버에서 바로 삭제된다.&lt;br /&gt;
이유는 POP 프로토콜의 핵심 철학은 &lt;strong&gt;서버는 임시 보관소일 뿐이며, 메일 데이터의 실제 소유 및 관리는 사용자의 로컬 단말이 전담한다.&lt;/strong&gt;는 것이기 때문이다.&lt;br /&gt;
POP 프로토콜을 사용하는 이메일 클라이언트는 사용자가 특정 메일을 마우스로 클릭하여 ‘열람’하는 액션을 취하지 않더라도, 
&lt;strong&gt;서버에 접속하는 순간 사서함에 쌓여 있는 신규 메시지 전체를 통째로 로컬 디스크에 백그라운드로 내려받는다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;더 중요한 점은 다운로드가 완료되는 즉시 &lt;strong&gt;메일 서버에 있던 원본 데이터를 강제로 삭제&lt;/strong&gt;한다는 것이다.&lt;br /&gt;
이로 인해 다음과 같은 치명적인 한계가 발생한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;단일 단말 종속성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;스마트폰에서 POP으로 메일을 한 번 받아버리면, 원본이 서버에서 지워지기 때문에 사무실 PC나 노트북에서는 해당 메일을 확인할 수 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;네트워크 병목&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이메일을 일부만 선택해서 읽을 수 없고 사서함 전체를 내려받아야 하므로, 용량이 큰 첨부 파일이 붙은 이메일이 중간에 껴 있으면 전체 메일함으로 확인하는데 엄청난 시간이 소요된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;반면, &lt;strong&gt;IMAP&lt;/strong&gt;은 메시지를 실제로 열기 전까지는 제목, 발신인 등의 &lt;strong&gt;헤더 정보만 가볍게 다운로드&lt;/strong&gt;하며, 사용자가 메일을 클릭해야 본문과 첨부 파일을 가져온다.&lt;br /&gt;
또한 서버의 원본 사본을 지우지 않고 유지하기 때문에 여러 단말에서 동일한 메일 상태를 공유할 수 있어 개인 계정에서 가장 널리 사용된다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;비교 항목&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;POP3&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;IMAP&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;메시지 다운로드 방식&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;사용자의 클릭 여부와 무관하게 신규 메시지 전체를 로컬로 강제 다운로드&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;클릭하기 전에는 헤더만 다운로드, 클릭 시 본문 및 첨부파일 다운로드&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;서버 원본 데이터&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;다운로드 직후 서버에서 즉시 삭제 (표준 스펙)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;클라이언트와 상태를 동기화하며 서버 사본을 안전하게 유지&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;멀티 디바이스 동기화&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;불가능 (최초로 연결된 단말이 데이터를 독점)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;완벽 지원 (스마트폰, 태블릿, PC에서 동일한 사서함 공유)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;대용량 첨부 파일 처리&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;사서함 전체를 받아야 하므로 초기 로딩 속도가 매우 느림&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;헤더만 먼저 가져오므로 인터넷 속도가 느려도 부드럽게 동작&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-dns-mx-레코드&quot;&gt;2.2. DNS MX 레코드&lt;/h2&gt;

&lt;p&gt;사용자가 이메일을 발송하면, 송신 측 메일 서버는 수신자 도메인(예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gmail.com&lt;/code&gt;)의 우편함을 관리하는 실제 SMTP 서버의 IP 주소를 알아내야 한다.&lt;br /&gt;
이때 DNS 서버에 &lt;strong&gt;MX(Mail Exchange) 레코드&lt;/strong&gt;에 질의하여 수신처를 파악한다.&lt;/p&gt;

&lt;p&gt;다음은 실제 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gmail.com&lt;/code&gt;의 DNS MX 레코드를 네임서버 조회 도구(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nslookup&lt;/code&gt;)로 검색한 예시이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;nslookup
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;q&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;mx
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; gmail.com
Server:		210.123.123.12
Address:	210.123.123.12#53

Non-authoritative answer:
gmail.com	mail exchanger &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 30 alt3.gmail-smtp-in.l.google.com.
gmail.com	mail exchanger &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 10 alt1.gmail-smtp-in.l.google.com.
gmail.com	mail exchanger &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 20 alt2.gmail-smtp-in.l.google.com.
gmail.com	mail exchanger &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 5 gmail-smtp-in.l.google.com.
gmail.com	mail exchanger &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 40 alt4.gmail-smtp-in.l.google.com.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;MX 레코드 우선순위 제어 원리&lt;/strong&gt;&amp;gt;&lt;br /&gt;
조회 결과에서 메일 교환기 주소 앞에 붙은 숫자(&lt;strong&gt;5,10,20,30,40&lt;/strong&gt;)는 &lt;strong&gt;우선순위 값&lt;/strong&gt;을 나타낸다.&lt;br /&gt;
&lt;strong&gt;이 값이 낮을수록 우선순위가 높아 시스템에서 가장 선호되는 서버&lt;/strong&gt;임을 의미한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;송신자 측 메일 서버는 우선순위 값이 가장 낮은(&lt;strong&gt;5&lt;/strong&gt;) &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gmail-smtp-in.l.google.com.&lt;/code&gt; 서버에 최우선적으로 접속하여 메시지 전송을 시도&lt;/li&gt;
  &lt;li&gt;만일 해당 최우선순위 서버가 트래픽 폭주나 네트워크 장애로 인해 대답하지 않는다면, 시스템은 차선책으로 그다음 우선순위가 높은(&lt;strong&gt;10&lt;/strong&gt;) &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alt1.gmail-smtp-in.l.google.com.&lt;/code&gt; 서버로 연결을 전환(failover)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이러한 메커니즘을 통해 대규모 도메인들은 여러 대의 인바운드 메일 서버를 두고 안정적인 트래픽 분산과 결함 내성을 확보한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-mime&quot;&gt;2.3. MIME&lt;/h2&gt;

&lt;p&gt;초기의 이메일 시스템(SMTP)은 오직 &lt;strong&gt;7비트 ASCII 텍스트&lt;/strong&gt;만을 전송할 수 있도록 설계되었다.&lt;br /&gt;
이 한계를 극복하고 &lt;a href=&quot;https://en.wikipedia.org/wiki/Email_attachment&quot;&gt;멀티미디어, 바이너리 문서, 이미지 등의 파일 인스턴스를 메시지와 함께 전송&lt;/a&gt;하기 위해 탄생한 표준 규격이 바로 &lt;a href=&quot;https://en.wikipedia.org/wiki/MIME&quot;&gt;MIME(Multipurpose Internet Mail Extensions)&lt;/a&gt;이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Base64 인코딩 체계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이메일 첨부 파일은 네트워크 전송 전 MIME 규격에 따라 바이너리 데이터를 ASCII 텍스트 문자로 매핑하는 &lt;strong&gt;Base64 인코딩&lt;/strong&gt;을 거치게 된다.&lt;/li&gt;
      &lt;li&gt;이 인코딩 과정을 거치면 원본 파일의 크기가 &lt;strong&gt;약 33% 증가&lt;/strong&gt;하는 특성이 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;크기 제한 정책&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;인코딩 오버헤드와 메일 서버의 메모리/디스크 부담을 줄이기 위해 대부분의 대형 이메일 서비스 사업자는 엄격한 크기 제한을 둔다.&lt;/li&gt;
      &lt;li&gt;예를 들어 MS Outlook은 &lt;strong&gt;20MB&lt;/strong&gt;, Gmail은 &lt;strong&gt;25MB&lt;/strong&gt;로 첨부 파일 최대 용량을 제한하고 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;24-전통적-메일-서버-아키텍처&quot;&gt;2.4. 전통적 메일 서버 아키텍처&lt;/h2&gt;

&lt;p&gt;분산 시스템으로의 전환 필요성을 이해하기 위해, 과거 단일 서버 장비 기반으로 구동되던 전통적인 메일 서버 구조와 이메일 전달 라이프사이클을 살펴보자.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/email.png&quot; alt=&quot;전통적 이메일 서비스&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;전통적 메일 시스템의 메시지 라우팅 흐름&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;송신 클라이언트 액션&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;앨리스가 아웃룩 클라이언트를 통해 이메일을 작성하고 ‘보내기’ 버튼을 누르면, 메시지는 SMTP 프로토콜을 타고 아웃룩 메일 서버로 푸시된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DNS 라우팅 주소 조회&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;아웃룩 메일 서버는 DNS 질의를 수행하여 수신자 도메인인 지메일의 SMTP 서버 주소를 확인한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;서버 간 전송&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;주소를 확인한 아웃룩 메일 서버는 지메일의 인바운드 SMTP 서버에 접속하여 SMTP 프로토콜을 통해 메시지를 전송한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;수신 서버 적재&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;지메일 서버는 수신된 이메일을 내부 스토리지에 저장하고 밥이 읽어갈 수 있도록 대기시킨다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;사서함 Pulling&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수신자인 밥이 지메일에 로그인하면 클라이언트는 IMAP 또는 POP 서버에 접속하여 새 이메일 데이터를 가져온다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;241-전통적-파일-시스템-저장-구조-maildir&quot;&gt;2.4.1. 전통적 파일 시스템 저장 구조: Maildir&lt;/h3&gt;

&lt;p&gt;전통적인 메일 서버는 데이터베이스 대신 파일 시스템의 디렉터리 구조를 활용해 사용자 사서함을 관리했다.&lt;br /&gt;
대표적으로 사용되는 구조가 바로 &lt;strong&gt;Maildir&lt;/strong&gt;이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;home
└── home
    ├── user1
    │   └── Maildir
    │       ├── cur  &lt;span class=&quot;c&quot;&gt;# 사용자가 이미 읽은 메시지 보관 디렉터리&lt;/span&gt;
    │       ├── new  &lt;span class=&quot;c&quot;&gt;# 아직 읽지 않은 신규 메시지 보관 디렉터리&lt;/span&gt;
    │       └── tmp  &lt;span class=&quot;c&quot;&gt;# 전송 중인 메시지가 임시로 머무는 디렉터리&lt;/span&gt;
    └── user2
        └── Maildir
            ├── cur
            ├── new
            └── tmp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Maildir 구조에서는 &lt;strong&gt;모든 이메일 메시지가 고유한 이름을 가진 하나의 개별 파일&lt;/strong&gt;로 생성되며, 사용자의 설정과 사서함 상태가 디렉터리 분기를 통해 관리된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;⚠️단일 장비 Maildir 구조의 치명적인 한계&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;디스크 I/O 병목 현상&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자 수가 수백만, 수십억 명으로 늘어나고 한 디렉터리 내에 수천만 개의 이메일 파일이 적재되면 파일 생성, 조회, 삭제 시 극심한 파일 시스템 Lock과 &lt;strong&gt;디스크 I/O 병목&lt;/strong&gt;이 발생한다.&lt;/li&gt;
      &lt;li&gt;수십억 개의 메시지를 고속 검색하거나 백업하는 것은 사실상 불가능하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;가용성 및 안정성 결여&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터가 특정 메일 서버 단일 장비의 로컬 파일 시스템에 종속되어 보관되므로, 디스크에 물리적 손상이 발생하거나 서버 가동이 중단되면 즉시 &lt;strong&gt;데이터 유실 및 서비스 마비&lt;/strong&gt;로 이어진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;프로토콜의 기능적 한계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;POP, IMAP, SMTP 같은 전통적 프로토콜은 1960~80년대에 설계되어 현대의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Thread_(online_communication)&quot;&gt;대화형 스레딩(Threading)&lt;/a&gt;, 스마트 레이블, 복잡한 메타데이터 필터링 등의 고급 기능을 대규모 사용자 환경에서 유연하고 확장성 있게 지원하지 못한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;전통적인 이메일 시스템은 SMTP, POP, IMAP이라는 견고한 표준 프로토콜과 Maildir 기반 파일 저장소를 바탕으로 성장해 왔다.&lt;br /&gt;
그러나 10억 명의 대규모 사용자를 수용하고 페타바이트 단위의 디스크 I/O 병목을 해결하기에는 구조적 한계가 명확하다.&lt;/p&gt;

&lt;p&gt;이러한 문제를 해결하기 위해 현대적인 이메일 서비스는 무거운 파일 시스템을 걷어내고, 확장 가능한 분산 캐시와 NoSQL 데이터베이스 계층이 융합된 &lt;strong&gt;맞춤형 분산 메일 아키텍처&lt;/strong&gt;로 진화하게 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-분산-메일-서버-아키텍처-개략-설계&quot;&gt;3. 분산 메일 서버 아키텍처 개략 설계&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;분산 메일 서버 아키텍처&lt;/strong&gt;란 수억 명의 유저가 발생시키는 대용량 메일 트래픽을 단일 장비의 한계 없이 처리하기 위해, 기능별로 컴포넌트를 마이크로서비스화하고 
메시지 큐와 분산 저장소를 결합한 고가용성 인프라 시스템이다.&lt;br /&gt;
웹 표준 HTTP/HTTPS API와 실시간 푸시 기술(WebSocket)을 융합하여 유연한 규모 확장을 달성하는 것을 골자로 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-restful-api&quot;&gt;3.1. RESTful API&lt;/h2&gt;

&lt;p&gt;현대적인 분산 메일 서비스는 클라이언트와의 통신 유연성을 극대화하기 위해 웹 기반의 RESTful API를 핵심 인터페이스로 사용한다.&lt;br /&gt;
여기서는 가장 기본이 되는 4가지 핵심 엔드포인트만 다룬다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;POST &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/messages&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;기능:&lt;/strong&gt; 지정된 수신자들에게 이메일 메시지를 생성하고 발송&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;주요 파라미터:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to[]&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cc[]&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bcc[]&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subject&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;body&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attachments[]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;To, Cc, Bcc 헤더의 정의&lt;/strong&gt;&lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;&lt;strong&gt;To(수신자)&lt;/strong&gt;
      &lt;ul&gt;
        &lt;li&gt;메일의 주 수신 대상자&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;Cc(참조, Carbon Copy)&lt;/strong&gt;
      &lt;ul&gt;
        &lt;li&gt;메일 내용을 함께 공유받아야 하는 참조자&lt;/li&gt;
        &lt;li&gt;수신자 전체에게 Cc 명단이 공개됨&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;Bcc(숨은 참조, Blind Carbon Copy)&lt;/strong&gt;
      &lt;ul&gt;
        &lt;li&gt;보안이나 프라이버시를 위해 메일을 받는 다른 사람들에게 노출하지 않고 비밀리에 복사본을 받는 수신자&lt;/li&gt;
        &lt;li&gt;전송 과정에서 SMTP 프로토콜 전달 규칙에 의해 수신자 목록 헤더에서 Bcc 대상자의 정보는 완벽히 제거되어 발송됨&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;GET &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/folders&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;기능:&lt;/strong&gt; 사용자의 메일 계정에 존재하는 모든 폴더 목록 반환&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;반환 데이터 예시:&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;fld_inbox_9921&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;고유한&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;폴더&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;식별자&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Inbox&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;           &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;폴더&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;이름&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;usr_alice_01&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;계정&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;소유자&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;fld_sent_1102&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Sent&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;usr_alice_01&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;GET &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/folders/{:folder_id}/messages&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;기능:&lt;/strong&gt; 주어진 특정 폴더(예: 받은 편지함, 보낸 편지함) 하위에 속한 모든 메시지의 요약 목록을 페이지네이션 형태로 반환&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;GET &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/messages/{:message_id}&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;기능:&lt;/strong&gt; 사용자가 특정 이메일을 클릭했을 때, 해당 메시지의 본문과 세부 정보 조회&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;반환 데이터 예시:&lt;/strong&gt;
    &lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;message_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;msg_739210&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;usr_bob_02&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;계정주의&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;from&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Alice&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;alice@outlook.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;   &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;발신자의&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;lt;이름&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;이메일&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;쌍&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;to&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Bob&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;email&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;bob@gmail.com&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;수신자&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;lt;이름&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;이메일&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;쌍의&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;목록&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;subject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;분산 아키텍처 회의 일정 안내&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;이메일&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;제목&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;body&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;안녕하세요, Bob. 내일 오전 10시 분산 시스템 설계 세션이 진행됩니다.&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;이메일&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;본문&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;is_read&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;수신자가&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;메시지를&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;읽었는지&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;여부&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-분산-메일-서버-아키텍처&quot;&gt;3.2. 분산 메일 서버 아키텍처&lt;/h2&gt;

&lt;p&gt;전통적인 메일 서버는 단일 장비 위에 모든 프로세스가 올라가 확장하기 어려웠지만, 분산 아키텍처에서는 역할에 따라 완전히 계층을 분리한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/draft.png&quot; alt=&quot;개략적 설계안&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;컴포넌트별 기술적 역할&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;웹메일(Webmail)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 브라우저 및 앱을 통해 사서함과 상호작용하는 웹 프론트엔드 인터페이스&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;웹서버(Web Server)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클라이언트가 요청하는 모든 HTTP 기반 RESTful API(로그인, 가입, 메일함 조회, 발송 등)를 처리하는 Stateless 게이트웨이 계층&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;실시간 서버(Real-time Server)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클라이언트가 브라우저 창을 새로고침하지 않아도 새 이메일이 도착하면 화면에 즉시 띄워주는 &lt;strong&gt;실시간 이벤트 전송 서버&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;여기서 말하는 실시간 서버의 클라이언트는 &lt;strong&gt;현재 메일 서비스 웹 브라우저나 모바일 앱을 켜놓고 로그인해 있는 상태의 활성 유저(Active User)&lt;/strong&gt;를 뜻함&lt;/li&gt;
          &lt;li&gt;새로운 메일이 수신처 계정이 도착하면, 백엔드 워커가 실시간 서버를 통해 해당 웹소켓 세션을 찾아 ‘새 메일이 왔으니 화면을 갱신하라’는 신호를 쏨&lt;/li&gt;
          &lt;li&gt;만일 유저가 앱을 완전히 종료한 오프라인 상태라면 실시간 서버는 세션이 없으므로 이 단계를 생략하고 저장소에만 보관하며, 추후 유저가 재접속할 때 HTTP API를 통해 리스트를 Pulling 해가게 됨&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;연결 메커니즘:&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;지속적인 커넥션을 유지해야 하므로 Stateful 서버 구조를 띔&lt;/li&gt;
          &lt;li&gt;현대 시스템에서는 웹소켓을 기본 프로토콜로 하되, 구형 브라우저 호환성 및 네트워크 환경을 고려하여 백업 수단으로 &lt;a href=&quot;https://assu10.github.io/dev/2026/06/12/architecture-distributed-message-queue-architecture/#long-polling-%EC%9D%B4%EB%9E%80&quot;&gt;&lt;strong&gt;롱 폴링(Long Polling)&lt;/strong&gt;&lt;/a&gt; 방식을 결합하는 하이브리드 아키텍처가 결함 내성에 유리함&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;메타데이터 DB&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이메일의 제목, 본문 텍스트, 발신인/수신인 매핑 관계, 읽음 상태 등 정형화되거나 준정형화된 이메일 메타데이터를 저장하는 핵심 데이터 레이어&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;첨부 파일 저장소&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;대용량 바이너리 데이터를 안전하고 저비용으로 무한히 확장 보관하기 위해 AWS S3와 같은 분산 객체 저장소(Object Storage)를 채택&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;분산 캐시&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자는 일반적으로 생성된 지 얼마 되지 않은 최근 메일(2주 이내 데이터가 전체 읽기 질의의 82% 차지)을 주로 읽음&lt;/li&gt;
      &lt;li&gt;따라서 최신 메일 사서함 인스턴스를 메모리 기반의 &lt;strong&gt;Redis 클러스터&lt;/strong&gt;에 캐싱하여 디스크 DB의 가중 부담을 경감하고 응답 속도를 극대화 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;검색 저장소(Search Storage)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;본문 텍스트, 제목 키워드 등의 초고속 질의를 처리하는 분산 문서 저장소(Distributed Document Store)&lt;/li&gt;
      &lt;li&gt;텍스트를 단어 단위로 쪼개어 위치를 기록해 두는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Inverted_index&quot;&gt;&lt;strong&gt;역인덱스(Inverted Index)&lt;/strong&gt;&lt;/a&gt; 자료구조를 사용하여 탐색 시간 복잡도를 혁신적으로 줄임&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;첨부-파일-저장소로-카산드라cassandra-nosql이-부적합한-이유&quot;&gt;💡첨부 파일 저장소로 카산드라(Cassandra) NoSQL이 부적합한 이유&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/06/05/architecture-nearby/#242-%EC%9C%84%EC%B9%98-%EC%9D%B4%EB%8F%99-%EC%9D%B4%EB%A0%A5-dbcassandra&quot;&gt;Apache Cassandra&lt;/a&gt;는 대용량 Row/Column 구조의 정형 데이터를 초고속으로 Write/Read 하는데 최적화된 LSM 트리 기반 저장소이다.&lt;br /&gt;
최대 2GB 크기의 BLOB 타입을 지원하긴 하지만, 실무 환경에서 &lt;a href=&quot;https://cwiki.apache.org/confluence/spaces/CASSANDRA2/pages/120732468/CassandraLimitations&quot;&gt;1MB 이상의 큰 바이너리 파일을 직접 밀어넣으면 아래와 같은 심각한 문제&lt;/a&gt;가 발생한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;힙 메모리 및 GC 압박&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;카산드라는 데이터를 디스크에 쓰기 전 Memtable이라는 메모리 버퍼에 먼저 적재한다.&lt;/li&gt;
      &lt;li&gt;대용량 파일이 들어오면 JVM의 힙 메모리가 순식간에 고갈되어 극심한 GC Stop-the-world 현상이 발생한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Compaction 지옥&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;카산드라는 순차 쓰기 후 배경에서 파일들을 병합하고 재정렬하는 컴팩션 작업을 끊임없이 수행한다.&lt;/li&gt;
      &lt;li&gt;수십 MB짜리 레코드가 섞여 있으면 컴팩션 과정에서 엄청난 디스크 읽기/쓰기 증폭이 일어나 전체 클러스터의 I/O 성능이 마비된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;레코드 캐시 오염&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;큰 파일이 메모리 캐시 레이어를 독접하여 가볍고 빈번하게 조회되어야 할 핵심 메타데이터 텍스트 레코드들이 캐시에서 밀려나는 성능 저하를 야기한다.&lt;/li&gt;
      &lt;li&gt;따라서 대용량 파일은 무조건 S3 같은 전용 객체 저장소에 넣고, DB에는 그 &lt;strong&gt;다운로드 URL 포인터(참조 정보)만 저장&lt;/strong&gt;하는 것이 정석이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-이메일-전송-워크플로우와-메시지-큐의-역할&quot;&gt;4. 이메일 전송 워크플로우와 메시지 큐의 역할&lt;/h1&gt;

&lt;p&gt;사용자가 이메일 화면에서 ‘보내기’ 버튼을 누른 후, 최종 목적지 서버까지 도달하는 백엔드 파이프라인의 내부 전송 흐름이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/transfer.png&quot; alt=&quot;이메일 전송 절차&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;(①,②)요청 접수 및 &lt;a href=&quot;https://assu10.github.io/dev/2026/06/21/architecture-distributed-rate-limiter/&quot;&gt;처리율 제한&lt;/a&gt;&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 웹메일 인터페이스에서 메일을 전송하면 HTTPS 요청이 로드밸런서로 인입된다.&lt;/li&gt;
      &lt;li&gt;로드밸런서는 특정 악성 유저의 DDoS 공격이나 무차별 스팸 발송을 차단하기 위해 &lt;strong&gt;처리율 제한(Rate Limit)&lt;/strong&gt; 한도를 검증한 후 웹서버로 분산 전달한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(③)웹서버의 1차 라우팅 및 유효성 검증&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;웹서버는 사전에 정의된 유효성 규칙(메일 포맷, 첨부파일 크기 한도 등)을 검사한다.
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;라우팅 조건 분기&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;이 때 &lt;strong&gt;수신자 이메일 주소의 도메인이 송신자 도메인과 같은지(내부 발송) 다른지(외부 발송)&lt;/strong&gt; 체크하여 &lt;strong&gt;도메인이 같다면 메시지 큐 적재를 포함한 이후 단계를 생략&lt;/strong&gt;한다.&lt;/li&gt;
              &lt;li&gt;예를 들어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gmail.com&lt;/code&gt; 유저가 동일한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gmail.com&lt;/code&gt; 유저에게 이메일을 보내는 시나리오라면, 전 세계 인터넷망을 타고 외부 SMTP 서버를 찾아 돌아다닐 필요가 전혀 없다.&lt;/li&gt;
              &lt;li&gt;웹서버 단계에서 도메인이 일치함을 감지하면, 즉각 내부 메인 데이터베이스와 메모리 캐시 계층을 다이렉트로 업데이트하여 송신자의 ‘보낸 편지함’과 수신자의 ‘받은 편지함’에 메시지를 동시에 다이렉트로 적재한다.&lt;/li&gt;
              &lt;li&gt;외부 전송용 SMTP 큐(4단계 이후)를 거치지 않기 때문에 네트워크 레이턴시가 거의 제로에 수렴하며 인프라 자원을 크게 아낄 수 있다.&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(④)메시지 큐 적재&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;웹서버는 무거운 검증 및 발송 작업을 비동기로 처리하기 위해 메시지 큐를 버퍼로 활용한다.
        &lt;ul&gt;
          &lt;li&gt;기본 유효성 검증을 정상 통과한 메시지는 &lt;strong&gt;외부 전송 큐&lt;/strong&gt;에 넣는다. 만일 첨부 파일 크기가 너무 큰 메일이라면 바이너리는 S3 객체 저장소에 따로 저장하고, 큐에 들어가는 메시지 바디에는 해당 S3 파일 주소 참조값만 포함하여 경량화한다.&lt;/li&gt;
          &lt;li&gt;형식이 잘못 되었거나 유효성 검증에 실패한 메일은 아예 처리 대상에서 제외하고 &lt;strong&gt;에러 큐&lt;/strong&gt;로 분류하여 예외 처리한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(⑤,⑥)SMTP 작업 프로세스의 검역 및 영속화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;외부 전송 담당 SMTP 워커 프로세스들이 큐에서 메시지를 대량으로 Consuming하여 보안 필터를 구동한다.
        &lt;ul&gt;
          &lt;li&gt;(⑤)이메일 내부 본문 및 첨부 파일을 검사하여 &lt;strong&gt;스팸 여부와 바이러스 감염 여부&lt;/strong&gt;를 2차 정밀 검증한다.&lt;/li&gt;
          &lt;li&gt;(⑥)검역을 통과한 이메일은 비로소 저장소 계층 내 송신자의 ‘보낸 편지함’ 테이블에 안전하게 영속화된다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(⑦)인터넷망을 통한 최종 SMTP 발송&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;전송 프로세스는 수신자 도메인의 DNS MX 레코드 주소를 조회한 후, 해당 원격지 수신 메일 서버의 인바운드 SMTP 포트로 메시지를 최종 푸시한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-타-도메인-발송-시의-메시지-큐-규모-제어-및-백오프-전략&quot;&gt;4.1. 타 도메인 발송 시의 메시지 큐 규모 제어 및 백오프 전략&lt;/h2&gt;

&lt;p&gt;도메인이 서로 다르면 외부 전송 SMTP 프로세스와 메시지 큐의 역할이 매우 중요해진다.&lt;br /&gt;
수신처 메일 서버(예: 기업 자체 메일 서버 등)은 일시적인 전원 다운이나 네트워크 장애로 인한 먹통이 되는 경우가 빈번하기 때문이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Exponential_backoff&quot;&gt;&lt;strong&gt;지수적 백오프(Exponential Backoff)&lt;/strong&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;외부 전송 SMTP 프로세스는 수신 서버가 응답하지 않으면 즉시 발송 실패로 처리하여 유실시키지 않고, &lt;strong&gt;지수적 백오프(예: 1분 뒤 재시도 → 2분 뒤 → 4분 뒤..)&lt;/strong&gt; 전략을 사용하여 외부 전송 큐에 메시지를 retry 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Consumer Scale-out&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 대형 도메인으로 메일 발송이 폭증하여 외부 전송 큐의 Queue Depth가 임계치를 넘고 메일 전송 지연이 모니터링되면, 외부 전송 담당 SMTP 워커 인스턴스의 개수를 Scale-out 하여 처리 시간을 단축시킨다.&lt;/li&gt;
      &lt;li&gt;웹서버와 발송 아키텍처가 큐를 통해 느슨하게 결합되어 있기에 가능한 구조적 장점이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-이메일-수신-워크플로우와-실시간-알림websocket-처리&quot;&gt;5. 이메일 수신 워크플로우와 실시간 알림(WebSocket) 처리&lt;/h1&gt;

&lt;p&gt;외부의 다른 메일 서버들이 우리 시스템의 유저에게 메일을 보낼 때, 인바운드 트래픽을 안전하게 받아 처리하는 역방향 파이프라인 흐름이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/receive.png&quot; alt=&quot;이메일 수신 절차&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;(①)SMTP 인바운드 요청 접수&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;외부 메일 서버가 우리 시스템 도메인의 IP를 향해 보낸 이메일이 SMTP 로드밸런서에 도착&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(②)인바운드 수락 정책(Policy) 필터링&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;로드밸런서는 게이트웨이 단계에서 엄격한 &lt;strong&gt;이메일 수락 정책&lt;/strong&gt;을 실행함&lt;/li&gt;
      &lt;li&gt;존재하지 않는 유저 아이디(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@앞의 주소 무효&lt;/code&gt;)로 발송되었거나 블랙리스트 IP에서 인입된 메일은 TCP 연결 단계에서 즉각 거부(반송 처리)하여 내부 시스템 자원 보호&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(③,④)첨부 파일 분리 및 수신 큐(Inbound Email Queue) 적재&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수락 레이어를 통과한 이메일 중 대용량 첨부 파일은 S3 객체 저장소로 즉시 우회 적재하며, 이메일 바디와 메타데이터는 수신 이메일 큐에 전달
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;버퍼의 역할:&lt;/strong&gt; 이 인바운드 큐는 외부에서 스팸 공격이나 마케팅 대량 메일 폭탄이 순간적으로 급증하더라도, 뒤의 메일 처리 서버들이 부하를 받지 않도록 트래픽을 흡수하는 &lt;strong&gt;버퍼 역할&lt;/strong&gt;을 수행함&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(⑤)메일 처리 워커의 정밀 검역 및 저장&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메일 처리 작업 프로세스(Worker)들이 수신 큐에서 데이터를 가져와 &lt;strong&gt;스팸 메일 필터링, 악성 URL 탐지, 바이러스 백신 검사&lt;/strong&gt; 등의 무거운 보안 검증 작업을 백그라운드에서 실행&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(⑥)스토리지 영속화 및 캐싱&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;정밀 검증을 마친 이메일 인스턴스는 하부 저장소 계층의 메타데이터 DB(받은 편지함 테이블)에 저장되고, Redis 캐시에 최신 데이터로 업데이트됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;(⑦,⑧,⑨)실시간 푸시 알림 트리거&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메일 처리 워커는 수신자 유저가 현재 브라우저에 연결되어 있는 &lt;strong&gt;온라인 상태&lt;/strong&gt;인지 세션 매니저를 통해 조회함
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;(⑦,⑧)온라인 상태인 경우:&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;이메일 이벤트를 실시간 서버(WebSocket)로 즉각 보내고, 실시간 서버는 유지하고 있던 웹소켓 커넥션을 통해 유저의 화면에 ‘새 메일 도착’ 알림과 함께 메일 리스트를 동기화함&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;(⑨)오프라인 상태인 경우:&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;실시간 푸시를 생략하고 저장소 계층에만 보관 상태를 유지함&lt;/li&gt;
              &lt;li&gt;이후 사용자가 PC나 스마트폰을 켜고 웹메일에 다시 접속(HTTP RESTful API 호출)하는 시점에 웹서버가 저장소 계층에서 신규 메일을 조회해서 화면에 반환함&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;메일-처리-워커의-검증-작업스팸-바이러스-등을-통과하지-못한-이메일들은-어떻게-처리될까&quot;&gt;💡메일 처리 워커의 검증 작업(스팸, 바이러스 등)을 통과하지 못한 이메일들은 어떻게 처리될까?&lt;/h2&gt;

&lt;p&gt;위의 ⑤단계를 통과하지 못한 이메일들은 그 위험도와 정책 점수(Score)에 따라 철저하게 격리 분기 처리가 이루어진다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;1단계: 완전 매립 및 Drop(명백한 악성 바이러스/랜섬웨어)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;첨부 파일이나 본문 코드에서 확실한 악성코드 식별자가 검출된 경우, 사용자의 안전을 위해 메일을 아예 노출시키지 않고 &lt;strong&gt;영구 삭제(Drop)&lt;/strong&gt;하거나, 최고 관리자 격리 저장소로 격리 이송함&lt;/li&gt;
      &lt;li&gt;송신자에게 오류 메일조차 보내지 않는 경우가 많음(악성 해커에게 ‘이 주소가 살아있다’라는 힌트를 주지 않기 위함)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2단계: 스팸함 자동 비정규화 적재(의심스러운 광고성 메일)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;바이러스는 없으나 스팸 점수(텍스트 분석, 발신 IP 평판 점수 등)가 임계치를 초과한 경우 시스템은 이 메일을 수신자의 일반 ‘받은 편지함’ 테이블이 아닌 &lt;strong&gt;‘스팸 메일함’ 폴더 식별자(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;folder_id&lt;/code&gt;)로 태깅하여 메타데이터 DB에 격리 저장&lt;/strong&gt;함&lt;/li&gt;
      &lt;li&gt;유저는 스팸 폴더를 직접 클릭하기 전까지는 메인 화면에서 이 메일을 보지 않게 됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;3단계: 발송처 거부 응답 반송(SMTP 5xx 에러 리턴)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;검증 워커 구동 이전 혹은 정책 필터 초기 단계에서 완전히 악성 도메인으로 확인된 경우, 수신 큐에 넣기 전 SMTP 규격에 따라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;550 악성 메시지 차단(Message rejected as spam)&lt;/code&gt;과 같은 프로토콜 오류 코드를 송신 측 서버에 Echo하여 발송 자체를 거부 처리함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;10억 명의 사용자를 수용하는 분산 이메일 서비스 아키텍처의 핵심은 ‘느슨한 결합’과 ‘역할의 격리’이다.&lt;br /&gt;
클라이언트와 소통은 경량화된 HTTP API와 실시간 웹소켓으로 처리하고, 무거운 전송 및 수신 검역 연산은 분산 메시지 큐 배후의 워커 프로세스들에게 비동기로 위임한다.&lt;br /&gt;
데이터 레이어 역시 텍스트 메타데이터, 대용량 첨부파일, 고속 검색 인덱스 저장소를 철저히 분리 운영함으로써 무한한 Scale-out의 기반을 완성한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-메타데이터-db-선정-및-nosql-데이터-모델링&quot;&gt;6. 메타데이터 DB 선정 및 NoSQL 데이터 모델링&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;이메일 메타데이터 데이터 모델링&lt;/strong&gt;이란 이메일의 헤더, 본문, 상태 값 등 대규모 준정형 데이터를 디스크 I/O 병목 없이 초고속으로 조회/갱신하기 위한 분산 NoSQL 
데이터베이스의 구조(파티션 키 및 클러스터 키)를 최적화하고 테이블을 비정규화하는 아키텍처 설계 기법이다.&lt;/p&gt;

&lt;p&gt;10억 명의 사용자가 유발하는 하루 수백 테라바이트의 데이터를 안정적으로 다루기 위해서는 분산 메일 서버의 핵심인 저장소 계층의 상세 설계가 완벽해야 한다.&lt;br /&gt;
대형 이메일 서비스 사업자들이 어떤 특성을 기반하여 데이터베이스를 선정하고 모델링하는지에 대해 알아보자.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;61-이메일-메타데이터의-특성&quot;&gt;6.1. 이메일 메타데이터의 특성&lt;/h2&gt;

&lt;p&gt;이메일 데이터는 일반적인 가상 커머스나 SNS 데이터와 비교했을 때 독특한 액세스 패턴과 제약 조건을 가진다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;비대칭적 크기와 빈도&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이메일 헤더(발신인, 수신인, 시간 등)는 크기가 작지만 목록 조회 등을 위해 매우 빈번하게 액세스된다.&lt;/li&gt;
      &lt;li&gt;반면 이메일 본문은 크기가 수KB에서 수백KB까지 다양하지만, 사용자가 메일을 한 번 읽은 후에는 거의 다시 조회하지 않는 낮은 빈도의 패턴을 보인다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;완벽한 유저 격리성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;SNS 게시글은 사방으로 공유되지만, 이메일은 특정 사용자별로 데이터가 완전히 격리되어 수행된다.&lt;/li&gt;
      &lt;li&gt;즉, 내가 받은 메일은 오직 나만 읽고, 쓰고, 삭제할 수 있어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;극단적인 데이터 신선도 편향&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자는 주로 &lt;strong&gt;최근 메일&lt;/strong&gt;만 읽는다.&lt;/li&gt;
      &lt;li&gt;통계에 따르면 생성된 지 16일 이하인 최신 데이터에 발생하는 읽기 질의 비율이 전체 질의의 82%에 달한다.&lt;/li&gt;
      &lt;li&gt;데이터 동적 캐싱이 매우 효과적인 이유이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;무조건적인 데이터 안정성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;금융 시스템과 마찬가지로 이메일은 단 한 건의 데이터 유실도 용납되지 않는 높은 무결성을 요구한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;62-올바른-데이터베이스-선정&quot;&gt;6.2. 올바른 데이터베이스 선정&lt;/h2&gt;

&lt;p&gt;하루에 수억 건씩 쏟아지는 메일 적재 트래픽을 감당하기 위해 기성 데이터베이스들의 장단점을 비교 분석해보자.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;621-rdbms&quot;&gt;6.2.1. RDBMS&lt;/h3&gt;

&lt;p&gt;인덱스를 활용한 구조화된 질의와 검색에 유리하다.&lt;br /&gt;
그러나 대규모 이메일 시스템에서는 메일 본문에 HTML, 이미지 등이 섞여 쉽게 100KB를 넘어간다.&lt;br /&gt;
이를 RDBMS의 &lt;strong&gt;BLOB(Binary Large Object)&lt;/strong&gt; 자료형으로 처리하려고 하면 비정형 BLOB 자료형 데이터에 대한 검색 질의 성능이 좋지 않기 때문에 치명적인 성능 저하가 발생한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;rdbms-blob-저장의-구조적-병목-원인&quot;&gt;💡RDBMS BLOB 저장의 구조적 병목 원인&lt;/h4&gt;

&lt;p&gt;MySQL(InnoDB)이나 PostgreSQL 같은 관계형 데이터베이스는 고정된 크기의 &lt;strong&gt;데이터 페이지(Page, 보통 4KB~16KB)&lt;/strong&gt; 단위로 디스크 I/O를 수행한다.&lt;br /&gt;
100KB가 넘는 이메일 본문이나 대용량 BLOB 데이터가 들어오면, 이 데이터는 단일 페이지에 담기지 못하고 여러 개의 오버플로우 페이지를 포인터로 연결하여 디스크 외부에 따로 저장한다.&lt;/p&gt;

&lt;p&gt;결과적으로 해당 컬럼에 접근할 때마다 고속 메모리 버퍼 풀을 거치지 못하고 수많은 디스크 오버플로우 페이지를 뒤지는 &lt;strong&gt;무거운 무작위 디스크 I/O&lt;/strong&gt;가 발생한다.&lt;br /&gt;
이로 인해 트래픽 폭주 시 RDBMS 전체의 &lt;a href=&quot;https://en.wikipedia.org/wiki/IOPS&quot;&gt;IOPS(Input/Output Operation Per Second, 초당 입/출력 연산 빈도)&lt;/a&gt; 한계치를 
초과하여 DB 서버가 마비되는 현상이 벌어진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;정형structured-blob-vs-비정형unstructured-blob&quot;&gt;💡정형(Structured) BLOB vs 비정형(Unstructured) BLOB&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;정형/준정형 BLOB&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;겉보기에는 바이너리 형태 데이터이지만, 내부에 특정 규칙이나 스키마가 있는 상태를 의미한다.&lt;/li&gt;
      &lt;li&gt;예를 들어 애플리케이션 단에서 객체를 &lt;strong&gt;Protocol Buffers, Thrift, 혹은 직렬화된 JSON&lt;/strong&gt; 형태로 가공하여 RDBMS BLOB 컬럼에 저장한 경우이다.&lt;/li&gt;
      &lt;li&gt;파싱 프로세스를 거치면 내부 필드 구조를 명확히 복원할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;비정형 BLOB&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;내부 규칙이 전혀 없는 순수 바이너리 스트림이다.&lt;/li&gt;
      &lt;li&gt;사용자가 업로드한 &lt;strong&gt;압축 파일(.zip), PDF 문서, 이미지 파일(.png), 혹은 원시 오디오 스레드&lt;/strong&gt; 등이 이에 해당한다.&lt;/li&gt;
      &lt;li&gt;데이터베이스 입장에서는 안을 열어봐도 구조를 파악할 수 없는 거대한 바이트 덩어리일 뿐이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;622-분산-객체-저장소aws-s3-등&quot;&gt;6.2.2. 분산 객체 저장소(AWS S3 등)&lt;/h3&gt;

&lt;p&gt;수 페타바이트의 파일 데이터를 매우 저렴하고 안정적으로 보관할 수 있다.&lt;br /&gt;
하지만 이메일의 읽음 상태 변경, 특정 키워드 필터링 등의 빈번한 업데이트와 세밀한 메타데이터 질의를 구현하기에는 API 제약과 Latency 장벽이 너무 높다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;623-분산-nosql-데이터베이스구글-빅테이블-카산드라-등&quot;&gt;6.2.3. 분산 NoSQL 데이터베이스(구글 빅테이블, 카산드라 등)&lt;/h3&gt;

&lt;p&gt;지메일의 경우 구글 내부의 거대한 분산 NoSQL 데이터베이스인 빅테이블(Bigtable)을 저장소로 사용한다.&lt;br /&gt;
대규모 무중단 Scale-out과 고속 쓰기를 완벽히 지원하기 때문이다.&lt;br /&gt;
오픈소스 진영에서는 아파치 카산드라(Cassandra)가 훌륭한 대안이 될 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;624-강한-일관성&quot;&gt;6.2.4. 강한 일관성&lt;/h3&gt;

&lt;p&gt;분산 데이터베이스는 네트워크 장애 상황에서 &lt;strong&gt;일관성&lt;/strong&gt;과 &lt;strong&gt;가용성&lt;/strong&gt; 중 하나를 타협해야 한다.(CAP 이론)&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;CAP 이론에 대해서는 추후 다룰 예정입니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;인스타그램의 ‘좋아요 수’는 조금 뒤늦게 동기화되어도(Eventual Consistency) 괜찮지만, &lt;strong&gt;이메일은 다르다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;유저가 중요한 비즈니스 메일을 읽고 ‘읽음’ 처리를 했는데 새로고침했을 때 다시 ‘안읽음’으로 바뀐다거나, 방금 받은 메일이 다른 복제본 노드로 접속했을 때 보이지 않는다면 
서비스의 신뢰도는 완전히 낮아진다.&lt;br /&gt;
따라서 이메일 아키텍처는 데이터 정확성을 최우선하는 &lt;strong&gt;CP(Consistency &amp;amp; Partition Tolerance) 시스템&lt;/strong&gt;을 지향한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;단일 주 사본(Single Primary Replica) 원칙&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이를 달성하기 위해 분산 메일 시스템은 특정 사용자의 사서함에 발생하는 모든 읽기/쓰기 질의를 &lt;strong&gt;반드시 단 하나의 ‘주 사본(Primary/Master) 노드’를 통해서만 처리하도록 강제&lt;/strong&gt;한다.&lt;/li&gt;
      &lt;li&gt;보조 사본(Secondary)들은 오직 백업 및 주 사본 장애 시의 대기 조로만 활용된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장애 발생 시의 가용성 희생 메커니즘&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;만일 특정 유저의 사서함을 담당하는 주 사본 노드에 장애가 발생하면 어떻게 될까?&lt;/li&gt;
      &lt;li&gt;클라이언트는 다른 보조 사본 노드에 대고 즉시 메일을 쓰거나 읽는 작업을 수행할 수 &lt;strong&gt;없다.&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;보조 사본이 새로운 주 사본으로 승격(Failover)되거나 원래의 주 사본이 완벽히 복원되어 데이터 동기화 정합성이 검증될 때까지, 해당 유저의 사서함 동기화 및 메일 갱신 작업을 &lt;strong&gt;일시적으로 차단&lt;/strong&gt;된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;정합성이 깨진 데이터를 보여주느니 시스템이 계속 가동되는 것보다, &lt;strong&gt;잠시 서비스를 멈추더라도 데이터의 완벽한 정확성을 보장하는 것이 이메일 서비스의 핵심&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;625-결론&quot;&gt;6.2.5. 결론&lt;/h3&gt;

&lt;p&gt;시중에 나와있는 기성 솔루션을 그대로 써서는 이 규모를 만족할 수 없다.&lt;br /&gt;
따라서 대형 메일 사업자들은 자체 커스텀 DB 인프라를 구축하거나 분산 NoSQL을 극도로 튜닝하여 아래의 5가지 조건을 충족하는 스토리지 계층을 완성한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;단일 컬럼/로우의 크기가 수 MB 수준이어도 성능 저하가 없어야 함&lt;/li&gt;
  &lt;li&gt;사용자 관점의 &lt;strong&gt;강력한 데이터 일관성&lt;/strong&gt; 보장&lt;/li&gt;
  &lt;li&gt;디스크 무작위 I/O를 최소화하도록 설계(순차 쓰기 지향)&lt;/li&gt;
  &lt;li&gt;장애 노드가 발생해도 서비스가 중단되지 않는 초고가용성&lt;/li&gt;
  &lt;li&gt;데이터 복구를 위한 증분 백업(Incremental Backup)의 용이성&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;증분-백업incremental-backup이란&quot;&gt;💡증분 백업(Incremental Backup)이란?&lt;/h4&gt;

&lt;p&gt;증분 백업이란 매번 시스템의 전체 데이터를 백업(Full Backup)하는 대신, &lt;strong&gt;가장 최근에 수행된 백업 시점 이후로 ‘새롭게 변경되거나 추가된 데이터’만 선택해서 백업&lt;/strong&gt;하는 방식이다.&lt;/p&gt;

&lt;p&gt;앞서 &lt;a href=&quot;#13-개략적인-규모-추정&quot;&gt;1.3. 개략적인 규모 추정&lt;/a&gt;에서 산정했듯이, 10억 명 규모의 시스템은 연간 &lt;strong&gt;2.2EB(엑사바이트)&lt;/strong&gt;의 데이터를 저장한다.&lt;br /&gt;
하루에 추가되는 데이터만 해도 수 테라바이트에서 페타바이트 스케일이다.&lt;br /&gt;
매일 전체 데이터를 백업하는 것은 네트워크 대역폭과 스토리지 비용 측면에서 불가능하다.&lt;/p&gt;

&lt;p&gt;분산 NoSQL(예: 카산드라)은 데이터를 디스크에 쓸 때 기존 파일을 수정하지 않고 &lt;strong&gt;SSTable&lt;/strong&gt;이라는 불변 파일로 순차 적재한다.&lt;br /&gt;
증분 백업을 실행하면, 마지막 백업 이후 새롭게 디스크에 생성된 신규 SSTable 파일들만 그대로 복사해 오면 되기 때문에 시스템 부하를 최소화하면서 페타바이트 급 백업을 
실시간으로 완료할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;63-nosql-데이터-모델링-파티션-키와-클러스터-키-설계&quot;&gt;6.3. NoSQL 데이터 모델링: 파티션 키와 클러스터 키 설계&lt;/h2&gt;

&lt;p&gt;분산 NoSQL 환경에서는 데이터를 어떤 노드에 배치하고 어떻게 정렬할 것인지 결정하는 &lt;strong&gt;기본키(Primary Key)&lt;/strong&gt; 설계가 시스템 성능의 성패를 가른다.&lt;br /&gt;
NoSQL의 기본키는 크게 파티션 키(Partition Key)와 클러스터 키(Clustering Key)의 복합 구조로 구성된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;파티션 키(Partition Key)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터를 여러 물리 노드(서버)에 균등하게 분산(샤딩)시키는 해시 키 역할을 한다.&lt;/li&gt;
      &lt;li&gt;동일한 파티션 키를 가진 레코드들은 물리적으로 완전히 같은 노드에 모여 저장된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;클러스터 키(Clustering Key)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;같은 파티션 노드 내부에 저장된 데이터들을 디스크 상에서 정렬하는 기준이 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;클러스터-키의-상세-메커니즘&quot;&gt;💡클러스터 키의 상세 메커니즘&lt;/h3&gt;

&lt;p&gt;NoSQL 저장소 내부에서 클러스터 키는 디스크 정렬의 물리적 기준선이 된다.&lt;br /&gt;
예를 들어 클러스터 키를 DESC로 설정하면, 새로운 데이터가 들어올 때 디스크의 연속된 물리적 공간에 정렬된 상태로 쌓인다.&lt;/p&gt;

&lt;p&gt;이 구조 덕분에 특정 사용자의 메일 리스트를 가져올 때, 디스크 이곳저곳을 찾는 게 아니라 &lt;strong&gt;정렬되어 모여 있는 특정 물리 구역만 통째로 긁어오는 순차 읽기(Sequential Read)&lt;/strong&gt;가 가능해져 
쿼리 속도가 매우 빨라진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;user_id-단일-파티션-키가-유발하는-핫스팟-및-wide-partition-문제&quot;&gt;💡&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt; 단일 파티션 키가 유발하는 ‘핫스팟 및 Wide Partition’ 문제&lt;/h3&gt;

&lt;p&gt;‘사용자별 격리 시스템이니까 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt; 만 파티션 키로 쓰면 특정 유저 메일이 한 곳에 모여서 좋은 것 아닌가?’ 라고 생각할 수 있다.&lt;br /&gt;
메일 양이 적은 일반 유저라면 괜찮다.&lt;br /&gt;
하지만 메일을 하루에 수천 건씩 주고받는 기업 계정, 인플루언서, 스팸 공격을 받는 헤비 유저의 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt; 하나에 수백만 건의 메일 메타데이터 레코드가 매핑된다.&lt;/p&gt;

&lt;p&gt;NoSQL에서 단일 파티션 크기가 너무 커지면(보통 수백 MB 이상), 해당 파티션을 관리하는 특정 물리 노드의 메모리와 디스크가 터져버리는 &lt;strong&gt;Wide Partition 현상&lt;/strong&gt;이 발생하고, 
해당 노드로만 트래픽이 몰리는 핫스팟(HotSpot) 장애로 이어진다.&lt;br /&gt;
또한, 메일함 공유 관점에서도 특정 대용량 공지 메일을 여러 사용자 파티션에 매번 통째로 복사해서 저장해야 하므로 스토리지가 낭비된다.&lt;/p&gt;

&lt;p&gt;이 문제를 방지하기 위해 시스템은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt; 뒤에 사서함 종류를 뜻하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;folder_id&lt;/code&gt;를 결합한 &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;user_id, folder_id&amp;gt;&lt;/code&gt; 형태의 복합 파티션 키&lt;/strong&gt;를 채택한다.&lt;br /&gt;
이렇게 하면 한 유저의 데이터라 할지라도 받은 편지함, 보낸 편지함, 스팸함 등이 서로 다른 물리 노드로 분산 적재되므로 단일 파티션이 비대해지는 리스크를 회피할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;64-nosql-질의-패턴&quot;&gt;6.4. NoSQL 질의 패턴&lt;/h2&gt;

&lt;p&gt;대규모 분산 메일 아키텍처가 실제로 지원해야 하는 주요 데이터베이스 질의 패턴과 NoSQL 테이블 가상 설계 스키마이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;질의 1. 특정 사용자의 모든 폴더 목록 조회&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/folder1.png&quot; alt=&quot;사용자별 폴더 목록&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;파티션 키: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;사용자가 로그인했을 때 좌측 사이드바 메뉴를 구성하기 위해 사용자의 모든 폴더 구조 조회&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;질의 2. 특정 폴더 내의 이메일 목록 최신순 출력&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/folder2.png&quot; alt=&quot;폴더별 이메일&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;복합 파티션 키: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;user_id, folder_id&amp;gt;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;클러스터 키: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;email (TIMEUUID, 내림차순)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;timeuuid-데이터-타입이-정렬에-필수인-이유&quot;&gt;💡TIMEUUID 데이터 타입이 정렬에 필수인 이유&lt;/h3&gt;

&lt;p&gt;일반적인 정수형 자동 증가 ID는 단일 RDBMS 환경에서만 순서가 보장될 뿐, 서버 수천 대가 분산되어 작동하는 NoSQL 환경에서는 동기화가 불가능해 사용할 수 없다.&lt;br /&gt;
그렇다고 일반 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UUID&lt;/code&gt;를 쓰자면 완전히 무작위 문자열이기 때문에 시간순 정렬이 불가능하다.&lt;/p&gt;

&lt;p&gt;이를 해결하는 게 바로 &lt;a href=&quot;https://docs.datastax.com/en/cql-oss/3.3/cql/cql_reference/uuid_type_r.html&quot;&gt;&lt;strong&gt;TIMEUUID(version 1 UUID)&lt;/strong&gt;&lt;/a&gt;이다.&lt;/p&gt;

&lt;p&gt;TIMEUUID는 기기의 고유 MAC 주소와 함께 &lt;strong&gt;1582년 10월 15일 이후 100ns 단위로 흘러간 정밀 타임스탬프 값&lt;/strong&gt;을 내부 비트 구조의 상위에 내장하여 생성된다.&lt;br /&gt;
즉, &lt;strong&gt;생성 시간(Timestamp)과 노드 ID를 결합한 128비트 식별자&lt;/strong&gt;이기 때문에 별도의 전역 동기화 없이도 여러 샤드 서버에서 충돌 없이 시간순으로 정렬 가능한 버전 ID를 즉시 생성한다.&lt;/p&gt;

&lt;p&gt;결과적으로 중앙 서버의 통제 없이 수많은 분산 워커들이 동시다발적으로 ID를 생성하더라도 &lt;strong&gt;전 세계에서 유일한 고유성&lt;/strong&gt;을 완벽히 만족함과 동시에, 
바이트 비교 만으로도 &lt;strong&gt;ms 미만의 정밀한 시간 역순 정렬&lt;/strong&gt;이 디스크 레벨에서 자동으로 이루어지게 만드는 분산 아키텍처의 필수 컴포넌트이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;질의 3. 이메일 상세 내용 조회 및 첨부 파일 데이터 조회&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/folder3.png&quot; alt=&quot;사용자별 이메일&quot; /&gt;&lt;/p&gt;

&lt;p&gt;사용자가 리스트에서 특정 메일을 클릭했을 때 본문과 첨부파일 포인터를 조회하는 테이블 구조이다.&lt;br /&gt;
대용량 쿼리 효율을 위해 유저별 메일 상세 테이블(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emails_by_user&lt;/code&gt;)와 첨부 파일 매핑 테이블(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attachments&lt;/code&gt;)을 분리하여 운영한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;65-읽음안읽음-메일-필터링과-nosql-테이블-비정규화&quot;&gt;6.5. 읽음/안읽음 메일 필터링과 NoSQL 테이블 비정규화&lt;/h2&gt;

&lt;p&gt;RDBMS 환경에서는 메일의 읽음 상태 필터링을 아래와 같이 간단한 SQL 조건절로 수행할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cm&quot;&gt;/* RDBMS 환경의 조건절 질의 (대규모 환경에서는 풀스캔 유발로 부적합) */&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;emails_by_folder&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;folder_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;folder_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;is_read&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ORDER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BY&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;email_id&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;DESC&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;위 쿼리에서 풀스캔이 일어나는 이유&lt;/strong&gt;&amp;gt;&lt;br /&gt;
RDBMS에는 쿼리를 어떻게 실행할지 결정하는 옵티마이저가 존재한다.&lt;br /&gt;
옵티마이저는 인덱스를 탈지, 아니면 풀스캔을 할지 Cost를 계산하는데, 이 때 가장 중요하게 보는 것이 Cardinality(값의 종류가 얼마나 다양한가)이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;낮은 Cardinality로 인한 옵티마이저 동작&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;is_read&lt;/code&gt; 컬럼은 true/false 딱 두 가지 값만 가진다.&lt;/li&gt;
      &lt;li&gt;만일 어떤 헤지 유저의 받은 편지함에 100만 건의 메일이 있고, 그 중 90만 건이 읽지 않은 메일(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;is_read=false&lt;/code&gt;)라고 가정해보자.&lt;/li&gt;
      &lt;li&gt;옵티마이저 입장에서는 ‘어차피 100만 건 중 90만 건(90%)이나 찾아야 하는데, 인덱스를 타고 디스크를 왔다 갔다(Random I/O)하느니 &lt;strong&gt;그냥 풀스캔(Sequential Scan)이 훨씬 빠르겠다&lt;/strong&gt;‘라고 판단해 버린다.&lt;/li&gt;
      &lt;li&gt;결과적으로 인덱스를 만들어 두었더라도 옵티마이저가 이를 의도적으로 무시하고 풀스캔을 하는 현상이 발생한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;복합 인덱스 범위 지정 후 ‘폴더 내 전수 조사’&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;만일 테이블에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(user_id, folder_id)&lt;/code&gt; 순으로 복합 인덱스가 아주 잘 걸려있다고 가정해보자.&lt;/li&gt;
      &lt;li&gt;데이터베이스 엔진은 인덱스를 보고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE user_id = :user_id AND folder_id = :folder_id&lt;/code&gt; 구역까지는 단 몇 번의 탐색(B-Tree 탐색)만으로 빠르게 좁혀 들어간다.
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;여기까지는 풀스캔이 아니다.&lt;/strong&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;문제는 그 다음이다. 해당 유저의 특정 폴더 ‘안’에 들어왔는데, 정작 인덱스 구조 안에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;is_read&lt;/code&gt;에 대한 정보가 없다.&lt;/li&gt;
      &lt;li&gt;결국 엔진은 &lt;strong&gt;그 폴더 안에 존재하는 수만~수백만 건의 메일 레코드 데이터를 디스크 페이지에서 일일이 풀스캔&lt;/strong&gt;하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;is_read = false&lt;/code&gt;인 데이터가 맞는지 하나하나 필터링해야 한다.
        &lt;ul&gt;
          &lt;li&gt;폴더 범위를 좁혔을 뿐, 그 내부에서는 사실상 풀스캔과 다름없는 무거운 무작위 디스크 I/O가 발생하는 것이다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ORDER BY&lt;/code&gt; 부하와의 최악의 시너지&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;여기에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ORDER BY email_id DESC&lt;/code&gt; 까지 붙어있다.&lt;/li&gt;
      &lt;li&gt;만약 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;is_read&lt;/code&gt; 조건 때문에 인덱스를 제대로 타지 못하고 풀스캔했는데, 데이터베이스는 이 필터링 된 수많은 데이터들을 시간 역순으로 다시 정렬해야 한다.&lt;/li&gt;
      &lt;li&gt;메모리 공간(Sort Buffer)이 부족하면 디스크에 임시 테이블을 만들어서 정렬하는 File Sort까지 발생하여 CPU와 디스크 I/O를 동시에 마비시키는 최악의 부하가 발생한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;요약하자면 이 문제가 가장 치명적인 이유는 ‘Boolean 필터링은 인덱스 효율이 극도로 떨어지며, 대규모 사서함 환경에서는 특정 폴더 내의 수백만 건을 전수 조사하게 만들기 때문’이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;NoSQL 인덱스의 치명적인 제약 조건과 아키텍처적 우회 방안&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apache Cassandra를 비롯한 대부분의 고성능 분산 NoSQL 데이터베이스는 오직 &lt;strong&gt;파티션 키와 클러스터 키 컬럼에 대한 조건문(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE&lt;/code&gt;)만 허용&lt;/strong&gt;한다.&lt;br /&gt;
키가 아닌 일반 컬럼인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;is_read (Boolean)&lt;/code&gt; 상태 값에 대고 조건 질의를 날리면 NoSQL 엔진은 쿼리를 거부하거나, 내부 모든 파티션 노드를 풀스캔하는 
최악의 병목(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALLOW FILTERING&lt;/code&gt;)을 유발한다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALLOW FILTERING&lt;/code&gt;은 Apache Cassandra와 같은 분산 NoSQL 데이터베이스에서 &lt;strong&gt;기본키(파티션 키 및 클러스터 키)로 인덱싱되지 않은 일반 컬럼을 WHERE 절에 
사용할 때, 예측 불가능한 성능 저하(전수 스캔)을 감수하고서라도 쿼리를 강제 실행하도록 데이터베이스 엔진에 승인하는 명시적 옵션&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;allow-filtering&quot;&gt;💡&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALLOW FILTERING&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALLOW FILTERING&lt;/code&gt; 의 메커니즘은 아래와 같다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;클라이언트 쿼리 요청: WHERE user_id &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;alice&apos;&lt;/span&gt; AND is_read &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;false &lt;/span&gt;ALLOW FILTERING
                              │
                              ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;1단계: 파티션 키&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;user_id&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;를 해싱하여 해당 데이터가 있는 특정 분산 노드로 진입]
                              │
                              ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;2단계: 노드 디스크에서 해당 유저의 사서함 레코드 &lt;span class=&quot;s1&quot;&gt;&apos;전체(Full Partition)&apos;&lt;/span&gt;를 메모리로 로드]
                              │
                              ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;3단계: JVM 힙&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Heap&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 메모리 단에서 루프를 돌며 &lt;span class=&quot;s1&quot;&gt;&apos;is_read == false&apos;&lt;/span&gt;인 데이터만 필터링]
                              │
                              ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;4단계: 필터링된 최종 결과 몇 건만 클라이언트에 반환 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;나머지 대량의 로드 데이터는 버림&lt;span class=&quot;o&quot;&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉, 메모리로 무식하게 다 퍼 올려서 수작업으로 골라내기를 수행하는 것이다.&lt;/p&gt;

&lt;p&gt;데이터가 몇십 건 없는 개발 환경에서는 수 ms 만에 끝나기 때문에 아무 문제 없어 보이지만, 여기서 설계하는 &lt;strong&gt;10억 명 규모의 하이엔드 시스템&lt;/strong&gt;에서는 이야기가 완전히 달라진다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;⚠️극심한 읽기 증폭&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 원하는 데이터는 단 5건의 ‘안 읽은 메일’일 지라도, 그 사서함에 쌓인 10만 건의 메일 원시 데이터를 읽기 위해 디스크의 수많은 불변 파일(SSTable)을 다 뒤져서 메모리로 가져와야 한다.&lt;/li&gt;
      &lt;li&gt;단 몇 건을 위해 수천배의 디스크 I/O 대역폭을 낭비하는 것이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;⚠️JVM GC Hell&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;초당 수만 건의 목록 조회(High QPS)가 들이치는데, 매 요청마다 수만 줄의 로우 데이터를 노드 메모리에 적재했다가 버리는 행위가 반복되면 Heap 메모리에 엄청난 가비지 객체가 쌓인다.&lt;/li&gt;
      &lt;li&gt;결국 GC가 구동되면서 서버가 일시적으로 완전히 마비되는 &lt;strong&gt;Stop-the-world&lt;/strong&gt; 현상으로 이어진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;아래는 GC Hell 이 발생하는 흐름이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;폭발적인 QPS 인입] 
       │
       ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;노드마다 수백만 개 로우를 JVM 힙 메모리로 로드] 
       │
       ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;메모리 임계치 초과 및 극심한 GC&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;가비지 컬렉션&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 발생] 
       │
       ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Stop-the-World &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;서버 일시 마비&lt;span class=&quot;o&quot;&gt;)]&lt;/span&gt; 
       │
       ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;클라이언트 타임아웃 및 재시도 요청 폭주 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;도미노 붕괴&lt;span class=&quot;o&quot;&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALLOW FILTERING&lt;/code&gt;을 써도 되는 유일한 예외&lt;/strong&gt;&amp;gt;&lt;br /&gt;
이 옵션이 무조건 나쁜 것은 아니다.&lt;br /&gt;
아키텍처 관점에서 딱 한 가지 안전하게 쓸 수 있는 시나리오가 있다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;‘파티션 키 조건을 극도로 좁혀서, 필터링할 대상 로우 수가 확실하게 통제될 때’&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;예를 들어 쿼리에서 파티션 키와 클러스터 키를 완벽하게 지정하여 이미 디스크 상에서 단 10~20줄의 레코드만 읽어오도록 바운더리가 쳐진 상태라면,&lt;br /&gt;
그 안에서 인덱스 없는 컬럼 한 두개를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALLOW FILTERING&lt;/code&gt; 으로 걸러내는 것은 메모리나 디스크에 부담을 주지 않으므로 안전하고 유용하다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;구분&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;일반 인덱스 쿼리&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;ALLOW FILTERING 쿼리&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;성능 예측 가능성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;데이터 규모가 커져도 항상 수 밀리초(ms) 이내 보장&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;파티션 내 데이터양에 비례하여 기하급수적으로 느려짐&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;주요 병목 지점&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;없음 (최적화된 탐색)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;극심한 디스크 읽기 증폭 및 노드 메모리(GC) 압박&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;대규모 시스템 적합성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;적합&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;부적합 (대형 장애의 주원인)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;결론적으로 대규모 분산 아키텍처를 설계할 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALLOW FILTERING&lt;/code&gt;을 써야만 쿼리가 돌아가는 상황을 맞이했다면, 그것은 쿼리의 문제가 아니라 
&lt;strong&gt;데이터 모델링(스키마 설계) 단계부터 잘못되었다는 강력한 경고 시그널&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;이를 해결하기 위해 저장 공간을 조금 더 희생하더라도 아래에 나올 내용처럼 &lt;strong&gt;읽음/안읽음 전용 테이블을 따로 만들어서 데이터를 복사 적재하는 비정규화 전략&lt;/strong&gt;을 취하는 것이 
하이엔드 분산 시스템을 구축하는 정석이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;nosql이-파티션-키와-클러스터-키를-활용하여-쿼리를-처리하는-예시&quot;&gt;💡NoSQL이 파티션 키와 클러스터 키를 활용하여 쿼리를 처리하는 예시&lt;/h3&gt;

&lt;p&gt;분산 NoSQL인 Cassandra에서 인덱스 제약을 우회하고 대규모 질의 속도를 초고속으로 유지하기 위해 사용하는 실제 &lt;strong&gt;CQL(Cassandra Query Language)&lt;/strong&gt; 구동 메커니즘이다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-cassandraql&quot;&gt;/* 1. NoSQL의 올바른 복합키 검색 예시 (디스크 순차 탐색으로 매우 빠름) */
SELECT * FROM emails_by_folder 
WHERE user_id = &apos;usr_alice&apos; AND folder_id = &apos;fld_inbox&apos;
AND email_id &amp;lt; max_timeuuid_tracker 
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;위 쿼리는 파티션 키로 정확한 서버 노드를 찾고, 디스크를 시간순으로 정렬된 키인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;email_id&lt;/code&gt;의 특정 구간만 정확히 오프셋 스캔하므로 10억 명 규모에서도 수 ms 안에 연산이 종료된다.&lt;/p&gt;

&lt;p&gt;하지만 읽음/안읽음 상태 필터링을 위해 이 스키마를 고집하면 대규모 트래픽에서 병목이 생기므로, 여기서는 &lt;strong&gt;테이블 비정규화 전략&lt;/strong&gt;을 사용하여 구조를 완전히 분리한다.&lt;br /&gt;
주어진 폴더에 속한 모든 이메일을 조회한 후 애플리케이션 단에서 필터링해도 되지만 그 방안은 대규모 서비스에는 그다지 적합하지 않다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/folder4.png&quot; alt=&quot;읽은 메일과 읽지 않은 메일을 위한 테이블&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 이미지에서 보듯 사서함을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;read_emails&lt;/code&gt;와  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unread_emails&lt;/code&gt;라는 &lt;strong&gt;두 개의 독립된 테이블로 복사 분할&lt;/strong&gt;해 버리는 것이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;상태 변경 시 애플리케이션 단의 듀얼 트랜잭션 제어 흐름&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저가 ‘안 읽은 메일’ 목록에서 특정 메일을 클릭하는 순간, BE 애플리케이션은 아래와 같이 NoSQL 쓰기 파이프라인을 작동시킨다.
        &lt;ul&gt;
          &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unread_emails&lt;/code&gt; 테이블에서 해당 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;email_id&lt;/code&gt; 로우 &lt;strong&gt;삭제(DELETE)&lt;/strong&gt;&lt;/li&gt;
          &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;read_emails&lt;/code&gt; 테이블에 해당 메일 메타데이터 로우를 새롭게 &lt;strong&gt;삽입(INSERT)&lt;/strong&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;쓰기 작업이 증가하고 코드는 다소 복잡해지지만, NoSQL의 핵심 장점인 &lt;strong&gt;‘쓰기 비용은 매우 싸다’&lt;/strong&gt;를 적극 활용하여 대규모 환경에서 고속 필터링 조회 성능을 취하는
트레이드오프 설계 방식이다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;비교 항목&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;[Bad] 일반 컬럼 필터링 스키마&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;[Good] 읽음/안읽음 테이블 분리 스키마&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;디스크 I/O 방식&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;파티션 내 모든 SSTable을 뒤지는 무작위 스캔&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;필요한 테이블(unread_emails)의 특정 구간만 순차 읽기&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;메모리(JVM Heap) 부하&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;전수 조사를 위한 대량의 데이터 로드로 GC 위험 높음&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;딱 보여줄 페이지 분량(예: 20건)만 로드하므로 메모리 매우 안정적&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;대규모 QPS 감당 여부&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;불가능 (디스크 및 CPU 병목으로 노드 다운)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;완벽 지원 (쓰기 비용을 활용해 읽기 성능 극대화)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;애플리케이션 복잡도&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;단순함 (상태값만 갱신하면 끝)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;복잡함 (상태 변경 시 Delete와 Insert를 동시에 제어해야 함)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;66-이메일-스레드threads-대화-타래-재구성-원리&quot;&gt;6.6. 이메일 스레드(Threads) 대화 타래 재구성 원리&lt;/h2&gt;

&lt;p&gt;현대적인 이메일 클라이언트가 제공하는 가장 핵심적인 UX는 연관된 답장 메일들을 하나의 타래로 묶어 보여주는 &lt;a href=&quot;https://en.wikipedia.org/wiki/Thread_(online_communication)&quot;&gt;&lt;strong&gt;스레딩(Threading)&lt;/strong&gt; 기능&lt;/a&gt;이다.&lt;br /&gt;
분산 환경에서는 대형 메일 클라이언트의 구동 원리인 &lt;a href=&quot;https://www.jwz.org/doc/threading.html&quot;&gt;&lt;strong&gt;JWZ 스레딩 알고리즘&lt;/strong&gt;&lt;/a&gt;을 주로 활용한다.&lt;/p&gt;

&lt;p&gt;이 알고리즘은 이메일 발송 시 메시지 내부에 규격으로 포함되는 아래 &lt;strong&gt;3가지 핵심 헤더 포인터&lt;/strong&gt;를 추적하여 메모리상에서 트리 구조를 동적으로 재구성한다.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;headers&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Message-Id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;msg_root_7BA04B2A@gmail.com&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;In-Reply-To&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;msg_root_7BA04B2A@gmail.com&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;References&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;msg_root_7BA04B2A@gmail.com&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&amp;lt;msg_reply_991A2B3C@gmail.com&amp;gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;핵심 헤더 (Header)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;아키텍처적 역할 및 기능 설명&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Message-Id&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;해당 이메일 메시지가 생성될 때 발신 클라이언트가 부여하는 전 세계 유일한 식별자&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;In-Reply-To&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;현재 메일이 &lt;strong&gt;어떤 부모 이메일 메시지에 대한 답신(Reply)&lt;/strong&gt;인지 직전의 부모 Message-Id를 기록함&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;최초의 시작 메일부터 현재 메일에 이르기까지 대화 타래에 엮인 모든 조상 및 형제 메시지 식별자 리스트를 체인 형태로 누적 보관함&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;이 필드들이 존재하면 이메일 클라이언트는 메일 DB에서 특정 유저의 메일 목록을 조회한 후, BE 메모리단에서 이 조상-부모 포인터 체인을 엮어 복잡한 관계형 Join 연산 없이도 
트리 형태의 대화 타래를 브라우저에 렌더링할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;67-요약&quot;&gt;6.7. 요약&lt;/h2&gt;

&lt;p&gt;대규모 분산 이메일 시스템의 상세 설계 핵심은 기성 RDBMS의 무거운 랜덤 I/O 잠금을 과감히 포기하고, &lt;strong&gt;분산 NoSQL 기반의 복합키 모델링과 과감한 테이블 비정규화&lt;/strong&gt;를 감행하는 것이다.&lt;br /&gt;
복합 파티션 키로 사서함의 크기 팽창을 막고, TIMEUUID 기반 클러스터 키로 디스크 순차 읽기를 보장하며, 읽음 상태별로 테이블을 쪼개어 배치함으로써 10억 명 규모에서도 지연 없는 
초고속 데이터 레이어를 완성할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;7-스팸-메일함을-피하기-위한-이메일-전송-가능성-극대화-전략&quot;&gt;7. 스팸 메일함을 피하기 위한 이메일 전송 가능성 극대화 전략&lt;/h1&gt;

&lt;p&gt;이메일 전송 가능성이란 발송된 이메일이 수신측 ISP(Gmail, Outlook 등)의 스팸 필터에 걸리지 않도록 하고, 사용자의 &lt;strong&gt;‘받은 편지함’에 안전하게 도착하는 비율&lt;/strong&gt;을 의미한다.&lt;br /&gt;
&lt;a href=&quot;https://www.statista.com/statistics/420391/spam-email-traffic-share/&quot;&gt;statista 사에서 수행한 연구&lt;/a&gt;에 따르면 메일 가운데 50%가 스팸으로 분류된다.&lt;br /&gt;
대규모 분산 메일 시스템에서는 아무리 인프라가 견고해도 이 전송 가능성이 확보되지 않으면 무용지물이 되므로, 아키텍처 설계 단계에서 보안 프로토콜과 IP 평판 제어가 완벽히 융합되어야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;71-이메일-인증-프로토콜-spf-dkim-dmarc&quot;&gt;7.1. 이메일 인증 프로토콜: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SPF&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DKIM&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DMARC&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;현재 이메일 생태계에서 수신측 서버는 신원이 불분명한 IP의 메일을 즉시 차단한다.&lt;br /&gt;
자신이 신뢰할 수 있는 합법적인 발송자임을 증명하기 위해 아래 3가지 핵심 인증 메커니즘을 DNS 레벨에 필수적으로 구성해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Sender_Policy_Framework&quot;&gt;&lt;strong&gt;SPF&lt;/strong&gt;(Sender Policy Framework)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;개념:&lt;/strong&gt; 도메인 소유주가 ‘우리 도메인의 메일은 이 IP 주소 목록에서만 발송된다.’고 DNS TXT 레코드로 명시해두는 화이트리스트 규격이다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;검증 방식:&lt;/strong&gt; 수신측 메일 서버는 메일이 인입되면 발신인의 도메인 DNS를 조회하여, 메일을 보낸 실제 서버 IP가 SPF 레코드 목록에 포함되어 있는지 대조한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail&quot;&gt;&lt;strong&gt;DKIM&lt;/strong&gt;(DomainKeys Identified Mail)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;개념:&lt;/strong&gt; 발신 메일의 헤더에 암호화된 디지털 서명을 포함하여, 전송 과정에서 메일 내용이 위변조되지 않았음을 증명하는 기술이다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;검증 방식:&lt;/strong&gt; 발신 서버가 비대칭키 쌍의 Private Key로 메일 헤더를 부호화해서 보내면, 수신 서버는 발신 도메인의 DNS에 공개된 Public Key를 가져와 서명을 해독하고 유효성을 검증한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://dmarc.org/&quot;&gt;&lt;strong&gt;DMARC&lt;/strong&gt;(Domain-based Message Authentication, Reporting and Conformance)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;개념:&lt;/strong&gt; SPF와 DKIM의 검증 결과를 기반으로, ‘만일 두 인증 중 하나라도 통과하지 못하면 이 메일을 어떻게 처리하라’고 수신 서버에게 내리는 최종 가이드라인 정책이다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;정책 레벨:&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;none&lt;/code&gt;: 모니터링만 하고 메일은 정상 수신 처리&lt;/li&gt;
          &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;quarantine&lt;/code&gt;: 메일을 스팸함으로 격리&lt;/li&gt;
          &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reject&lt;/code&gt;: 메일 수신을 강력하게 Block하고 반송&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;아래는 지메일 메시지의 헤더 예시이다.&lt;br /&gt;
발송자 도메인 @info6.citi.com 이 SPF, DKIM, DMARC의 검증을 거쳤다는 사실이 기록되어 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/gmail.png&quot; alt=&quot;지메일 헤더 예시&quot; /&gt;&lt;/p&gt;

&lt;p&gt;메일 서버를 구성하고 메일을 보내는 것은 쉽지만 특정 사용자의 메일함에 실제로 메일이 전달되도록 하는 것은 어렵다.
이메일이 스팸 폴더에 들어가버리면 수신자가 메일을 읽을 가능성은 아주 낮아진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;spf와-dkim이-둘-다-적용되어-있을-때-하나만-실패해도-메일이-차단될까&quot;&gt;💡SPF와 DKIM이 둘 다 적용되어 있을 때, 하나만 실패해도 메일이 차단될까?&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;SPF와 DKIM의 독립적 관계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;기본적으로 SPF와 DKIM은 서로 완전히 독립적인 프로토콜이다.&lt;/li&gt;
      &lt;li&gt;네트워크 포워딩 환경 등 특이 시나리오에서는 IP가 바뀌어 SPF는 실패하지만 암호 서명은 유지되어 DKIM은 통과하는 경우가 발생할 수 있다.&lt;/li&gt;
      &lt;li&gt;DMARC가 설정되어 있지 않다면, 수신측 ISP(Gmail 등) 자체 알고리즘에 따라 복합 점수를 매겨 통과 여부를 결정한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DMARC 정렬과 의사결정 구조&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;DMARC는 이 혼란을 정리하기 위해 &lt;strong&gt;정렬&lt;/strong&gt; 개념을 도입한다.&lt;/li&gt;
      &lt;li&gt;사용자의 눈에 보이는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;From&lt;/code&gt; 헤더의 도메인이 SPF 검증 도메인 및 DKIM 서명 도메인과 완벽히 일치하는지 체크한다.&lt;/li&gt;
      &lt;li&gt;DMARC 정책이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reject&lt;/code&gt;로 강하게 세팅되어 있다면, SPF와 DKIM 검증 중 단 하나라도 실패하고 도메인 정렬이 깨졌을 때 수신 서버는 메일을 차단해버린다.&lt;/li&gt;
      &lt;li&gt;따라서 대규모 아키텍처에서는 완벽한 무결성을 위해 세 가지 레코드를 체인처럼 엮어 동기화해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;72-ip-평판-관리와-발송-ip-범주화-전략&quot;&gt;7.2. IP 평판 관리와 발송 IP 범주화 전략&lt;/h2&gt;

&lt;p&gt;메일을 발송하는 인프라의 외부 IP 주소가 어떤 평판을 유지하고 있느냐에 따라 ISP 정책 수락이 달라진다.&lt;br /&gt;
시스템은 공유 IP와 전용 IP의 장단점을 파악하는 것을 넘어, 이메일의 성격에 따라 발송 인프라를 격리(범주화)해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;발송 구조별 특징 비교&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;아키텍처 분류&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;공유 IP (Shared IP)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;전용 IP (Dedicated IP)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;개념&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;여러 유저나 서비스가 동일한 발송 IP 대역을 함께 공유하여 사용&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;우리 서비스(또는 특정 헤비 유저) 전용으로 독립된 발송 IP를 고정 할당&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;장점&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;초기 비용이 저렴하며, 메일 발송량이 적어도 다른 유저들의 트래픽 덕분에 기본 IP 평판이 유지됨&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;타 유저의 스팸 발송으로 인한 동반 블록 위험이 전혀 없음. 평판 통제권 100% 확보&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;단점&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;같은 IP를 쓰는 이웃 유저가 스팸 메일을 대량 살포하면 나까지 덩달아 스팸 IP로 낙인찍힘&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;비용이 비싸며, 주기적으로 대량의 메일을 일정하게 발송하여 스스로 평판을 관리해야 함&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;적합한 타깃&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;스타트업 초기 단계, 혹은 일일 발송량이 수천 건 미만인 경량 서비스&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;10억 유저 감당 시스템, 금융/결제 알림 시스템, 일 발송량 수십만 건 이상의 엔터프라이즈&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;이메일 성격에 따른 IP 범주화&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;범주가 다른 이메일은 반드시 물리적으로 분리된 별도의 IP 주소를 통해 발송&lt;/strong&gt;해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;마케팅/광고성 이메일&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;대량 발송 특성을 가지며 수신자가 ‘스팸’이나 ‘프로모션’ 탭으로 분류할 확률이 매우 높다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;트랜잭션(중요 알림) 이메일&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;비밀번호 재설정, 결제 영수증, 보안 인증 등 사용자가 즉시 받아보아야 하는 필수 메일&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;만일 중요 알림 메일과 광고성 마케팅 메일을 동일한 서버(IP)에서 발송하면, ISP의 보안 엔진은 마케팅 메일의 높은 스팸 신고율과 낮은 오픈율을 근거로 해당 IP 전체의 
평판 점수를 깎아버린다.&lt;br /&gt;
그 결과 &lt;strong&gt;같은 IP를 공유하던 다른 유저의 비밀번호 변경 인증 메일까지 ISP 단에서 대거 블록 당하거나 스팸함으로 가는 치명적인 연쇄 장애&lt;/strong&gt;가 발생한다.&lt;/p&gt;

&lt;p&gt;새로 구성한 메일 서버가 보내는 메일은 십중팔구 스팸 폴더로 떨어지는데, 인터넷에서 좋은 평판을 쌓을 기회가 전혀 없었기 때문이다.
이메일 전송 가능성을 높이기 위해 아래 요소들을 고려해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;73-ip-warming&quot;&gt;7.3. IP Warming&lt;/h2&gt;

&lt;p&gt;대규모 서비스를 위해 새로운 전용 IP(Dedicated IP) 대역을 대량 구매했다고 해서, 첫날부터 1억 명에게 메일을 보내면 &lt;strong&gt;100% 확률로 모든 ISP에서 영구 차단&lt;/strong&gt;당한다.&lt;/p&gt;

&lt;p&gt;ISP 보안 엔진은 과거 발송 이력이 전혀 없는 신규 IP(Cold IP)에서 갑자기 대량의 트래픽이 터져 나오는 것을 전형적인 ‘해킹된 봇넷의 스팸 공격’으로 인지하기 때문이다.&lt;br /&gt;
이를 방지하기 위한 필수 절차가 바로 &lt;strong&gt;IP Warming&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;IP Warming은 새 발송 IP에 대한 ISP의 신뢰를 구축하기 위해, &lt;strong&gt;수주에 걸쳐 일일 이메일 발송 볼륨을 단계적으로 조금씩 늘려나가는 과정&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;아마존 SES에 따르면 새로운 IP 주소를 메일 발송에 아무 문제없이 쓸 수 있게 되는 때는 &lt;a href=&quot;https://docs.aws.amazon.com/ses/latest/dg/dedicated-ip-warming.html&quot;&gt;대략 2주&lt;/a&gt;가 소요된다고 한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;1주차: 극도의 조심성] ──&amp;gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;2주차: 완만한 성장] ──&amp;gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;3주차: 본격적인 가속] ──&amp;gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;4주차: 타깃 스케일 달성]
  - 1일차: 100건            - 8일차: 2,000건          - 15일차: 20,000건        - 22일차: 200,000건
  - 3일차: 500건            - 10일차: 5,000건         - 17일차: 50,000건        - 24일차: 500,000건
  - 7일차: 1,000건          - 14일차: 10,000건        - 21일차: 100,000건       - 28일차: 1,000,000건+
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;ip-warming-기간-동안-발송-속도와-볼륨-제어는-be-내에서-어떻게-구현할까&quot;&gt;💡IP Warming 기간 동안 발송 속도와 볼륨 제어는 BE 내에서 어떻게 구현할까?&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;메시지 큐 기반의 &lt;a href=&quot;https://assu10.github.io/dev/2026/06/21/architecture-distributed-rate-limiter/&quot;&gt;처리율 제한(Rate Limit)&lt;/a&gt; 연동&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;인프라 단에서 IP 웜업 스케줄러가 BE 외부 전송 큐의 &lt;strong&gt;Consumer 스레드 풀 크기를 동적으로 제어&lt;/strong&gt;한다.&lt;/li&gt;
      &lt;li&gt;예를 들어 1일차 제한이 100건이라면, 스케줄러는 당일 총 100건의 메시지만 워커로 넘어가도록 게이트를 제어하고, 초과 유입되는 발송 요청은 버퍼 큐에 묶어두거나 애플리케이션 단에서 ‘내일 발송 예약’ 상태로 Backoff 시킨다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Engagement 기반의 유저 필터링 기법&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;웜업 성공률을 극대화하려면 1~2주차 초기 트래픽에 &lt;strong&gt;‘가장 메일을 잘 열어볼 것 같은 활성 유저(High Engagement User)’&lt;/strong&gt;를 배치해야 한다.&lt;/li&gt;
      &lt;li&gt;최근 1주일 이내에 로그인했거나 메일을 읽었던 유저들의 데이터셋을 Redis 캐시나 분석 DB에서 추출하여 웜업 초기 타깃으로 넣는다.&lt;/li&gt;
      &lt;li&gt;ISP 필터는 ‘이 신규 IP가 보낸 메일은 수신자들이 다들 바로 열어본다’라고 판단하여 평판 점수를 올려주게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;즉, BE 내에서는 외부 전송 큐 배후의 Consumer 워커 스레드 풀 개수를 이 웜업 스케줄러와 연동하여 일일 쿼터를 초과하는 메일을 다음 날로 Backoff 시키는 제어 로직을 구동한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;74-스팸-필터-레이더-우회-및-피드백-루프-운영&quot;&gt;7.4. 스팸 필터 레이더 우회 및 피드백 루프 운영&lt;/h2&gt;

&lt;p&gt;발송 파이프라인의 최종 단계에서는 수신자들의 부정적인 반응을 실시간으로 감지하고 처리하는 시스템이 작동해야 평판 하락을 막을 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;피드백 루프(FBL, FeedBack Loop) 연동 아키텍처&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;지메일 등 대형 ISP는 사용자가 메일 화면에서 &lt;strong&gt;스팸으로 신고&lt;/strong&gt; 버튼을 누르면, 발신자 측에 이 사실을 Webhook이나 메일 형태로 즉시 통보해주는 ‘피드백 루프’ 서비스를 제공한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;수신자가 메일함에서 우리 메일을 ‘스팸 신고’함&lt;/li&gt;
  &lt;li&gt;ISP(예: Gmail)가 우리 시스템의 FBL 수신 엔드포인트(HTTP API)로 신고 이벤트 패킷 전송&lt;/li&gt;
  &lt;li&gt;우리 백엔드의 FBL 처리 워커가 해당 수신자 이메일 주소를 발송 금지 블랙리스트 테이블에 즉시 영속 적재&lt;/li&gt;
  &lt;li&gt;이후 마케팅이나 공지 대량 발송 워커가 구동될 때 이 발송 금지 블랙리스트를 먼저 조회하여 해당 유저를 발송 대상에서 원천 배제&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;사용자가 스팸 신고를 했음에도 불구하고 시스템이 감지하지 못하고 다음 날 또 메일을 보내면, ISP는 해당 발신 IP 대역 전체를 영구 차단 리스트에 올린다.&lt;br /&gt;
따라서 FBL 수신 가공은 실시간 스트리밍 아키텍처로 매우 기민하게 움직여야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Hard Bounce와 Soft Bounce의 격리 처리&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;아래 그림은 외부 ISP로부터 인입되는 다양한 형태의 피드백(스팸 신고, 영구 실패, 일시 오류)을 수신 큐 배후의 워커들이 어떻게 분기하여 처리하는지 보여주는 아키텍처 내부 흐름이다.
&lt;img src=&quot;/assets/img/dev/2026/0704/feedback.png&quot; alt=&quot;피드백 유형별 처리&quot; /&gt;&lt;/p&gt;

&lt;p&gt;메일 발송 후 목적지 서버로부터 에러 리턴을 받았을 때, 에러의 종류를 엄격히 분기해야 파이프라인 오염을 막을 수 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Hard Bounce(경성 반송, 5xx 에러 수신 시)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수신자 이메일 주소가 존재하지 않거나 도메인이 완전히 폐쇄된 경우처럼 &lt;strong&gt;영구적인 발송 실패&lt;/strong&gt;를 의미함&lt;/li&gt;
      &lt;li&gt;유저 계정 상태를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Invalid&lt;/code&gt;로 변경 ➔ 영구 발송 차단하여 IP 평판 방어
        &lt;ul&gt;
          &lt;li&gt;존재하지 않는 주소로 메일을 계속 발송하는 행위는 스팸 봇의 대표적인 특징이기 때문&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Soft Bounce(연성 반송, 4xx 에러 수신 시)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수신자의 사서함 용량이 가득 찼거나, 상대 서버의 일시적인 네트워크 마비 등 &lt;strong&gt;일시적인 발송 실패&lt;/strong&gt;를 의미함&lt;/li&gt;
      &lt;li&gt;메시지 큐의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Retry Queue&lt;/code&gt;로 우회 ➔ 지수적 백오프 알고리즘에 의거해 재시도 스케줄링&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;FBL(Spam) 알림 수신 시&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수신인이 ‘스팸 신고’ 버튼을 누른 경우&lt;/li&gt;
      &lt;li&gt;즉시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Suppression List(발송 금지 테이블)&lt;/code&gt;에 유저 저장 ➔ 향후 모든 발송 대상에서 제외&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 모든 용어를 기억할 필요는 없고, 이메일이 목적지에 성공적으로 도착하기 어렵다는 사실만 알면 된다.
도메인 지식이 필요한 것은 물론이고, ISP와 좋은 관계를 유지할 필요도 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;8-고성능-검색-엔진-설계&quot;&gt;8. 고성능 검색 엔진 설계&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;이메일 검색 엔진 아키텍처&lt;/strong&gt;란 사용자가 사서함 내 수많은 건의 메일 중 본문, 제목, 발신인 등의 키워드를 입력했을 때 수 ms 이내에 정확한 결과를 찾아 반환하는 
고속 분산 텍스트 탐색 시스템이다.&lt;br /&gt;
메인 저장소인 NoSQL의 부하를 가중시키지 않기 위해 비동기 데이터 파이프라인(Kafka/CDC)을 기반으로 검색 전용 &lt;strong&gt;역인덱스(Inverted Index)&lt;/strong&gt; 클러스터를 독립적으로 
운용하는 것이 골자이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;81-이메일-검색-특성-극단적인-write-heavy&quot;&gt;8.1. 이메일 검색 특성: 극단적인 Write-Heavy&lt;/h2&gt;

&lt;p&gt;대규모 메일 서비스에서 검색 기능을 설계할 때 가장 먼저 이해해야 하는 시스템적 특성은 &lt;strong&gt;Read보다 Write 연산이 압도적으로 많이 발생&lt;/strong&gt;한다는 점이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;끊임없는 쓰기(색인)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이메일 검색 엔진은 사용자가 이메일을 발송할 때, 수신할 때, 그리고 메일을 삭제하거나 폴더를 이동할 때마다 실시간으로 인덱싱 작업을 즉각 수행해야 한다.&lt;/li&gt;
      &lt;li&gt;10억 명 규모에서는 초당 수십만 건의 색인 요청이 쏟아진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;간헐적인 읽기(검색)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;그에 반해 검색(읽기) 연산은 사용자가 ‘검색’ 버튼을 누를 때만 간헐적으로 실행된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 이메일 검색 시스템은 ‘초고속 대용량 쓰기(Index Write) 성능을 버텨내면서도 읽기 Latency를 타협하지 않는 구조’로 설계되어야 한다.&lt;/p&gt;

&lt;p&gt;구글 검색과 이메일 검색의 차이를 보자.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;구분&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;범위 (Scope)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;정렬 기준 (Sorting)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;정확도 및 실시간성 (Accuracy)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;구글 검색&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;인터넷 전체 문서 세트&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;관련성 및 문서 평판 점수 기준&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;색인에 다소 시간이 걸리므로 신규 항목이 검색 결과에 즉시 나타나지 않아도 수용됨&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;이메일 검색&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;사용자의 개인 메일함 (Tenant 격리)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;수신 시각, 첨부 파일 유무, 날짜 범위, 읽음 여부 등의 속성 기준&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;색인 작업이 거의 실시간으로 이루어져야 하며, 내가 방금 보낸 메일도 정확히 검색되어야 함&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Tenant&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;전체 시스템 중 특정 사용자가 독립적으로 사용하는 가상의 공간이자 단위&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;82-검색-기능을-지원하기-위한-두-가지-선택지&quot;&gt;8.2. 검색 기능을 지원하기 위한 두 가지 선택지&lt;/h2&gt;

&lt;p&gt;대규모 시스템에서 사서함 검색을 구현하는 방법은 크게 두 가지가 있다.&lt;br /&gt;
각 방식의 장단점과 확장성 한계에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;821-방안-1-데이터-저장소nosql-내부의-내장-검색-기능-활용&quot;&gt;8.2.1. 방안 1: 데이터 저장소(NoSQL) 내부의 내장 검색 기능 활용&lt;/h3&gt;

&lt;p&gt;일부 최신 분산 데이터베이스나 RDBMS는 자체적인 Full-Text Search(전문 검색) 인덱스 기능을 내장하고 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;아키텍처가 단순해진다.&lt;/li&gt;
      &lt;li&gt;별도의 검색 엔진 클러스터를 띄우고 운영할 필요가 없으므로 데이터 동기화 파이프라인 관리 비용이 제로에 수렴한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;검색 인덱싱 연산과 메일 송수신 핵심 연산이 동일한 서버의 CPU와 메모리 지원을 공유한다.&lt;/li&gt;
      &lt;li&gt;사용자가 대량 검색을 하거나 마케팅 메일 폭탄으로 색인 트래픽이 튀면, 메인 DB의 처리 성능(QPS)이 동반 추락한다.&lt;/li&gt;
      &lt;li&gt;페타바이트급 환경에서는 확장의 한계가 매우 명확하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;데이터베이스에-내장된-전용-검색-솔루션을-이용한다는-것은-정확히-무엇일까&quot;&gt;💡데이터베이스에 내장된 전용 검색 솔루션을 이용한다는 것은 정확히 무엇일까?&lt;/h4&gt;

&lt;p&gt;외부의 별도 검색 엔진(Elasticsearch 등)에 데이터를 복사하지 않고, &lt;strong&gt;데이터가 저장된 바로 그 메인 스토리지 엔진 내부에서 검색 인덱싱 알고리즘을 밀결합(Tight-Coupling)하여 구동하는 방식&lt;/strong&gt;을 뜻한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;실제 아키텍처 예시:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;MS Outlook의 기반이 되는 &lt;strong&gt;Exchange Server&lt;/strong&gt;가 대표적이다.&lt;/li&gt;
      &lt;li&gt;Exchange 인프라는 내부 메타데이터 스토리지 엔진과 검색 인덱싱 코어를 로컬 서버 노드 단위로 통합하여 처리한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;또 다른 예로는 Apache Cassandra DB에 내장된 &lt;strong&gt;SASI(SSTable-Attached Secondary Index)&lt;/strong&gt; 기능이 있다.
    &lt;ul&gt;
      &lt;li&gt;이는 데이터를 디스크에 쓸 때(SSTable 생성 시) 검색용 역인덱스도 한 장표에 함께 쓰는 방식이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;왜-일반-nosqlrdmbs-질의로는-검색이-불가능할까&quot;&gt;💡왜 일반 NoSQL/RDMBS 질의로는 검색이 불가능할까?&lt;/h4&gt;

&lt;p&gt;대규모 메일 서비스에서 ‘메인 DB 테이블에 인덱스를 잘 걸어서 검색하면 안될까?’ 라는 의문이 들 수 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;RDBMS의 텍스트 검색 한계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;RDBMS에서 본문 검색을 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LIKE &apos;%공지사항%&apos;&lt;/code&gt; 같은 와일드카드 질의를 던지는 순간, 인덱스를 타지 못하고 풀스캔이 발생한다.&lt;/li&gt;
      &lt;li&gt;페타바이트 단위의 데이터셋에서는 한 번의 쿼리로 서버 전체가 다운된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;NoSQL 키-밸류 구조의 한계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;앞서 본 Cassandra는 특정 파티션 키(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;user_id, folder_id&amp;gt;&lt;/code&gt;) 내부의 정렬을 보장하지만, ‘내가 보낸 메일 중 ‘계약서’라는 단어가 들어간 메일 모두 조회’ 같은 &lt;strong&gt;임의의 텍스트 다중 조건 필터링에는 태생적으로 대응이 불가&lt;/strong&gt;하다.&lt;/li&gt;
      &lt;li&gt;secondary index(보조 인덱스)를 지원하긴 하지만, 분산 노드 간의 극심한 네트워크 분산 스캔(Scatter-Gather) 현상으로 인해 실무에서는 절대 사용 금지 항목이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;위와 같은 문제를 해결하기 위해 Gmail이나 Outlook 등은 전용 분산 문서 검색 엔진을 스토리지 레이어 옆에 병렬로 배치한다.&lt;br /&gt;
오픈소스 진영에서는 Lucene 기반의 &lt;strong&gt;Elasticsearch&lt;/strong&gt; 또는 &lt;strong&gt;OpenSearch&lt;/strong&gt; 클러스터가 표준이 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;822-방안-2-독립된-전문-검색-엔진elasticsearch-도입&quot;&gt;8.2.2. 방안 2: 독립된 전문 검색 엔진(Elasticsearch) 도입&lt;/h3&gt;

&lt;p&gt;메인 저장소 옆에 전용 분산 문서 검색 엔진 클러스터(&lt;strong&gt;Elasticsearch&lt;/strong&gt; 또는 &lt;strong&gt;OpenSearch&lt;/strong&gt;)를 독립적으로 배치하는 전략이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;핵심 메일 데이터 레이어와 검색 레이어가 완벽히 분리된다.&lt;/li&gt;
      &lt;li&gt;검색 트래픽이 아무리 폭주해도 메일 송수신 코어 서버는 아무런 영향을 받지 않는다.&lt;/li&gt;
      &lt;li&gt;또한 Lucene 기반의 강력한 &lt;strong&gt;역인덱스(Inverted Index)&lt;/strong&gt; 구조를 사용하여 텍스트 토큰 탐색 시간이 복잡도 \(O(1)\) 수준으로 수렴한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메인 데이터베이스와 검색 엔진 간의 데이터를 동기화해야 하는 백엔드 파이프라인을 추가로 구축하고 유지보수해야 하는 운영 복잡도가 증가한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;83-elasticsearch-기반-분산-검색-레이어-설계&quot;&gt;8.3. Elasticsearch 기반 분산 검색 레이어 설계&lt;/h2&gt;

&lt;h3 id=&quot;831-역인덱스-아키텍처와-유저-사서함-격리&quot;&gt;8.3.1. 역인덱스 아키텍처와 유저 사서함 격리&lt;/h3&gt;

&lt;p&gt;질의가 대부분 사용자의 이메일 서버에서 실행되므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;를 파티션 키로 사용하여 같은 사용자의 이메일은 같은 노드에 묶어야 한다.&lt;/p&gt;

&lt;p&gt;Elasticsearch는 기본적으로 문서의 ID 값을 해싱하여 샤드에 분산하지만, 인덱싱 및 검색 시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;routing&lt;/code&gt; 매개변수를 명시하면 특정 기준에 따라 문서를 강제 그룹핑할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 이메일 색인 시 user_id를 라우팅 키로 지정&lt;/span&gt;
PUT /mail_index/_doc/msg_12345?routing&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;user_id_999
&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;user_id&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;user_id_999&quot;&lt;/span&gt;,
  &lt;span class=&quot;s2&quot;&gt;&quot;subject&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;프로젝트 계약서 최종본입니다.&quot;&lt;/span&gt;,
  &lt;span class=&quot;s2&quot;&gt;&quot;body&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;안녕하세요, 요청하신 계약서 내용을...&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 설정하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id_999&lt;/code&gt; 를 가진 모든 이메일 문서는 클러스터 내의 &lt;strong&gt;동일한 특정 루씬 샤드(Lucene Shard)로만 라우팅되어 저장&lt;/strong&gt;된다.&lt;br /&gt;
결과적으로 사용자가 검색을 요청할 때도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;?routing=user_id_999&lt;/code&gt; 쿼리를 함께 던지면, 검색 엔진은 클러스터 전체 노드를 다 뒤지는 낭비(Scatter-Gather)없이 
&lt;strong&gt;해당 유저의 데이터가 모여있는 단 하나의 샤드만 검색&lt;/strong&gt;하므로 엄청난 Latency 단축 효과를 얻는다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;832-kafka를-활용한-비동기-인덱싱-파이프라인&quot;&gt;8.3.2. Kafka를 활용한 비동기 인덱싱 파이프라인&lt;/h3&gt;

&lt;p&gt;메일이 수신되거나 전송될 때 검색 엔진에 데이터를 넣는 과정을 인덱싱이라고 한다.&lt;br /&gt;
이 색인 작업을 메일 송수신 API 스레드에서 &lt;strong&gt;동기&lt;/strong&gt; 방식으로 처리하면, 검색 엔진에 부하가 올 때마다 메일 발송 자체가 실패하거나 지연되는 대형 장애가 발생한다.&lt;/p&gt;

&lt;p&gt;따라서 이럴 때는 &lt;strong&gt;분산 메시지 큐(카프카) 또는 CDC(Change Data Capture)&lt;/strong&gt; 기술을 활용해 데이터 레이어를 분리하여 운영한다.&lt;/p&gt;

&lt;p&gt;사용자는 검색 버튼을 누른 후 다음 결과가 수신될 때까지 기다리므로 검색 요청은 동기 방식으로 처리되어야 한다.&lt;br /&gt;
반면, ‘이메일 전송’, ‘이메일 수신’, ‘이메일 삭제’ 같은 이벤트는 처리 결과를 클라이언트에 즉각 전달할 필요가 없으므로 백그라운드 비동기 작업으로 처리하는 것이 이상적이다.&lt;/p&gt;

&lt;p&gt;여기서는 카프카를 활용하여 ‘인덱싱 작업을 시작하는 서비스’와 ‘인덱싱을 실제로 수행할 서비스’ 사이의 결합도를 낮추는 방안을 채택하였다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/elastic.png&quot; alt=&quot;Elasticsearch&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;발송 레이턴시 제로&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메일 전송 서버는 메인 DB에 메타데이터를 저장하자마자 유저에게 ‘성공’ 응답을 보낸다.&lt;/li&gt;
      &lt;li&gt;검색 색인은 뒤에서 돌기 때문에 유저가 체감하는 속도가 극대화된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Backpressure 조절&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;블랙 프라이데이 등 마케팅 대량 메일이 폭발하여 초당 수만 건의 수신 트래픽이 몰려도 카프카가 중간에서 버퍼 역할을 한다.&lt;/li&gt;
      &lt;li&gt;인덱서 워커들은 자신이 감당할 수 있는 속도로 큐에서 데이터를 가져와 가공하므로 검색 엔진 클러스터가 과부하로 터지는 현상을 방어한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;페타바이트-급-검색-엔진에서-인덱스가-오염되거나-스키마가-되었을-때-재색인하는-부하는-어떻게-제어할까&quot;&gt;💡페타바이트 급 검색 엔진에서 인덱스가 오염되거나 스키마가 되었을 때 재색인하는 부하는 어떻게 제어할까?&lt;/h4&gt;

&lt;p&gt;데이터 규모가 페타바이트 스케일을 넘어가면 단일 검색 인덱스를 통째로 교체하는 것은 원천적으로 불가능하다.&lt;br /&gt;
이를 극복하기 위해 대형 아키텍처에서는 두 가지 묘수를 사용한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;시간 기반 인덱스 샤딩&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하나의 거대한 인덱스에 10년 치 메일을 다 넣지 않고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mail_search_2026_Q1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mail_search_2026_Q2&lt;/code&gt; 처럼 &lt;strong&gt;분기별/월별로 인덱스 자체를 쪼개서 생성&lt;/strong&gt;한다.&lt;/li&gt;
      &lt;li&gt;사용자가 검색창에 ‘최근 1달’ 조건을 걸면 시스템은 오직 해당 월의 인덱스만 뒤지므로 연산 낭비가 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Alias 기반 제로 다운타임 전환&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;라우팅 레이어에서는 실제 인덱스명을 직접 바라보지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mail_search_read&lt;/code&gt; 와 같은 Alias 포인터를 바라보게 만든다.&lt;/li&gt;
      &lt;li&gt;시스템 개편으로 재색인이 필요하면 백그라운드에서 완전히 새로운 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mail_search_v2&lt;/code&gt; 인덱스를 몰래 빌드해 둔 뒤, 완성이 끝나는 순간 포인터의 방향만 &lt;strong&gt;스위칭&lt;/strong&gt; 해주는 방식으로 무중단 마이그레이션을 달성한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;833-주-저장소nosql과-elasticsearch-간의-데이터-정합성-동기화&quot;&gt;8.3.3. 주 저장소(NoSQL)과 Elasticsearch 간의 데이터 정합성 동기화&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://db-engines.com/en/ranking/search+engine&quot;&gt;일래스틱서치는 2026.07 기준으로 가장 널리 사용되는 검색 엔진 데이터베이스&lt;/a&gt;이며, 
이메일 검색에 필요한 텍스트 기반 검색을 잘 지원한다.&lt;/p&gt;

&lt;p&gt;그러나 주 이메일 저장소와 동기화를 실시간으로 맞추는 작업은 매우 까다로운 작업이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;주-이메일-저장소와-elasticsearch-간의-실시간-동기화-맞추기&quot;&gt;💡주 이메일 저장소와 Elasticsearch 간의 실시간 동기화 맞추기&lt;/h4&gt;

&lt;p&gt;애플리케이션 레이어에서 메인 DB와 Elasticsearch에 데이터를 동시에 쓰는 ‘이중 쓰기(Dual-Write)’ 방식은 한쪽 네트워크가 다운되었을 때 정합성이 깨지므로 절대 금물이다.&lt;br /&gt;
실무에서는 &lt;strong&gt;로그 기반의 &lt;a href=&quot;https://assu10.github.io/dev/2024/08/25/kafka-data-pipeline-2/#1331-cdcchange-data-capture-%EB%B3%80%EA%B2%BD-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%BA%A1%EC%B2%98-%EC%99%80-%EB%94%94%EB%B9%84%EC%A7%80%EC%9B%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8&quot;&gt;CDC(Change Data Capture)&lt;/a&gt; 아키텍처&lt;/strong&gt;를 정석으로 삼는다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Cassandra나 MySQL 같은 메인 저장소의 &lt;strong&gt;커밋 로그(Commit Log/&lt;a href=&quot;https://assu10.github.io/dev/2026/06/12/architecture-distributed-message-queue-architecture/#412-%EC%84%A0%ED%83%9D%EC%A7%80-2-%EC%93%B0%EA%B8%B0-%EC%9A%B0%EC%84%A0-%EB%A1%9C%EA%B7%B8wal-write-ahead-log%EC%B1%84%ED%83%9D&quot;&gt;WAL&lt;/a&gt;)&lt;/strong&gt;를 실시간으로 추적하는 CDC 에이전트(예: Debezium)을 심어준다.&lt;/li&gt;
  &lt;li&gt;메인 DB에 쓰기가 성공하는 순간, CDC 에이전트가 로컬 디스크 로그에서 변경 분을 가져와서 카프카에 쓴다.&lt;/li&gt;
  &lt;li&gt;인덱스 워커가 이 로그를 기반으로 Elasticsearch에 반영한다.
    &lt;ul&gt;
      &lt;li&gt;만약 Elasticsearch가 일시적으로 다운되더라도 카프카에 데이터가 남아있으므로 복구되는 순간 재처리가 가능하여 &lt;strong&gt;최종적 일관성(Eventual Consistency)&lt;/strong&gt;를 완벽히 보장할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;역인덱스inverted-index의-기본-구조&quot;&gt;💡역인덱스(Inverted Index)의 기본 구조&lt;/h4&gt;

&lt;p&gt;검색 엔진은 메일 본문 텍스트를 Tokenizer를 통해 단어 단위로 쪼갠 후, 각 단어가 ‘어느 사용자의 몇 번 메일(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;message_id&lt;/code&gt;)에 위치하는지 역으로 지도를 그려 저장한다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;추출된 단어 (Term)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;해당 단어가 포함된 이메일 식별자 목록 (Posting List)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;아키텍처&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;msg_101, msg_204, msg_509&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;도서&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;msg_101, msg_302&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;디자인&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;msg_204, msg_401, msg_509&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;사용자가 ‘아키텍처 디자인’ 이라고 검색하면, 시스템은 메일 전수를 조회하는 것이 아니라 위 테이블에서 두 단어가 동시에 교집합으로 걸린 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;msg_204&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;msg_509&lt;/code&gt;만 조회하여 
반환하므로 탐색 속도가 \(O(1)\) 에 수렴하게 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;84-맞춤형-검색-엔진과-디스크-io-병목-해결&quot;&gt;8.4. 맞춤형 검색 엔진과 디스크 I/O 병목 해결&lt;/h2&gt;

&lt;p&gt;대규모 이메일 서비스 사업자는 보통 자기 제품의 고유한 요구사항을 만족시키기 위해 검색 엔진을 자체적으로 개발하여 사용한다.&lt;br /&gt;
자체 솔루션을 구현할 때 마주하게 되는 치명적인 장벽은 바로 &lt;strong&gt;디스크 I/O 병목&lt;/strong&gt; 문제 이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;841-lsm-트리-도입&quot;&gt;8.4.1. LSM 트리 도입&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;#13-개략적인-규모-추정&quot;&gt;1.3. 개략적인 규모 추정&lt;/a&gt;에서 보았듯이 메일 저장소에 추가되는 메타데이터와 첨부 파일의 양은 페타바이트 수준이다.&lt;br /&gt;
인덱싱 프로세스는 다량의 쓰기 연산을 발생시킬 수밖에 없으므로, &lt;a href=&quot;https://assu10.github.io/dev/2026/06/05/architecture-nearby/#242-%EC%9C%84%EC%B9%98-%EC%9D%B4%EB%8F%99-%EC%9D%B4%EB%A0%A5-dbcassandra&quot;&gt;LSM(Log-Structured Merge) 트리&lt;/a&gt;를 
사용하여 디스크에 저장하는 색인을 구조화하는 것이 바람직하다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/lsm.png&quot; alt=&quot;LSM 트리&quot; /&gt;&lt;/p&gt;

&lt;p&gt;LSM 트리는 빅테이블이나 Cassandra, RocksDB 같은 대용량 데이터베이스의 핵심 자료 구조이다.&lt;br /&gt;
새로운 이메일이 도착하면 우선 메모리 캐시로 구현되는 0번 계층(MemTable)에 추가된다.&lt;br /&gt;
메모리에 보관된 데이터의 양이 사전에 정의된 임계치를 넘으면 데이터는 디스크의 다음 계층(SSTable)에 순차적으로 병합(Compaction)된다.&lt;/p&gt;

&lt;p&gt;전통적인 RDBMS(B+ Tree 구조)는 데이터가 들어오면 디스크의 이리저리 흩어진 주소를 찾아가서 수정하는 &lt;strong&gt;무작위 쓰기(Random Write)&lt;/strong&gt;를 수행한다.&lt;br /&gt;
이는 디스크 헤드가 물리적으로 움직여야 하거나(HDD), SSD의 플래시 메모리 페이지를 지우고 다시 써야 하므로 극심한 I/O 병목을 유발한다.&lt;/p&gt;

&lt;p&gt;반면, LSM 트리의 순차적 쓰기(Sequential Write)는 디스크 주소를 찾아 헤매지 않는다.&lt;br /&gt;
들어오는 모든 새로운 데이터와 수정 정보를 메모리(MemTable)에 모았다가, 디스크에 내릴 때는 &lt;strong&gt;기존 파일 뒤에 연속되도록 통째로 이어붙이는 Append-Only 방식&lt;/strong&gt;을 사용한다.&lt;br /&gt;
디스크 포인터가 일직선으로 쭉 나아가며 쓰기 때문에, 무작위 쓰기에 비해 물리적 연산 속도가 수백 배 이상 빠르며, 페타바이트급 쓰기 트래픽을 중단 없이 받아낼 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;842-변하지-않는-이메일과-자주-변하는-폴더-정보의-분리-메커니즘&quot;&gt;8.4.2. 변하지 않는 이메일과 자주 변하는 폴더 정보의 분리 메커니즘&lt;/h3&gt;

&lt;p&gt;LSM을 사용하는 또 다른 중요한 이유는 자주 바뀌는 데이터를 그렇지 않은 데이터와 분리하기 위해서이다.&lt;br /&gt;
예를 들어 이메일 본문 데이터는 한 번 수신되면 절대 바뀌지 않지만, 이메일 폴더 정보나 읽음 상태(Label)는 상이한 필터링 규칙과 유저의 행동 때문에 굉장히 자주 바뀐다.&lt;/p&gt;

&lt;p&gt;따라서 데이터를 두 개의 파트로 나누고, 어떤 요청이 폴더 변경에 관한 것이면 폴더 정보만 바꾸고 이메일 데이터는 내버려 두는 전략이 필요하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;lsm-트리-구조-안에서-데이터를-어떻게-두-개의-파트로-쪼개는-걸까&quot;&gt;💡LSM 트리 구조 안에서 데이터를 어떻게 두 개의 파트로 쪼개는 걸까?&lt;/h4&gt;

&lt;p&gt;메일 텍스트 전체를 들고 있는 &lt;strong&gt;‘불변의 본문 인덱스’&lt;/strong&gt;와, 폴더 ID 및 상태값만 관리하는 &lt;strong&gt;‘가변의 메타데이터 인덱스’&lt;/strong&gt;를 물리적으로 분리하여 LSM 테이블을 이원화한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;사용자가 이메일을 ‘받은 편지함’에서 ‘보관함’으로 이동시키면, GB 단위의 무거운 본문 역인덱스를 다시 생성하는 것이 아니다.&lt;/li&gt;
  &lt;li&gt;본문 테이블은 그대로 두고, 가벼운 폴더 상태 테이블에 &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[Message_A] Folder: Archive&lt;/code&gt; 라는 변경 이력 사본을 LSM 트리의 Sequential Write 방식으로 저장&lt;/strong&gt;한다.&lt;/li&gt;
  &lt;li&gt;이후 검색 엔진이 질의를 할 때는 두 파트의 결과를 레이어 스택처럼 Merge해서 최종 상태를 유저에게 보여주기 때문에 본문 재색인으로 인한 디스크 I/O 폭발을 막을 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;85-elasticsearch-vs-맞춤형-검색-엔진&quot;&gt;8.5. Elasticsearch vs 맞춤형 검색 엔진&lt;/h2&gt;

&lt;p&gt;두 가지 설계 방안은 시스템의 스케일과 엔지니어링 리소스에 따라 명확한 트레이드오프 관계를 가진다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;비교 항목&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;일래스틱서치 (Elasticsearch)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;맞춤형 검색 엔진 (Custom Engine)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;규모 확장성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;어느 정도까지는 무난히 확장 가능하나 극단적 대용량 시 클러스터 관리 한계 직면&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;이메일 특유의 읽기/쓰기 사용 패턴에 맞춤형 최적화(LSM 튜닝 등)가 가능하므로 극단적 확장 용이&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;시스템 복잡도&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;두 가지 상이한 대형 시스템(메인 NoSQL 저장소 + 일래스틱서치 클러스터)을 동시에 운영해야 함&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;메인 저장소 아키텍처와 통합된 하나의 거대한 인프라 시스템으로 관리 가능&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;데이터 일관성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;메타데이터 저장소와 일래스틱서치 내에 동일 데이터의 사본이 각각 존재하므로 동기화 및 일관성 유지가 매우 까다로움&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;메타데이터 저장소 체계 내에 단 하나의 정합성 사본만 유지되도록 설계 가능&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;데이터 손실 가능성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;없음. 색인이 깨지거나 손상되더라도 주 저장소(NoSQL)의 원본 데이터를 역추적해 100% 복구 가능&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;없음.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;개발 및 유지비용&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;오픈소스를 활용하므로 초기 통합은 쉬우나, 10억 유저 스케일 시 ES 내부 인프라만 전담하는 전문 엔지니어링 팀이 필수적임&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;개발 난이도가 상상을 초과하며, 프로덕션 레벨의 커스텀 엔진 구축을 위해 엄청난 수의 엔지니어링 리소스 투입 필요&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;소규모 및 대다수의 이메일 시스템은 Elasticsearch가 좋은 선택지이다. 시스템을 빠르게 통합할 수 있고 인프라 초기 엔지니어링 비용을 획기적으로 아낄 수 있다.&lt;/p&gt;

&lt;p&gt;Gmail 규모의 글로벌 초대형 시스템은 독립적인 외부 검색 클러스터를 두기보다는, 디스크 I/O 효율을 극대화하기 위해 앞서 설명한 &lt;strong&gt;데이터베이스 엔진 내부에 내장된 전용 맞춤형 검색 솔루션이나 통합 스토리지 인프라를 직접 설계하여 사용&lt;/strong&gt;하는 것이 
장기적인 인프라 비용과 정합성 측면에서 바람직하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;86-요약&quot;&gt;8.6. 요약&lt;/h2&gt;

&lt;p&gt;10억 사용자를 위한 이메일 검색 엔진 설계의 본질은 &lt;strong&gt;극단적인 쓰기 중심 환경에서 어떻게 디스크 I/O 병목을 지우고 테넌트 간 격리를 달성하느냐&lt;/strong&gt;에 있다.&lt;/p&gt;

&lt;p&gt;초기 빌드와 빠른 기능 제공이 목표라면 &lt;strong&gt;Elasticsearch에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;routing=user_id&lt;/code&gt; 설정을 얹고 카프카 CDC 파이프라인으로 메인 NoSQL과 동기화하는 구조&lt;/strong&gt;가 바람직하다.&lt;br /&gt;
그러나 데이터가 수백 페타바이트를 넘어서는 스케일로 전진할수록, &lt;strong&gt;LSM 트리의 순차적 쓰기 메커니즘을 응용하여 불변의 본문 데이터와 가변의 상태 데이터를 분리 제어하는 맞춤형 내장 검색 아키텍처&lt;/strong&gt;로 전환하는 것이 
시스템의 생존을 보존하는 궁극의 전략이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;9-가용성-향상&quot;&gt;9. 가용성 향상&lt;/h1&gt;

&lt;p&gt;대규모 이메일 시스템의 고가용성이란 특정 데이터 센터의 마비, 물리 노드의 서버 다운 등의 인프라 장애 상황에서도 유저의 사서함 접근성과 메일 송수신 기능을 
무중단으로 유지하는 것이다.&lt;br /&gt;
데이터 정합성을 양보할 수 없는 이메일의 특성 상, 강한 일관성을 유지하면서도 신속한 장애 복구(Failover)를 달성하는 결함 방지 및 복제 아키텍처가 핵심이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/datacenter.png&quot; alt=&quot;여러 데이터센터를 통한 시스템 다중화&quot; /&gt;&lt;/p&gt;

&lt;p&gt;데이터를 여러 노드와 데이터 센터가 복제하는 것은 가용성의 기본이지만, 이메일 시스템은 단순 소셜 미디어 피드와 달리 &lt;strong&gt;궁극적 일관성을 적용할 수 없는 영역&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;91-단일-마스터single-primary-복제-모델을-채택하는-이유&quot;&gt;9.1. 단일 마스터(Single-Primary) 복제 모델을 채택하는 이유&lt;/h2&gt;

&lt;p&gt;이메일 사서함은 유저가 메일을 읽음 처리하거나, 폴더를 이동하는 등의 행위가 IMAP 프로토콜의 상태 플래그 및 폴더 구조와 밀접하게 맞물려 돌아간다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;다중 마스터(Multi-Primary)의 위험성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;만일 유저가 사서함 쓰기 요청을 여러 노드에서 동시에 받아들이면, 네트워크 분할 시 동일 메일에 대해 서로 다른 수정 사항이 발생하는 Merge Conflict나 &lt;strong&gt;Split-Brain&lt;/strong&gt; 현상이 발생하는데 이를 복구하는 비용은 상상을 초월한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단일 마스터 모델의 해법&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;따라서 특정 유저의 메일박스는 분산 클러스터 내에서 &lt;strong&gt;오직 단 하나의 주 노드(Primary Node)만 쓰기 연산을 전담&lt;/strong&gt;하도록 강제한다.&lt;/li&gt;
      &lt;li&gt;나머지 노드들은 Replica로서 주 노드의 변경 이력(Write-Ahead Log)을 실시간으로 동기화하는 역할만 수행한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;92-cap-정리-관점에서의-트레이드오프&quot;&gt;9.2. CAP 정리 관점에서의 트레이드오프&lt;/h2&gt;

&lt;p&gt;CAP 정리(Consistency, Availability, Partition Tolerance)에서, 본 이메일 시스템 아키텍처는 네트워크 분할(P) 상황이 발생했을 때 
&lt;strong&gt;가용성(A)보다 일관성(C)를 우선하는 선택&lt;/strong&gt;을 한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;네트워크 분할 장애 발생]
                             │
            ┌────────────────┴────────────────┐
            ▼                                 ▼
   &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;일관성&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;C&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 선택 - 본 시스템]        &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;가용성&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;A&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 선택 - 일반 SNS]
   - 문제 노드의 쓰기 일시 차단        - 우선 양쪽 노드 다 쓰기 허용
   - 정합성 오염 원천 차단            - 나중에 영구적인 데이터 유실/
   - 신속한 Failover로 가용성 복구      충돌 발생 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;메일 유실 위험&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;주 노드가 다운되었을 때 대기 노드로 마스터 권한을 넘기는 순간(Failover Phase)에는 해당 유저의 사서함 쓰기가 아주 잠깐 제한될 수 있다.&lt;br /&gt;
하지만 이는 이메일이 중복 발송되거나 읽음 상태가 꼬이는 비즈니스적 오류를 막기 위한 필수적인 아키텍처적 트레이드오프이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;split-brain-이란&quot;&gt;💡Split-Brain 이란?&lt;/h3&gt;

&lt;p&gt;분산 시스템 내에서 네트워크 단절(Partition)로 인해 클러스터가 두 개 이상의 독립된 그룹으로 쪼개졌을 때, 
&lt;strong&gt;각 그룹이 서로의 생존을 확인하지 못해 스스로를 ‘유일한 리더(Primary)’로 오인하여 동시에 쓰기 연산을 수행하는 장애 상태&lt;/strong&gt;를 의미한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;정상 상태] Node A &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Primary&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &amp;lt;─── 정상 네트워크 연결 ───&amp;gt; Node B &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Replica&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;장애 발생 - 네트워크 단절!]
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;좌측 그룹] Node A : &lt;span class=&quot;s2&quot;&gt;&quot;B 노드와 연락이 안 되네? 내가 계속 Primary 역할을 해야지.&quot;&lt;/span&gt; -&amp;gt; 쓰기 계속 허용
             &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;뚝!&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; ⚡ 네트워크 분할 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Network Partition&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; ⚡ &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;뚝!&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;우측 그룹] Node B : &lt;span class=&quot;s2&quot;&gt;&quot;Primary였던 A 노드가 죽었나 봐! 내가 새로운 Primary가 되어야겠다!&quot;&lt;/span&gt; -&amp;gt; 스스로 리더 승격 및 쓰기 허용
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이메일 아키텍처에서 Split-Brain이 터지면 아래와 같은 일이 발생한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;메일 상태의 유령화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저가 Node A에 접근해서 ‘A 이메일 삭제’를 눌렀는데, 동시에 다른 메일 송신자가 보낸 메일은 Node B에 적재된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;네트워크 복구 시의 데이터 충돌&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;끊어졌던 네트워크가 다시 연결되었을 때, 두 노드를 하나로 합쳐야 한다.&lt;/li&gt;
      &lt;li&gt;하지만 한쪽은 메일이 삭제되었다고 하고, 다른 한쪽은 메일이 살아있다고 하기 때문에 무엇이 진실인지 알 수 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;결과&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저 입장에서는 분명히 지웠던 메일이 다시 살아나거나, 장애 시간 동안 수신된 중요 메일이 유실되는 최악의 사용자 경험을 겪게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이런 문제를 해결하기 위해 분산 시스템에서는 아래와 같은 방식을 사용한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;과반수 투표 체계(Quorum 기반 합의 알고리즘)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Raft 같은 분산 합의 알고리즘의 핵심 규칙은  “전체 노드의 과반수(\(N/2 + 1\)) 이상이 동의하고 살아있는 그룹만 리더를 선출하거나 쓰기를 처리할 수 있다”는 것이다.&lt;/li&gt;
      &lt;li&gt;예를 들어 노드가 5대인 클러스터가 네트워크 단절로 2대와 3대로 쪼개지면, 2대짜리 그룹은 과반수(3대)를 채우지 못하므로 스스로 리더가 되는 것을 포기하고 쓰기는 차단한다.&lt;/li&gt;
      &lt;li&gt;오직 3대까리 그룹만 정상 가동되므로 Split-Brain이 원천 봉쇄된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;코디네이션 서비스(ZooKeeper/etcd) 활용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;중앙 분산 Lock 시스템을 사용하여, 클러스터 전체에서 ‘Primary 리더’라는 타이틀을 단 한 개만 발행하고 관리하도록 통제한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;10-추가-고려사항&quot;&gt;10. 추가 고려사항&lt;/h1&gt;

&lt;h2 id=&quot;101-결함-감지-및-무중단-장애-복구failover&quot;&gt;10.1. 결함 감지 및 무중단 장애 복구(Failover)&lt;/h2&gt;

&lt;p&gt;10억 유저 스케일의 인프라에서는 매일 수십, 수백 대의 서버가 노후화나 하드웨어 결함으로 다운된다.&lt;br /&gt;
중앙 집중식 하트비트 체크는 그 자체로 병목이 되므로, 대규모 시스템은 &lt;strong&gt;탈중앙화된 결함 감지 체계&lt;/strong&gt;를 가진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;1011-gossip-protocol을-통한-상태-관리&quot;&gt;10.1.1. Gossip Protocol을 통한 상태 관리&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Gossip Protocol&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;분산 시스템에서 중앙 관리자없이, 각 노드가 무작위로 선택한 다른 노드들과 주기적으로 정보를 교환하며 클러스터 전체의 상태(노드 활성화 여부 등)를 동기화하는 
&lt;strong&gt;탈중앙화 피어 투 피어 통신 규격&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;각 노드가 무작위로 다른 노드들과 주기적으로 메타데이터를 교환하는 &lt;strong&gt;고십 프로토콜&lt;/strong&gt;을 활용한다.&lt;br /&gt;
특정 노드가 응답하지 않는다는 사실이 네트워크 전체로 소문 퍼지듯 빠르게 확산되며, 이를 통해 중앙 제어 장치 없이도 클러스터 전체가 노드의 생존 상태를 정확하게 인지하게 된다.&lt;/p&gt;

&lt;p&gt;주 노드의 장애가 감지되는 순간부터 클라이언트가 연결을 회복할 때까지 분산 복구 수순을 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Step 1. 장애 감지] ➔ &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Step 2. 리더 승격 수행] ➔ &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Step 3. 라우팅 갱신] ➔ &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Step 4. 커넥션 재연결]
  Gossip 프로토콜로       ZooKeeper/etcd가        라우팅 프록시가       웹소켓 클라이언트가
  Primary 다운 확인       Replica를 새 주노드로    라우팅 장부&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;맵&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;를    새 주노드로 튜브 재연결
                          즉각 승격&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Promote&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;       최신 상태로 업데이트   &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;유저는 장애 체감 못함&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;102-글로벌-인프라를-위한-보안-compliance-및-데이터-격리&quot;&gt;10.2. 글로벌 인프라를 위한 보안, Compliance 및 데이터 격리&lt;/h2&gt;

&lt;p&gt;글로벌 서비스를 지향하는 메일 시스템은 단순히 기능의 확장성을 넘어, 국가별 법률적 규제인 컴플라이언스를 반드시 아키텍처에 녹여내야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;1021-gdpr-및-데이터-주권data-sovereignty-대응&quot;&gt;10.2.1. GDPR 및 데이터 주권(Data Sovereignty) 대응&lt;/h3&gt;

&lt;p&gt;유럽 연합의 &lt;a href=&quot;https://en.wikipedia.org/wiki/General_Data_Protection_Regulation&quot;&gt;&lt;strong&gt;GDPR&lt;/strong&gt;&lt;/a&gt; 등 현대 보안 Compliance의 핵심은 ‘유럽 시민의 개인 데이터는 물리적으로 유럽 영토 내에 위치한 데이터 센터에만 저장되어야 한다’는 &lt;strong&gt;데이터 주권&lt;/strong&gt;이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;이를 해결하기 위해 분산 NoSQL 데이터 모델링 단계에서 유저의 국가 정보를 기반으로 물리적 클러스터를 논리적으로 완전히 분할(Geo-graphical Sharding)해야 한다.&lt;/li&gt;
  &lt;li&gt;유럽 유저의 메일 데이터가 미국이나 아시아의 스토리지 노드로 절대 복제되지 않도록 Replication Placement Policy를 엄격히 통제한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Lawful_interception&quot;&gt;합법적 감청(Legal Intercept)&lt;/a&gt;은 이 분야의 또 다른 대표적 특징이다.&lt;br /&gt;
설계 단계부터 감청 통로를 만들어, 법적 절차를 거친 국가 기관이 수사를 위해 통신 내용을 중간에서 열람할 수 있도록, 통신 시스템 자체에 합법적인 데이터 추출 통로를 마련해 두는 기술적·법적 체계이다.
당연히 감청이 실행되는 동안에도 사용자는 이 사실을 전혀 몰라야 하고, 권한이 없는 일반 직원이 오용하지 못하도록 극도로 엄격한 보안 및 로그 시스템이 적용되어야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;1022-이중-암호화-표준-도입&quot;&gt;10.2.2. 이중 암호화 표준 도입&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;전송 중 데이터 암호화(Data-in-Transit)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;외부 SMTP 통신 및 클라이언트와 API 서버 간의 모든 통신은 &lt;strong&gt;TLS 1.3&lt;/strong&gt; 프로토콜을 강제하여 중간자 공격(MITM, Man-in-the-Middle)을 원천 차단한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;저장 중 데이터 암호화(Data-at-Rest)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;페타바이트급 데이터 레이어에 저장되는 메일 본문과 첨부 파일은 &lt;strong&gt;AES-256&lt;/strong&gt; 알고리즘으로 암호화된다.&lt;/li&gt;
      &lt;li&gt;키 관리 서비스(KMS)를 연동하여 유저별 마스터 키를 기반으로 데이터 복호화 키를 따로 생성하는 Envelope Encryption 메커니즘을 적용한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;103-모니터링-및-메트릭-핵심-지표&quot;&gt;10.3. 모니터링 및 메트릭 핵심 지표&lt;/h2&gt;

&lt;p&gt;시스템의 가동 상태를 선제적으로 모니터링하지 않으면 대규모 장애의 파고를 막을 수 없다.&lt;br /&gt;
인프라의 가동률 외의 메일 시스템만의 특화 지표를 관리해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;1031-종단-간-지연e2e-delivery-latency과-backpressure-관측&quot;&gt;10.3.1. 종단 간 지연(E2E Delivery Latency)과 Backpressure 관측&lt;/h3&gt;

&lt;p&gt;메일이 인바운드 SMTP 서버에 도착한 시점부터 유저의 사서함 NoSQL에 적재되고 푸시 알림이 가기까지의 &lt;strong&gt;종단 간 지연 시간&lt;/strong&gt;을 추적한다.&lt;br /&gt;
만약 중간 메시지 큐(카프카)의 Consumer Group에서 컨슈밍 지연(Lag)이 발생하면 Backpressure 점검 경보를 띄우고 워커 노드를 Auto-Scaling 해야 한다.&lt;/p&gt;

&lt;p&gt;IP 평판 하락과 메일 유실을 실시간으로 방어하기 위해 전송 가능성과 직결되는 수신측 ISP들의 거부 반응을 실시간 모니터링 대시보드(Prometheus/Grafana)의 &lt;strong&gt;최우선 알람 지표&lt;/strong&gt;로 설정해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;하드 바운스 비율(Hard Bounce Rate)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;영구적인 수신 실패 오류(5xx 계열 코드) 비율이다.&lt;/li&gt;
      &lt;li&gt;이 지표가 &lt;strong&gt;1%를 초과&lt;/strong&gt;하면 스팸 발송자로 간주되어 전체 IP 대역이 블록당할 수 있으므로, 감지 즉시 해당 발송 유저를 격리 조치하는 자동 차단 스크립트가 돌아야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;스팸 신고율&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저들이 메일을 받고 ‘스팸 신고’를 누른 비율이다.&lt;/li&gt;
      &lt;li&gt;이 지표를 &lt;strong&gt;0.1% 미만으로 유지&lt;/strong&gt;하는 것이 철칙이다.&lt;/li&gt;
      &lt;li&gt;조금이라도 튀는 징후가 보이면 해당 발송 세그먼트의 Throttling Rate를 즉각 조사해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;RBL(Real-time Blackhole List) 등재 여부&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유명 스팸 블랙리스트 기관(Spamhaus 등)에 우리 시스템의 발송 전용 IP 대역이 포함되었는지 실시간 파싱 봇을 통해 주기적으로 감시한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;11-요약&quot;&gt;11. 요약&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;프로토콜 &amp;amp; 전송 가능성(Deliverability) 레이어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;외부와의 통신은 SMTP로 표준을 준수하되, SPF/DKIM/DMARC 인증과 트랜잭션/마케팅 IP 범주화 격리 및 철저한 4주 간의 IP 웜업을 적용하여 스팸 메일함으로 빠지는 병목 제거&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;스토리지 레이어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;구조화된 메일 데이터는 카산드라(NoSQL)의 LSM 트리 순차 쓰기 특성을 활용하여 초고속 쓰기 성능을 확보하고, 대용량 첨부 파일은 저비용의 Object Storage로 분리하여 비용과 성능 최적화&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;검색 레이어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;극단적인 쓰기 중심 검색 요구사항을 풀기 위해, 메인 DB의 자원을 갉아먹는 내장 쿼리 대신 카프카 CDC 기반의 비동기 파이프라인과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;routing=user_id&lt;/code&gt; 를 적용한 독립 Elasticsearch 클러스터를 연동하여 준실시간성과 테넌트 격리 달성&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;가용성 &amp;amp; 보안 운영&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사서함의 완벽한 정합성을 위해 단일 마스터 복제 모델과 CAP 정리의 일관성(C) 우선 정책을 고수하며, Gossip Protocol 기반의 무중단 Failover 체계와 GDPR 맞춤형 지리적 파티셔닝을 더해 거대한 글로벌 메일 서비스 지탱&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0704/mindmap.png&quot; alt=&quot;마인드 맵&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬, 산 람 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000211656186&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links_vol2.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://resources.review42.com/how-many-emails-are-sent-per-day/&quot;&gt;How Many Emails Are Sent Per Day? (2026 Statistics)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/ActiveSync&quot;&gt;ActiveSync&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Email_attachment&quot;&gt;Email attachment&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/MIME&quot;&gt;MIME&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Thread_(online_communication)&quot;&gt;Threading&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://cwiki.apache.org/confluence/spaces/CASSANDRA2/pages/120732468/CassandraLimitations&quot;&gt;CassandraLimitations&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Inverted_index&quot;&gt;Inverted Index&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Exponential_backoff&quot;&gt;지수적 백오프(Exponential backoff)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/IOPS&quot;&gt;IOPS&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.datastax.com/en/cql-oss/3.3/cql/cql_reference/uuid_type_r.html&quot;&gt;UUID and timeuuid types&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.jwz.org/doc/threading.html&quot;&gt;message threading.&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.statista.com/statistics/420391/spam-email-traffic-share/&quot;&gt;Monthly share of spam in the total e-mail traffic worldwide from January 2014 to December 2025&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/ses/latest/dg/dedicated-ip-warming.html&quot;&gt;Documentation Amazon Simple Email Service Developer Guide Documentation Amazon Simple Email Service Developer Guide Warming up dedicated IP addresses (standard)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://db-engines.com/en/ranking/search+engine&quot;&gt;DB-Engines Ranking of Search Engines&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/General_Data_Protection_Regulation&quot;&gt;General Data Protection Regulation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Lawful_interception&quot;&gt;Lawful interception&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://safety.google/intl/en_us/products/gmail/&quot;&gt;Email that Keeps your private information safe&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 04 Jul 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/07/04/architecture-distributed-email-service/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/07/04/architecture-distributed-email-service/</guid>
        
        <category>architecture</category>
        
        <category>대규모아키텍처</category>
        
        <category>분산시스템</category>
        
        <category>이메일서버</category>
        
        <category>시스템디자인</category>
        
        <category>분산데이터베이스</category>
        
        <category>대용량트래픽</category>
        
        <category>스토리지규모추정</category>
        
        <category>스팸필터링</category>
        
        <category>메타데이터</category>
        
        <category>모델링</category>
        
        <category>비정규화</category>
        
        <category>일래스틱서치</category>
        
        <category>LSM트리</category>
        
        <category>웹소켓</category>
        
        <category>피드백루프</category>
        
        <category>system-design</category>
        
        <category>distributed-system</category>
        
        <category>email-server</category>
        
        <category>capacity-estimation-qps</category>
        
        <category>smtp</category>
        
        <category>imap</category>
        
        <category>pop3</category>
        
        <category>nosql-modeling</category>
        
        <category>lsm-tree</category>
        
        <category>elasticsearch</category>
        
        <category>websocket</category>
        
        <category>deliverability</category>
        
        <category>jwz-algorithm</category>
        
        <category>gdpr</category>
        
        <category>lawful-interception</category>
        
        <category>split-brain</category>
        
        <category>gossip-protocol</category>
        
        <category>compliance</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(1) - 안정 해시(Consistent Hashing)</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-해시-키-재배치rehash-문제&quot;&gt;1. 해시 키 재배치(rehash) 문제&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-서버-개수가-변할-때-발생하는-대규모-캐시-미스&quot;&gt;1.1. 서버 개수가 변할 때 발생하는 대규모 캐시 미스&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-안정-해시consistent-hash&quot;&gt;2. 안정 해시(Consistent hash)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-해시-공간과-해시-링&quot;&gt;2.1. 해시 공간과 해시 링&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-해시-링-위에-서버와-키-배치하기&quot;&gt;2.2. 해시 링 위에 서버와 키 배치하기&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-데이터가-저장될-서버-조회서버-조회-알고리즘&quot;&gt;2.3. 데이터가 저장될 서버 조회(서버 조회 알고리즘)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#24-유연한-확장과-장애-대응-서버-추가-및-제거-시의-동작&quot;&gt;2.4. 유연한 확장과 장애 대응: 서버 추가 및 제거 시의 동작&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-기본-안정-해시의-한계와-해결책&quot;&gt;3. 기본 안정 해시의 한계와 해결책&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-파티션-크기-불균형과-데이터-쏠림-현상&quot;&gt;3.1. 파티션 크기 불균형과 데이터 쏠림 현상&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-가상-노드virtual-node&quot;&gt;3.2. 가상 노드(Virtual Node)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-노드-변동-시-재배치되는-키의-범위-결정-방식&quot;&gt;3.3. 노드 변동 시 재배치되는 키의 범위 결정 방식&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-정리하며&quot;&gt;4. 정리하며..&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#데이터가-균등하게-분포하는-것과-수평적-규모-확장은-무슨-관계일까&quot;&gt;💡데이터가 균등하게 분포하는 것과 수평적 규모 확장은 무슨 관계일까?&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#실제-상용-시스템의-안정-해시-활용-사례&quot;&gt;실제 상용 시스템의 안정 해시 활용 사례&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;대규모 시스템을 설계할 때 가장 중요한 과제 중 하나는 폭발적으로 늘어나는 트래픽을 감당하기 위한 Scale-out 이다.&lt;br /&gt;
이를 성공적으로 달성하려면 수많은 요청이나 데이터를 여러 대의 서버에 고르게 분산시키는 기술이 필수적이다.&lt;/p&gt;

&lt;p&gt;데이터가 한쪽 서버에만 몰리게 된다면 확장의 의미가 퇴색된다.&lt;br /&gt;
이 때 데이터의 균등한 분산을 돕고, 서버의 유연한 확장을 가능하게 하는 기술이 바로 안정 해시(Consistent Hashing)이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-해시-키-재배치rehash-문제&quot;&gt;1. 해시 키 재배치(rehash) 문제&lt;/h1&gt;

&lt;p&gt;안정 해시를 이해하기 위해 먼저 ‘전통적인 해시 방식이 왜 대규모 시스템에 적합하지 않은지’를 알아야 한다.&lt;/p&gt;

&lt;p&gt;N개의 캐시 서버가 있다고 가정해보자.&lt;br /&gt;
이 서버들에 부하를 균등하게 나누기 위해 보편적으로 사용하는 방법은 아래와 같이 나머지(Modulo) 연산을 활용한 해시 함수를 사용하는 것이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;기본 해시 분산 공식&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;serverIndex = hash(key) % N&lt;/code&gt; (N은 서버 개수)&lt;/p&gt;

&lt;p&gt;총 4대의 서버(N=4)를 사용한다고 가정하고, 주어진 각각의 key에 대해 해시 값과 저장될 서버 인덱스를 계산하면 아래와 같은 결과가 나온다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;키&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;해시(Hash)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;해시 % 4 (서버 인덱스)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key0&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;18358617&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key1&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;26143584&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key2&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;18131146&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key3&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;35863496&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key4&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;34085809&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key5&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;27581703&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key6&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;38164978&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key7&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;22530351&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;예를 들어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash(key0) % 4 = 1&lt;/code&gt; 이므로 클라이언트는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;key0&lt;/code&gt;에 해당하는 데이터를 가져오기 위해 1번 서버에 접속하게 된다.&lt;br /&gt;
이 규칙에 따라 4대의 서버에 키 값이 분산되는 모습을 그리면 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/51.png&quot; alt=&quot;해시 키 배치&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-서버-개수가-변할-때-발생하는-대규모-캐시-미스&quot;&gt;1.1. 서버 개수가 변할 때 발생하는 대규모 캐시 미스&lt;/h2&gt;

&lt;p&gt;이 방식은 &lt;strong&gt;서버 풀(Pool)의 크기가 고정되어 있고 데이터 분포가 균등할 때만&lt;/strong&gt; 완벽하게 동작한다.&lt;br /&gt;
하지만 대규모 시스템에서는 트래픽 폭주로 서버를 증설하거나, 반대로 장애가 발생해 서버가 다운되는 일이 빈번하다.&lt;/p&gt;

&lt;p&gt;예를 들어 1번 서버가 장애를 일으켜 다운되었다고 가정해보자.&lt;br /&gt;
이제 활성화된 전체 서버의 개수(N)은 4에서 3으로 변한다.&lt;/p&gt;

&lt;p&gt;키에 대한 고유한 해시값 자체는 변하지 않지만, &lt;strong&gt;나머지 연산(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%3&lt;/code&gt;)을 적용하여 계산된 서버 인덱스 값은 완전히 달라지게 된다.&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;키&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;해시&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;해시 % 3 (서버 인덱스)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key0&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;18358617&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key1&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;26143584&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key2&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;18131146&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key3&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;35863496&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key4&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;34085809&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key5&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;27581703&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key6&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;38164978&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key7&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;22530351&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;아래 그림은 활성 서버가 3대로 줄었을 때 새롭게 변화된 키의 분포이다.&lt;/p&gt;

&lt;p&gt;결과적으로 장애가 난 1번 서버에 보관되어 있던 키뿐만 아니라, 거의 모든 키의 위치가 재배치(Rehash)되어버린다.&lt;br /&gt;
클라이언트는 엉뚱한 서버로 데이터를 요청하게 되고, 데이터가 없으니 대규모 캐시 미스가 발생한다.&lt;br /&gt;
이는 결국 원본 데이터베이스에 엄청난 읽기 부하를 일으켜 시스템 전체의 마비로 이어질 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/52.png&quot; alt=&quot;해시 키 재배치&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-안정-해시consistent-hash&quot;&gt;2. 안정 해시(Consistent hash)&lt;/h1&gt;

&lt;p&gt;앞서 살펴본 해시 키 재배치 문제를 근본적으로 해결하기 위해 등장한 기술이 바로 안정 해시이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Consistent_hashing&quot;&gt;안정 해시&lt;/a&gt;란?&lt;/strong&gt;&lt;br /&gt;
해시 테이블의 크기가 조정(서버 추가 또는 삭제)될 때, 전체 키를 재배치하는 것이 아니라 &lt;strong&gt;평균적으로 오직 \(k/n\)개(k: 전체 키의 개수, n: 서버의 개수)의 키만 재배치&lt;/strong&gt;하는 효율적인 해시 기술이다.&lt;br /&gt;
대부분의 키는 원래의 위치를 유지하므로 대규모 캐시 미스 사태를 방지할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-해시-공간과-해시-링&quot;&gt;2.1. 해시 공간과 해시 링&lt;/h2&gt;

&lt;p&gt;안정 해시의 동작 원리를 이해하기 위해, 해시 공간(Hash Space)을 하나의 ‘Ring’으로 만들어보자.&lt;/p&gt;

&lt;p&gt;해시 함수 \(f\)로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SHA-1&lt;/code&gt;을 사용한다고 가정해보자.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SHA-1&lt;/code&gt; 알고리즘이 생성할 수 있는 해시 값의 범위는 0부터 \(2^{160}-1\)까지로 매우 넓다.&lt;br /&gt;
이 출력 값 범위를 \(x_0, x_1, \dots, x_n\)이라고 할 때, \(x_0\)은 0, \(x_n\)은 \(2^{160}-1\)이 된다.&lt;/p&gt;

&lt;p&gt;이 넓은 1차원의 직선 해시 공간을 그림으로 표현하면 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/53.png&quot; alt=&quot;해시 공간&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이제 이 직선의 양 끝(\(x_0\)과 \(x_n\))을 구부려 이어 붙이면, 아래와 같은 해시 링이 완성된다.&lt;br /&gt;
안정 해시는 바로 이 원형 공간 위에서 동작한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/54.png&quot; alt=&quot;해시 링&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-해시-링-위에-서버와-키-배치하기&quot;&gt;2.2. 해시 링 위에 서버와 키 배치하기&lt;/h2&gt;

&lt;p&gt;이 해시 링 위에는 ‘서버’와 ‘데이터(키)’를 모두 올릴 수 있다.&lt;br /&gt;
여기서 중요한 점은 앞서 사용한 나머지 연산을 전혀 사용하지 않는다는 것이다.&lt;/p&gt;

&lt;p&gt;서버의 IP 주소나 이름을 해시 함수에 통과시켜 해시 링 위의 특정 위치에 대응시킨다.&lt;br /&gt;
아래 그림은 4대의 서버(서버0~서버3)을 해시 링 위에 배치한 모습이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/55.png&quot; alt=&quot;해시 서버&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-데이터가-저장될-서버-조회서버-조회-알고리즘&quot;&gt;2.3. 데이터가 저장될 서버 조회(서버 조회 알고리즘)&lt;/h2&gt;

&lt;p&gt;캐시할 데이터의 키(key0, key1, key2, key3) 역시 동일한 해시 함수를 거쳐 해시 링 위의 특정 지점에 배치된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/56.png&quot; alt=&quot;해시 키 배치&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그렇다면 특정 키는 어느 서버에 저장될까?&lt;br /&gt;
&lt;strong&gt;어떤 키가 저장될 서버는, 해당 키의 위치로부터 링을 시계 방향으로 탐색해 나가면서 만나는 ‘첫 번째 서버’이다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;아래 그림을 보면 key0의 위치에서 시계 방향으로 순회할 때 가장 먼저 만나는 서버가 s0(서버 0)이므로, key0은 서버 0에 저장된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/57.png&quot; alt=&quot;서버 조회&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;24-유연한-확장과-장애-대응-서버-추가-및-제거-시의-동작&quot;&gt;2.4. 유연한 확장과 장애 대응: 서버 추가 및 제거 시의 동작&lt;/h2&gt;

&lt;p&gt;이 원형 링 구조가 빛을 발하는 순간은 바로 &lt;strong&gt;서버의 개수가 변할 때&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;서버를 추가할 때(Scale-out)&lt;/strong&gt;&lt;br /&gt;
트래픽이 늘어나 새로운 서버 4(s4)가 추가되었다고 가정해보자.&lt;br /&gt;
아래 그림을 보면, 기존 서버들에 저장되어 있던 모든 데이터가 이동하는 것이 아니라 &lt;strong&gt;새로 추가된 s4와 그 반시계 방향에 있는 첫 번째 서버(s3) 사이의 키들만 재배치&lt;/strong&gt;된다.&lt;br /&gt;
결과적으로 key0만 서버 0에서 서버 4로 이동하며, 나머지 키들은 제자리를 유지한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/58.png&quot; alt=&quot;서버 추가&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;서버를 제거할 때(Scale-in/장애 발생)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;서버 1(s1)에 장애가 발생해 링에서 제거되었다고 해보자.&lt;br /&gt;
이 경우에도 전체 데이터가 흔들리지 않는다.&lt;br /&gt;
&lt;strong&gt;삭제된 s1과 그 반시계 방향에 있는 최초 서버(s0) 사이의 키들만 다음 시계 방향 서버인 s2로 재배치&lt;/strong&gt;된다.&lt;br /&gt;
즉, key1만 서버 2로 이동하게 된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/59.png&quot; alt=&quot;서버 제거&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-기본-안정-해시의-한계와-해결책&quot;&gt;3. 기본 안정 해시의 한계와 해결책&lt;/h1&gt;

&lt;p&gt;기본 안정 해시 알고리즘 절차는 매우 훌륭하지만, 실제 대규모 운영 환경에 바로 적용하기에는 두 가지 치명적인 한계가 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-파티션-크기-불균형과-데이터-쏠림-현상&quot;&gt;3.1. 파티션 크기 불균형과 데이터 쏠림 현상&lt;/h2&gt;

&lt;p&gt;첫 번째 문제는 &lt;strong&gt;서버별 파티션 크기를 균등하게 유지하는 것이 불가능&lt;/strong&gt;하다는 것이다.&lt;br /&gt;
여기서 파티션은 해시 링 위에서 인접한 서버 사이의 해시 공간을 의미한다.&lt;/p&gt;

&lt;p&gt;서버가 지속적으로 추가되고 삭제되다 보면, 어떤 서버는 굉장히 작은 해시 공간을 할당받고, 어떤 서버는 굉장히 큰 해시 공간을 갖게 된다.&lt;/p&gt;

&lt;p&gt;아래 그림은 서버 1이 삭제되는 바람에 서버 2의 파티션이 다른 파티션 대비 거의 두 배로 커진 상황이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/510.png&quot; alt=&quot;파티션 크기 문제&quot; /&gt;&lt;/p&gt;

&lt;p&gt;두 번째 문제는 파티션 크기가 제멋대로 변하면서 &lt;strong&gt;키의 균등 분포(Uniform Distribution)를 달성하기 어려워진다&lt;/strong&gt;는 점이다.&lt;/p&gt;

&lt;p&gt;아래 그림처럼 서버가 우연히 한쪽으로 몰려 배치되었다고 가정해보자.&lt;br /&gt;
이 경우 서버 1과 서버 3은 아무 데이터도 갖지 못하는 반면, 링의 절반 이상을 차지하는 서버 2의 파티션에 대부분의 키가 집중적으로 보관된다.&lt;br /&gt;
이렇게 되면 트래픽을 분산하려던 원래의 목적이 완전히 무너진다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/511.png&quot; alt=&quot;키의 균등 분포 문제&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-가상-노드virtual-node&quot;&gt;3.2. 가상 노드(Virtual Node)&lt;/h2&gt;

&lt;p&gt;이러한 불균형 문제를 해결하기 위해 도입된 기법이 &lt;strong&gt;가상 노드(Virtual Node)&lt;/strong&gt;, 또는 복제(Replica)라 불리는 기술이다.&lt;/p&gt;

&lt;p&gt;가상 노드는 실제 물리적인 서버 1대를 해시 링 위에서 여러 대의 논리적인 노드로 쪼개어 표현하는 방식이다.&lt;br /&gt;
아래 그림을 보면 물리 서버 0과 서버 1이 링 위에서 각각 3개의 가상 노드(s0_0, s0_1, s0_2 등)를 갖고 있다.&lt;br /&gt;
(숫자 3은 설명을 위한 임의의 값일 뿐, 실제 상용 시스템에서는 데이터의 고른 분포를 위해 100~200개 이상의 훨씬 큰 값을 사용한다.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/512.png&quot; alt=&quot;가상 노드&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이렇게 가상 노드를 촘촘하게 배치하면 각 물리 서버가 관리해야 할 파티션이 링 전체에 걸쳐 여러 개로 분산된다.&lt;/p&gt;

&lt;p&gt;데이터가 저장될 서버를 찾는 방법은 기존과 동일하다.&lt;br /&gt;
키의 위치에서 시계 방향으로 탐색하다 만나는 최초의 ‘가상 노드’가 해당 키를 품게 된다.&lt;/p&gt;

&lt;p&gt;아래 그림에서 키 k0은 시계 방향으로 탐색 시 가상 노드 s1_1을 가장 먼저 만나게 되므로, 물리적으로는 &lt;strong&gt;서버 1&lt;/strong&gt;에 저장된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/513.png&quot; alt=&quot;가상 노드&quot; /&gt;&lt;/p&gt;

&lt;p&gt;가상 노드의 개수를 늘리면 표준 편차가 작아져 키의 분포는 점점 더 완벽한 균등 상태에 가까워진다.&lt;br /&gt;
하지만 가상 노드 데이터를 저장할 메모리 공간도 그만큼 더 많이 필요해지므로, 시스템 요구사항에 맞는 적절한 &lt;strong&gt;트레이드오프&lt;/strong&gt; 조정이 필요하다.&lt;/p&gt;

&lt;p&gt;표준 편차(Standard Deviation)는 데이터가 평균값으로부터 얼마나 흩어져 있는지를 나타내는 통계 용어이다.&lt;br /&gt;
표준 편차가 ‘크다’는 것은 데이터가 서버 2에만 왕창 몰려 있고, 다른 서버는 텅 비어있는 등 들쭉날쭉하다는 뜻이다.&lt;br /&gt;
반대로 가상 노드의 개수를 늘려 표준 편차가 ‘작아졌다’는 것은, &lt;strong&gt;모든 서버가 비슷한 양의 데이터를 고르게 나눠 가졌음&lt;/strong&gt;을 의미한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-노드-변동-시-재배치되는-키의-범위-결정-방식&quot;&gt;3.3. 노드 변동 시 재배치되는 키의 범위 결정 방식&lt;/h2&gt;

&lt;p&gt;가상 노드 환경을 이해한 상태에서, 서버가 추가되거나 제거될 때 실제로 데이터가 어떻게 이동하는지 다시 한 번 보자.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;서버 추가 시 데이터 이동 범위&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;아래 그림처럼 새로운 서버 4(s4)가 링에 추가되었다고 해보자.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/514.png&quot; alt=&quot;서버 추가 시 키의 재배치&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이에 영향을 받는 범위는 새로 추가된 노드(s4)부터 그 반시계 방향에 있는 첫 번째 노드(s3)까지이다.&lt;br /&gt;
즉, &lt;strong&gt;s3부터 s4 사이에 있는 키들만 s4로 재배치&lt;/strong&gt;하면 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;서버 삭제 시 데이터 이동 범위&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;반대로 서버 1(s1)이 장애로 인해 삭제된다면 아래 그림처럼 삭제된 노드(s1)부터 그 반시계 방향에 있는 최초 노드(s0) 사이의 공간이 영향을 받는다.&lt;br /&gt;
이 공간에 있던 키들은 오갈 데가 없어졌으므로, 시계 방향을 따라 다음으로 살아있는 노드인 &lt;strong&gt;s2로 모두 재배치&lt;/strong&gt;해주면 된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0702/515.png&quot; alt=&quot;서버 삭제 시 키의 재배치&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-정리하며&quot;&gt;4. 정리하며..&lt;/h1&gt;

&lt;p&gt;대규모 아키텍처 설계에서 트래픽을 감당하기 위해 안정 해시는 선택이 아닌 필수 교양이다.&lt;br /&gt;
이번 포스트에서 다룬 안정 해시를 시스템에 도입했을 때 얻을 수 있는 3가지 핵심 이점은 다음과 같다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1)서버 변동 시 데이터 이동 최소화&lt;/strong&gt;&lt;br /&gt;
서버가 추가되거나 삭제될 때 전체 데이터를 다시 섞는 것이 아니라, 링 구조상 인접한 노드의 일부 데이터(평균 \(k/n\)개)만 재배치한다.&lt;br /&gt;
덕분에 대규모 캐시 미스로 인한 데이터베이스(DB) 과부하 사태를 막을 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2)진정한 수평적 규모 확장성(Scale-out) 달성&lt;/strong&gt;&lt;br /&gt;
가상 노드(Virtual Node) 기법을 통해 데이터를 링 위에 촘촘하고 고르게 분포시킬 수 있어, 유연한 시스템 확장이 가능해진다.&lt;/p&gt;

&lt;h2 id=&quot;데이터가-균등하게-분포하는-것과-수평적-규모-확장은-무슨-관계일까&quot;&gt;💡데이터가 균등하게 분포하는 것과 수평적 규모 확장은 무슨 관계일까?&lt;/h2&gt;
&lt;p&gt;만약 데이터가 고르게 분배되지 않고 특정 서버 한 대에만 몰려있다면(데이터 쏠림 현상), 
트래픽을 감당하려고 물리 서버를 100대로 늘려도 사용자들의 요청은 여전히 그 1대의 서버로만 향하게 된다.&lt;br /&gt;
확장의 의미가 전혀 없는 것이다.&lt;br /&gt;
데이터가 모든 서버에 공평하게 쪼개져 있어야만 서버를 1대 추가했을 때 기존 서버들의 짐(부하)을 정확히 덜어줄 수 있고, 
전체 시스템의 처리 용량도 추가한 만큼 정비례해서 늘어나는 ‘진정한 수평적 규모 확장’이 가능해진다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3)핫스팟(Hotspot) 키 문제 방지&lt;/strong&gt;&lt;br /&gt;
특정 데이터(샤드)에 대한 접근이 지나치게 빈번해 발생하는 서버 과부하를 줄여준다.&lt;br /&gt;
예를 들어 수천만 명의 팔로워를 가진 유명인의 SNS 게시물 정보가 우연히 한 대의 서버에 집중적으로 저장되는 최악의 상황을 방지하고, 트래픽을 여러 서버로 분산시켜 준다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;실제-상용-시스템의-안정-해시-활용-사례&quot;&gt;실제 상용 시스템의 안정 해시 활용 사례&lt;/h2&gt;

&lt;p&gt;안정 해시는 실제로 우리가 매일 사용하는 글로벌 IT 기업들의 아키텍처에 깊숙이 자리 잡고 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf&quot;&gt;Amazon DynamoDB의 파티셔닝 관련 컴포넌트&lt;/a&gt;: 전 세계적인 스케일을 자랑하는 아마존의 키-값(Key-Value) 스토어 파티셔닝 컴포넌트&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.cs.cornell.edu/Projects/ladis2009/papers/lakshman-ladis2009.pdf&quot;&gt;Apache Cassandra: 클러스터에서의 데이터 파티셔닝&lt;/a&gt;: 대규모 클러스터 환경에서의 강력한 데이터 파티셔닝&lt;/li&gt;
  &lt;li&gt;디스코드(Discord) 채팅 애플리케이션: 초당 수억 건의 메시지가 오가는 대규모 실시간 채팅 애플리케이션의 트래픽 분산&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://theory.stanford.edu/~tim/s16/l/l1.pdf&quot;&gt;아카마이(Akamai) CDN&lt;/a&gt;: 전 세계에 흩어진 콘텐츠 전송 네트워크(Edge Server)의 부하 분산&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44824.pdf&quot;&gt;Google 매글레브(Maglev) 네트워크 부하 분산기&lt;/a&gt;: 구글의 빠르고 안정적인 소프트웨어 기반 네트워크 부하 분산기&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001033116&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Consistent_hashing&quot;&gt;Consistent hashing 개념 정리 (Wikipedia)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf&quot;&gt;Dynamo: Amazon’s Highly Available Key-value Store (논문 원문)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.cs.cornell.edu/Projects/ladis2009/papers/lakshman-ladis2009.pdf&quot;&gt;Cassandra - A Decentralized Structured Storage System (논문 원문)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://theory.stanford.edu/~tim/s16/l/l1.pdf&quot;&gt;Stanford CS168: Introduction and Consistent Hashing (강의 자료)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44824.pdf&quot;&gt;Maglev: A Fast and Reliable Software Network Load Balancer (Google Research)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 02 Jul 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/07/02/architecture-consistent-hashing/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/07/02/architecture-consistent-hashing/</guid>
        
        <category>architecture</category>
        
        <category>consistent-hashing</category>
        
        <category>load-balancing</category>
        
        <category>system-design</category>
        
        <category>안정해시</category>
        
        <category>분산시스템</category>
        
        <category>대규모시스템</category>
        
        <category>hash-ring</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(2) - 호텔 예약 시스템 아키텍처</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-요구사항-정의&quot;&gt;1. 요구사항 정의&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-비기능-요구사항&quot;&gt;1.1. 비기능 요구사항&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-개략적-규모-추정&quot;&gt;2. 개략적 규모 추정&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-일일-예상-예약-건수와-tps&quot;&gt;2.1. 일일 예상 예약 건수와 TPS&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-단계별-유저-이탈률을-고려한-페이지별-qps-예측&quot;&gt;2.2. 단계별 유저 이탈률을 고려한 페이지별 QPS 예측&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-msa-기반의-개략적-아키텍처-및-api-설계&quot;&gt;3. MSA 기반의 개략적 아키텍처 및 API 설계&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-컴포넌트별-역할-분담&quot;&gt;3.1. 컴포넌트별 역할 분담&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#311-인프라-및-게이트웨이-계층&quot;&gt;3.1.1. 인프라 및 게이트웨이 계층&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#312-핵심-도메인-마이크로서비스-계층&quot;&gt;3.1.2. 핵심 도메인 마이크로서비스 계층&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-api-설계&quot;&gt;3.2. API 설계&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#321-호텔-및-객실-관리-api주로-관리자-및-조회용&quot;&gt;3.2.1. 호텔 및 객실 관리 API(주로 관리자 및 조회용)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#322-예약-api-및-멱등성-설계&quot;&gt;3.2.2. 예약 API 및 멱등성 설계&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-초기-데이터-모델링과-rdbms-선택-이유&quot;&gt;3.3. 초기 데이터 모델링과 RDBMS 선택 이유&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#왜-대규모-트래픽-아키텍처에-rdbms를-사용했을까&quot;&gt;💡왜 대규모 트래픽 아키텍처에 RDBMS를 사용했을까?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#34-마이크로서비스-간-고성능-통신-전략-grpc&quot;&gt;3.4. 마이크로서비스 간 고성능 통신 전략: gRPC&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-상세-설계-1단계-데이터-모델-개선&quot;&gt;4. 상세 설계 1단계: 데이터 모델 개선&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-room-중심에서-roomtype-중심으로의-전환&quot;&gt;4.1. Room 중심에서 RoomType 중심으로의 전환&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#42-가용-객실-파악을-위한-쿼리-및-초과-예약-로직&quot;&gt;4.2. 가용 객실 파악을 위한 쿼리 및 초과 예약 로직&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#43-데이터-볼륨-관리와-수평-확장sharding&quot;&gt;4.3. 데이터 볼륨 관리와 수평 확장(Sharding)&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-상세-설계-2단계-분산-환경에서의-동시성concurrency&quot;&gt;5. 상세 설계 2단계: 분산 환경에서의 동시성(Concurrency)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#51-시나리오-a-동일-사용자가-예약-버튼을-여러-번-누르는-경우&quot;&gt;5.1. 시나리오 A: 동일 사용자가 예약 버튼을 여러 번 누르는 경우&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#52-시나리오-b-여러-사용자가-같은-객실을-동시에-예약하려-하는-경우&quot;&gt;5.2. 시나리오 B: 여러 사용자가 같은 객실을 동시에 예약하려 하는 경우&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#트랜잭션-격리-수준의-종류와-직렬화-가능-수준이란&quot;&gt;💡트랜잭션 격리 수준의 종류와 ‘직렬화 가능 수준’이란?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#repeatable-read반복-가능-읽기&quot;&gt;💡Repeatable Read(반복 가능 읽기)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#serializable직렬화-가능-수준&quot;&gt;💡Serializable(직렬화 가능 수준)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#521-방안-1-비관적-락pessimistic-lock&quot;&gt;5.2.1. 방안 1: 비관적 락(Pessimistic Lock)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#deadlock이-생기지-않는-애플리케이션-코드를-어떻게-짜야-할까&quot;&gt;💡DeadLock이 생기지 않는 애플리케이션 코드를 어떻게 짜야 할까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#522-방안-2-낙관적-락optimistic-lock&quot;&gt;5.2.2. 방안 2: 낙관적 락(Optimistic Lock)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#523-데이터베이스-제약-조건db-constraint&quot;&gt;5.2.3. 데이터베이스 제약 조건(DB Constraint)&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-상세-설계-3단계-시스템-규모-확장&quot;&gt;6. 상세 설계 3단계: 시스템 규모 확장&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#61-데이터베이스-샤딩&quot;&gt;6.1. 데이터베이스 샤딩&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#62-redis-캐시-계층-도입과-데이터-동기화-아키텍처&quot;&gt;6.2. Redis 캐시 계층 도입과 데이터 동기화 아키텍처&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#redis의-lru란&quot;&gt;💡Redis의 LRU란?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#cdc와-debezium이란&quot;&gt;💡CDC와 Debezium이란?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#캐시-지연으로-인한-데이터-불일치의-해답&quot;&gt;💡캐시 지연으로 인한 데이터 불일치의 해답&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#63-마이크로서비스-간-데이터-일관성-제어&quot;&gt;6.3. 마이크로서비스 간 데이터 일관성 제어&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#631-방안-1-2단계-커밋2-phase-commit-2pc&quot;&gt;6.3.1. 방안 1: 2단계 커밋(2-Phase Commit, 2PC)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#632-방안-2-사가-패턴saga-pattern&quot;&gt;6.3.2. 방안 2: 사가 패턴(Saga Pattern)&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-마무리-및-요약&quot;&gt;7. 마무리 및 요약&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#71-호텔-예약-시스템-핵심-아키텍처-요약&quot;&gt;7.1. 호텔 예약 시스템 핵심 아키텍처 요약&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#72-상용-솔루션-시장-동향&quot;&gt;7.2. 상용 솔루션 시장 동향&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;여기서 설계할 대규모 호텔 예약 시스템은 수백만 개의 객실 데이터를 관리하고, 성수기나 대규모 이벤트 시 발생하는 폭발적인 트래픽 속에서도 
&lt;strong&gt;이중 예약(Double Booking) 없이 정확하고 안정적으로 예약을 처리하는 분산 아키텍처 시스템&lt;/strong&gt;이다.&lt;br /&gt;
본질적으로 항공권 예약이나 영화 티켓 예매 시스템과 동일한 기술적 과제(동시성 제어, 초과 예약 처리 등)를 해결해야 한다.&lt;/p&gt;

&lt;p&gt;여기서는 &lt;strong&gt;5,000개의 호텔과 100만 개의 객실&lt;/strong&gt;을 갖춘 대규모 호텔 체인 시스템을 설계해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-요구사항-정의&quot;&gt;1. 요구사항 정의&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;시스템 규모&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 시스템의 전반적인 규모는 어느 정도인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 전 세계 &lt;strong&gt;5,000개 호텔&lt;/strong&gt;과 총 &lt;strong&gt;100만 개의 객실&lt;/strong&gt;을 보유한 대형 호텔 체인을 위한 웹사이트 및 앱 시스템이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;결제 및 예약 채널&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 대금 지불은 어느 시점에 이루어지며, 어떤 채널을 지원해야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 예약 시점에 온라인으로 &lt;strong&gt;전액 결제&lt;/strong&gt;한다. 채널은 오직 &lt;strong&gt;웹사이트와 모바일 앱&lt;/strong&gt;을 통해서만 예약할 수 있으며, 전화 예약을 고려하지 않는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;예약 취소 및 정책&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 예약 취소 기능도 지원해야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 필수로 지원해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;객실 가격 변동성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 객실 가격은 고정인가, 실시간으로 변동하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; &lt;strong&gt;유동적&lt;/strong&gt;이다. 해당 날짜에 객실 여유가 얼마나 있는지, 성수기인지 비성수기인지에 따라 매일 가격이 달라질 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;초과 예약(Overbooking) 정책&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 호텔 비즈니스 특성상 발생하는 초과 예약을 고려해야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 실제 객실 수의 &lt;strong&gt;10% 수준까지 초과 예약&lt;/strong&gt;을 허용해야 한다. 실제 투숙객들이 예약을 취소하는 비율을 고려하여 방을 더 많이 판매할 수 있도록 지원해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;설계 및 구현 범위&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 타이트한 일정 내에 가장 집중해야 할 핵심 기능은 무엇인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 일정 제약이 있으므로 복잡한 ‘객실 검색/필터링’ 기능을 제외한다. 아래 기능에 집중한다.
        &lt;ul&gt;
          &lt;li&gt;호텔/객실 상세 페이지 조회&lt;/li&gt;
          &lt;li&gt;객실 예약 처리&lt;/li&gt;
          &lt;li&gt;관리자용 백오피스(호텔/객실 정보 CRUD 및 초과 관리)&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-비기능-요구사항&quot;&gt;1.1. 비기능 요구사항&lt;/h2&gt;

&lt;p&gt;대규모 호텔 예약 시스템의 성패는 비즈니스 로직의 구현보다 &lt;strong&gt;분산 환경에서의 안정성&lt;/strong&gt;에 달려 있다.&lt;br /&gt;
여기서 만족해야 할 핵심 비기능 요구사항은 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;높은 수준의 동시성 지원&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;성수기나 대규모 이벤트(예: 유명 콘서트, 축제 등) 기간에는 특정 인기 호텔의 한정된 객실을 예약하려는 트래픽이 한 번에 몰린다.&lt;/li&gt;
      &lt;li&gt;이 때 시스템은 &lt;strong&gt;이중 예약&lt;/strong&gt; 없이 정확하게 단 하나의 요청만 성공시켜야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;적절한 응답 지연 시간&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 잔여 객실을 조회할 때는 ms 단위의 빠른 응답이 필요하지만, 최종 ‘예약하기’ 버튼을 눌렀을 때는 안정적인 데이터 처리를 위해 수 초 정도의 지연 시간이 발생하는 것은 비즈니스적으로 허용된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-개략적-규모-추정&quot;&gt;2. 개략적 규모 추정&lt;/h1&gt;

&lt;h2 id=&quot;21-일일-예상-예약-건수와-tps&quot;&gt;2.1. 일일 예상 예약 건수와 TPS&lt;/h2&gt;

&lt;p&gt;시스템의 평균적인 부하를 파악하기 위해 아래와 같은 비즈니스 지표를 가정한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;총 객실 수:&lt;/strong&gt; 1,000,000개&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;평균 객실 가동률(Occupancy Rate):&lt;/strong&gt; 70%&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;고객별 평균 투숙 기간:&lt;/strong&gt; 3일&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이를 바탕으로 하루에 새롭게 발생하는 예약 건수를 계산하면 아래와 같다.&lt;/p&gt;

\[\text{일일 예상 예약 건수} = \frac{1,000,000객실 \times 0.7}{3일} = 233,333.33\]

&lt;p&gt;계산 편의를 위해 하루 평균 &lt;strong&gt;240,000건&lt;/strong&gt;의 예약이 발생한다고 가정하자.&lt;br /&gt;
이를 초당 예약 건수, 즉 예약 트랜잭션 수(TPS)로 환산하면 아래와 같다.&lt;/p&gt;

\[\text{평균 예약 TPS} = \frac{240,000 \text{ 건}}{86,400 \text{ 초}} \approx 2.77\]

&lt;p&gt;즉, 일상적인 상황에서 최종 예약 버튼을 누르는 트래픽은 &lt;strong&gt;약 3 TPS&lt;/strong&gt;로 그리 높지 않은 편이다.&lt;br /&gt;
하지만, 이것만 보고 안심해서는 안 된다.&lt;br /&gt;
사용자의 실제 서비스 이용 흐름에 따른 페이지별 QPS(Queries Per Second)를 봐야 진짜 병목 구간이 보인다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-단계별-유저-이탈률을-고려한-페이지별-qps-예측&quot;&gt;2.2. 단계별 유저 이탈률을 고려한 페이지별 QPS 예측&lt;/h2&gt;

&lt;p&gt;호텔 웹사이트를 이용하는 사용자는 최종 결제 단계로 갈수록 &lt;strong&gt;깔때기(Funnel) 형태의 이탈 흐름&lt;/strong&gt;을 보인다.&lt;br /&gt;
업계 표준을 반영하여 다음 단계로 진입하는 사용자의 비율을 10%로 가정하자.&lt;br /&gt;
즉, 90%의 유저는 최종 예약을 완료하기 전에 이탈한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;1단계: 호텔/객실 상세 페이지 조회&lt;/strong&gt;(정보 탐색) → 90% 이탈&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2단계: 예약 상세 정보 확인 페이지&lt;/strong&gt;(날짜, 결제 수단 입력) → 90% 이탈&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;3단계: 최종 객실 예약 요청&lt;/strong&gt;(트랜잭션 발생, &lt;strong&gt;3 QPS&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;최종 예약 단계가 3 QPS이므로 역산하여 각 단계별 필요한 최대 QPS를 도출하면 아래와 같은 규모 추정 표가 완성된다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;서비스 단계&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;예상 처리량 (QPS)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;주요 작업 특성&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3단계: 객실 예약 페이지&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;쓰기(Write) 연산 위주, 고도의 일관성 필요&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2단계: 예약 상세 확인 페이지&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;30&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;읽기(Read) 연산 위주, 실시간 가용성 확인&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1단계: 호텔/객실 상세 페이지&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;300&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;압도적인 읽기(Read) 연산, 캐싱 적극 활용 구간&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;p&gt;규모 추정 결과, 최종 예약 처리(3 QPS) 자체는 단일 데이터베이스 서버로도 충분히 처리할 수 있는 수준이다.&lt;br /&gt;
하지만 &lt;strong&gt;1단계와 2단계의 조회 트래픽(총 330 QPS)이 성수기나 대규모 이벤트 시점에 수십에서 수백 배 급증할 수 있다는 점&lt;/strong&gt;이 이 아키텍처 설계의 핵심 과제이다.&lt;/p&gt;

&lt;p&gt;따라서 뒤에 다룰 상세 설계에서는 &lt;strong&gt;조회 성능 극대화를 위한 캐싱 전략(Redis)&lt;/strong&gt;과 &lt;strong&gt;안전한 예약 처리를 위한 동시성 메커니즘&lt;/strong&gt;을 결합한 하이브리드 아키텍처를 도출한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-msa-기반의-개략적-아키텍처-및-api-설계&quot;&gt;3. MSA 기반의 개략적 아키텍처 및 API 설계&lt;/h1&gt;

&lt;p&gt;대규모 호텔 예약 시스템은 시스템의 확장성과 서비스 간 독립적인 배포를 위해 MSA를 채택한다.&lt;br /&gt;
각 도메인의 핵심 비즈니스 로직을 격리하고, 폭발적인 읽기 트래픽과 안정적인 쓰기 트래픽을 동시에 처리할 수 있는 개략적 구조를 먼저 살펴본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-컴포넌트별-역할-분담&quot;&gt;3.1. 컴포넌트별 역할 분담&lt;/h2&gt;

&lt;p&gt;시스템의 전체적인 구조는 클라이언트 요청이 들어오는 외부 영역과, 실제 비즈니스 로직이 처리되는 내부 사설망 영역으로 명확히 분리된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/overall.png&quot; alt=&quot;개략적 설계안&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;311-인프라-및-게이트웨이-계층&quot;&gt;3.1.1. 인프라 및 게이트웨이 계층&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;CDN(콘텐츠 전송 네트워크)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;자바스크립트 번들, 호텔 전경 이미지, 룸 타입별 동영상, 정적 HTML 등 모든 &lt;strong&gt;정적 콘텐츠를 캐시&lt;/strong&gt;하여 클라이언트와 가장 가까운 엣지 서버에서 반환한다.&lt;/li&gt;
      &lt;li&gt;백엔드 서버의 부하를 줄이고 초기 페이지 로딩 속도를 극대화한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;공개 API 게이트웨이&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;인증, &lt;a href=&quot;https://assu10.github.io/dev/2026/06/21/architecture-distributed-rate-limiter/&quot;&gt;처리율 제한(Rate Limiting)&lt;/a&gt;, SSL 종단 등의 공통 인프라 기능을 수행하는 Fully Managed 서비스이다.&lt;/li&gt;
      &lt;li&gt;엔드포인트에 따라 적절한 마이크로서비스로 요청을 라우팅한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;내부 API/VPN&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;외부 인터넷망에 노출되지 않는 사설 가상망(VPN)이다.&lt;/li&gt;
      &lt;li&gt;오직 승인된 사내 시스템이나 백오피스 관리자 페이지를 통해서만 접근이 가능하도록 보호한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;312-핵심-도메인-마이크로서비스-계층&quot;&gt;3.1.2. 핵심 도메인 마이크로서비스 계층&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;호텔 서비스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;호텔 위치, 전화번호, 객실 유형 등 정적인 마스터 데이터를 제공한다.&lt;/li&gt;
      &lt;li&gt;데이터 변경 빈도가 매우 낮기 때문에 대다수 요청은 내장 캐시나 데이터베이스 복제본(Replica)에서 소화한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;요금 서비스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 날짜, 시즌, 가용 객실 추이에 따라 실시간으로 변동되는 유동적 객실 요금 데이터를 제공한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;예약 서비스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자의 예약 요청을 접수하고, 예약 상태를 관리하며, 잔여 객실 재고를 차감하거나 복구하는 시스템의 핵심 코어이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;결제 서비스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;PG(Payment Gateway)사와의 연동을 통해 실제 결제를 승인/취소하며, 결제 성공 여부에 따라 예약 서비스의 상태를 갱신하도록 트리거한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;호텔 관리 서비스(백오피스)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;승인된 호텔 임직원이 객실 상태를 변경하거나, 긴급 유지보수를 위해 가용 객실을 차감하고, 수동으로 예약 및 취소를 처리하는 관리용 서비스이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/relation.png&quot; alt=&quot;서비스 간 연결&quot; /&gt;&lt;/p&gt;

&lt;p&gt;실무 환경에서는 이러한 마이크로서비스 간 통신 성능을 극대화하기 위해 REST API 대신 &lt;a href=&quot;#34-마이크로서비스-간-고성능-통신-전략-grpc&quot;&gt;&lt;strong&gt;gRPC 프레임워크&lt;/strong&gt;&lt;/a&gt;를 적극적으로 활용한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-api-설계&quot;&gt;3.2. API 설계&lt;/h2&gt;

&lt;p&gt;호텔 웹사이트를 완성하려면 특정 기준에 맞는 객실을 검색하는 등의 기능도 필요하지만 기술적으로 도전적이지 않으므로 여기서는 핵심 비즈니스 기능인 호텔, 객실, 예약 도메인 API만 정의한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;321-호텔-및-객실-관리-api주로-관리자-및-조회용&quot;&gt;3.2.1. 호텔 및 객실 관리 API(주로 관리자 및 조회용)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;호텔 관련 엔드포인트&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;HTTP 메서드&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;엔드포인트&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;설명&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;접근 권한&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/hotels/:id&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;호텔의 상세 정보 및 소개 반환&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;전체 공개&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/hotels&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;신규 호텔 체인 추가&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;호텔 직원 전용&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PUT&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/hotels/:id&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;호텔 기본 정보 수정&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;호텔 직원 전용&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/hotels/:id&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;호텔 정보 삭제 (Soft Delete 권장)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;호텔 직원 전용&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;객실 관련 엔드포인트&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;HTTP 메서드&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;엔드포인트&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;설명&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;접근 권한&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/hotels/:id/rooms/:room_id&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;특정 객실의 상세 정보 조회&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;전체 공개&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/hotels/:id/rooms&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;특정 호텔에 신규 객실 물리 데이터 추가&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;호텔 직원 전용&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PUT&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/hotels/:id/rooms/:room_id&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;객실 상태 정보 (예: 정비 중 등) 업데이트&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;호텔 직원 전용&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/hotels/:id/rooms/:room_id&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;특정 객실 데이터 삭제&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;호텔 직원 전용&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;322-예약-api-및-멱등성-설계&quot;&gt;3.2.2. 예약 API 및 멱등성 설계&lt;/h3&gt;

&lt;p&gt;고객이 실제 머무를 방을 선택하고 결제 프로세스로 진입하기 위한 API 세트이다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;HTTP 메서드&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;엔드포인트&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;설명&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/reservations&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;현재 로그인한 사용자의 과거 및 현재 예약 이력 전체 반환&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/reservations/:id&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;특정 예약 건에 대한 결제 상태 및 투숙 상세 정보 반환&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/reservations&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;[핵심]&lt;/strong&gt; 신규 객실 예약 요청 접수 및 가용 재고 선점&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DELETE&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/reservations/:id&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;특정 예약 취소 및 잔여 객실 재고 복구 요청&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;신규 예약 생성 시 Request Body와 멱등 키&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;startDate&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2026-07-01&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;endDate&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2026-07-03&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;hotelID&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;111&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;roomID&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;U12354673389&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;reservationID&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;13422445&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;요청 본문의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservationID&lt;/code&gt;는 단순한 데이터 식별자가 아니라, 네트워크 지연 등으로 인한 동일한 요청이 중복 접수되었을 때 시스템이 단 한 번만 처리하도록 
보장하는 &lt;strong&gt;멱등 키(Idempotent Key)&lt;/strong&gt; 역할을 수행한다.&lt;br /&gt;
이 유일성 제약 조건을 통해 분산 환경에서의 ‘따닥 클릭’ 중복 예약을 원천 차단한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-초기-데이터-모델링과-rdbms-선택-이유&quot;&gt;3.3. 초기 데이터 모델링과 RDBMS 선택 이유&lt;/h2&gt;

&lt;p&gt;시스템의 트래픽 흐름을 보면, 전체 사용자 중 90% 이상이 ‘조회’ 연산을 수행하고, 단 1% 미만의 사용자만이 최종 ‘예약(쓰기)’ 연산에 도달한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;호텔 상세 정보 확인&lt;/li&gt;
  &lt;li&gt;지정된 날짜 범위에 사용 가능한 객실 유형 확인&lt;/li&gt;
  &lt;li&gt;예약 정보 기록&lt;/li&gt;
  &lt;li&gt;예약 내역 또는 과거 예약 이력 정보 조회&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이러한 접근 패턴을 바탕으로 초기 데이터베이스 스키마를 다음과 같이 설계할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/hotel.png&quot; alt=&quot;DB 스키마&quot; /&gt;&lt;/p&gt;

&lt;p&gt;또한 예약 테이블(reservation)의 status 필드는 결제 진행 상황에 따라 유연하게 변하는 상태 머신(State Machine) 구조를 가진다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/reservation.png&quot; alt=&quot;예약 상태&quot; /&gt;&lt;/p&gt;

&lt;p&gt;대략적인 추정 과정을 통해 시스템 규모가 크지 않은 것은 알았지만, 대규모 이벤트가 있는 경우 트래픽이 급증할 수 있으니 대비해야 한다.
여기서는 RDBMS를 사용한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;왜-대규모-트래픽-아키텍처에-rdbms를-사용했을까&quot;&gt;💡왜 대규모 트래픽 아키텍처에 RDBMS를 사용했을까?&lt;/h3&gt;

&lt;p&gt;아래와 같은 의문이 들 수 있다.&lt;/p&gt;

&lt;p&gt;‘트래픽이 높으면 당연히 무한 확장 가능한 NoSQL을 써야 하는 것 아닐까?’&lt;br /&gt;
‘읽기 트래픽이 많을 때 앞단에 Redis 캐시를 두는 구조라면, 백엔드 DB는 NoSQL이 더 유리하지 않을까?’&lt;/p&gt;

&lt;p&gt;결론부터 말하자면, &lt;strong&gt;예약 및 결제 도메인에서는 RDBMS가 절대적으로 유리&lt;/strong&gt;하다.&lt;br /&gt;
이유는 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;강력한 ACID 트랜잭션 보장&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;예약 시스템은 ‘재고 차감’과 ‘예약서 생성’이 하나의 원자적(Atomicity) 단위로 묶여야 한다.&lt;/li&gt;
      &lt;li&gt;NoSQL은 단일 도큐먼트 수준의 트랜잭션은 잘 지원하지만, 여러 테이블 혹은 분산 노드 간의 엄격한 격리성(Isolation)을 보장하는데 취약하다.&lt;/li&gt;
      &lt;li&gt;반면, RDBMS는 Lock 기능을 이용하여 이전 작업이 완전히 끝날 때까지 다른 스레드가 대기하도록 할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;읽기 최적화는 캐시 계층의 몫&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;RDBMS도 적절한 인덱스 설계가 동반되면 수천 QPS 수준의 읽기는 매우 안정적으로 처리한다.&lt;/li&gt;
      &lt;li&gt;한계를 넘어선 초대형 읽기 트래픽은 &lt;strong&gt;앞단에 Redis 같은 인메모리 캐시 계층을 배치하여 흡수&lt;/strong&gt;하면 되기 때문에, 굳이 영속성 저장소인 메인 DB를 ACID가 약한 NoSQL로 변경하여 데이터 정합성 지옥에 빠질 이유가 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;명확한 구조적 관계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;호텔, 객실, 고객, 예약은 비즈니스적 관계가 매우 끈끈한 정형 데이터이다.&lt;/li&gt;
      &lt;li&gt;FK를 통한 무결성 검증과 Join 연산의 이점을 RDBMS에서 온전히 누릴 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;🚨 초기 데이터 모델의 치명적인 한계점과 수정 예고&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;위 스키마를 보면 &lt;strong&gt;실무 비즈니스 환경에서는 사용할 수 없는 설계 결함&lt;/strong&gt;이 존재한다.&lt;br /&gt;
바로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation&lt;/code&gt; 테이블이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_id&lt;/code&gt;를 직접 참조하고 있다는 점이다.&lt;/p&gt;

&lt;p&gt;고객은 호텔을 예약할 때 ‘302호 방’이라는 특정 물리 객실을 예약하지 않는다.&lt;br /&gt;
대다수의 호텔 예약 플랫폼은 ‘디럭스 킹 룸(Room Type)’을 예약하고, 실제 몇 호에 묵을지는 투숙객이 당일 체크인하는 시점에 결정된다.&lt;/p&gt;

&lt;p&gt;이 요구사항을 반영하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_id&lt;/code&gt; 기반의 스키마를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_type_id&lt;/code&gt; 중심의 확장 가능한 모델로 변경해야 한다.&lt;br /&gt;
이 구체적인 개선 과정은 &lt;a href=&quot;#4-상세-설계-1단계-데이터-모델-개선&quot;&gt;4. 상세 설계 1단계: 데이터 모델 개선&lt;/a&gt;에서 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;34-마이크로서비스-간-고성능-통신-전략-grpc&quot;&gt;3.4. 마이크로서비스 간 고성능 통신 전략: gRPC&lt;/h2&gt;

&lt;p&gt;MSA로 시스템을 잘게 쪼개면 서비스 간 네트워크 통신 횟수가 급증한다.&lt;br /&gt;
예를 들어 예약 서비스는 최종 결제 전에 총 요금을 정산하기 위해 요금 서비스에 질의해야 하고, 관리 서비스는 변경 사항을 타 서비스로 전파해야 한다.&lt;br /&gt;
이 때 2026년 대규모 아키텍처의 필수 프로토콜로 자리잡은 &lt;a href=&quot;https://grpc.io/docs/what-is-grpc/introduction/&quot;&gt;&lt;strong&gt;gRPC&lt;/strong&gt;&lt;/a&gt;가 활약한다.&lt;/p&gt;

&lt;p&gt;전통적인 REST API(JSON over HTTP/1.1)는 텍스트 기반 포맷을 사용하여 직렬화/역직렬화 비용이 크고, 한 연결당 하나의 요청만 처리할 수 있어 MSA 환경에서 
심각한 네트워크 병목(Head-of-Line Blocking)을 유발한다.&lt;/p&gt;

&lt;p&gt;Google이 개발한 gRPC는 이를 혁신적으로 해결한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;HTTP/2 기반 멀티플렉싱&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하나의 TCP 커넥션 위에서 수많은 요청과 응답을 동시에 비동기적으로 주고받는다.&lt;/li&gt;
      &lt;li&gt;네트워크 핸드셰이크 비용이 극적으로 절감되어 MSA 내부 통신의 지연 시간이 최소화된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;프로토콜 버퍼 활용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사람이 읽는 텍스트(JSON) 대신, 컴파일된 &lt;strong&gt;바이너리 형식으로 데이터를 압축하여 전송&lt;/strong&gt;한다.&lt;/li&gt;
      &lt;li&gt;페이로드 크기가 JSON 대비 최대 10배 이상 작아져 대역폭을 극도로 절감한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;강력한 타입 명시와 Stub 생성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;IDL(Interface Definition Language) 파일인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.proto&lt;/code&gt; 파일에 데이터 구조를 정의하면 Java, Kotlin, Go 등 다양한 언어로 통신 코드가 자동 생성(Stub)된다.&lt;/li&gt;
      &lt;li&gt;서비스 간 호출이 마치 같은 프로세스 내의 로컬 함수를 호출하는 것처럼 명확하고 안전해진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-상세-설계-1단계-데이터-모델-개선&quot;&gt;4. 상세 설계 1단계: 데이터 모델 개선&lt;/h1&gt;

&lt;p&gt;위에서 언급했듯이, 투숙객은 구체적인 호수(예: 302호)가 아니라 객실 유형(Room Type)을 보고 예약한다.&lt;br /&gt;
따라서 데이터 모델을 한 단계 진화시켜야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-room-중심에서-roomtype-중심으로의-전환&quot;&gt;4.1. Room 중심에서 RoomType 중심으로의 전환&lt;/h2&gt;

&lt;p&gt;기존의 예약 API(POST /v1/reservations) 요청 구조에서 구체적인 방을 가리키던 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;roomID&lt;/code&gt;를 객실 유형을 나타내는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;roomTypeID&lt;/code&gt;로 변경한다.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;/*&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;변경된&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;/v&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;/reservations&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;Body&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;*/&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;startDate&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2026-07-01&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;endDate&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2026-07-03&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;hotelID&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;111&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;roomTypeID&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;RT001&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;roomID에서&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;roomTypeID로&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;변경&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;reservationID&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;13422445&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 비즈니스 요구사항을 지원하기 위해 데이터베이스 스키마를 아래와 같이 구조화한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/schema.png&quot; alt=&quot;갱신된 스키마&quot; /&gt;&lt;/p&gt;

&lt;p&gt;핵심은 예약 서비스 관리를 위해 추가된 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_type_inventory&lt;/code&gt; 테이블이다.&lt;br /&gt;
이 테이블은 특정 호텔의 객실 유형별로 일자마다 재고를 관리한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;total_inventory&lt;/code&gt;: 총 객실 수에서 일시적으로 제외한 객실 수(유지보수, 청소 등으로 인한 가용 제외)를 뺀 실제 판매 가능 객실 수&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;total_reserved&lt;/code&gt;: 지정된 hotel_id, room_type_id, date에 이미 예약이 완료된 객실의 수&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_type_inventory&lt;/code&gt; 테이블의 PK는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(hotel_id, room_type_id, date)&lt;/code&gt;의 &lt;strong&gt;복합키&lt;/strong&gt;로 구성한다.&lt;br /&gt;
이 테이블은 대략 미래 2년 치의 데이터를 매일 주기적인 일괄 작업(Batch)으로 돌려 미리 채워두는 전략을 취한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;42-가용-객실-파악을-위한-쿼리-및-초과-예약-로직&quot;&gt;4.2. 가용 객실 파악을 위한 쿼리 및 초과 예약 로직&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;hotel_id&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;room_type_id&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;date&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;total_inventory&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;total_reserved&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;211&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2026-06-01&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;100&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;80&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;211&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2026-06-02&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;100&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;82&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;211&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2026-06-03&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;100&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;86&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;211&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;…&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;…&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;…&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;211&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2028-05-31&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;100&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;211&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1002&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2026-06-01&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;200&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;164&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2210&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;101&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2026-06-01&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;30&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;23&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2210&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;101&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2026-06-02&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;30&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;25&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;ul&gt;
  &lt;li&gt;입력: startDate(2026-06-01), endDate(2026-06-03), roomTypeId, hotelId, numberOfRoomsToReserve&lt;/li&gt;
  &lt;li&gt;출력: 해당 유형의 객실에 여유가 있고, 사용자가 예약 가능한 상태이면 True, 아니면 False&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;사용자가 특정 기간(2026-06-01 ~ 2026-06-03) 동안 방이 남아있는지 조회할 때, 시스템은 아래와 같은 2단계 절차를 거친다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;-- 1단계: 주어진 기간 내의 해당 객실 유형 레코드를 확보&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_inventory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_inventory&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;roomTypeId&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hotel_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hotelId&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BETWEEN&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startDate&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;endDate&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 쿼리의 결과는 아래와 같다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;date&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;total_inventory&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;total_reserved&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2026-06-01&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;100&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;97&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2026-06-02&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;100&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;96&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2026-06-03&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;100&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;95&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;애플리케이션 레이어에서는 반환된 모든 일자별 레코드를 순회하며 아래 조건문을 검증한다.&lt;br /&gt;
요구 사항이었던 &lt;strong&gt;10% 초과 예약&lt;/strong&gt; 수식도 이 비즈니스 로직에 함께 녹여낼 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 일반적인 재고 검증 로직&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;numberOfRoomsToReserve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_inventory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 10% 초과 예약을 허용하는 비즈니스 적용 시&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;numberOfRoomsToReserve&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total_inventory&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;43-데이터-볼륨-관리와-수평-확장sharding&quot;&gt;4.3. 데이터 볼륨 관리와 수평 확장(Sharding)&lt;/h2&gt;

&lt;p&gt;5,000개 호텔에 각각 20개의 객실 유형이 있고, 2년(730일) 치 데이터를 미리 적재한다고 하면 이 테이블의 레코드 수는 약 &lt;strong&gt;7,300만 건(5,000개 호텔 * 20개 객실 유형 * 730일)&lt;/strong&gt;이 된다.&lt;br /&gt;
단일 데이터베이스 서버로도 저장 자체는 가능하지만, 단일 장애점(SPOF, Single-Point-Of-Failure)을 방지하고 트래픽을 분산하기 위해 수평 확장(Sharding)을 도입해야 한다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;#22-단계별-유저-이탈률을-고려한-페이지별-qps-예측&quot;&gt;2.2. 단계별 유저 이탈률을 고려한 페이지별 QPS 예측&lt;/a&gt;에서 계산한 시스템 전체의 평상 시 조회 및 쓰기 트래픽은 아래와 같았다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;객실 상세 조회: 300 QPS&lt;/li&gt;
  &lt;li&gt;예약 상세 확인: 30 QPS&lt;/li&gt;
  &lt;li&gt;최종 예약 완료: 3 QPS&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;전체 시스템 요청: 약 333 QPS&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;만일 호텔 시스템이 자체 웹사이트뿐 아니라, 전 세계의 수많은 여행 대행사(OTA, Online Travel Agency, 온라인 여행 대행사) API와 연동된다면 트래픽이 평소보다 
100배는 늘어날 수 있다. (333 * 100 = 약 30,000 QPS)&lt;br /&gt;
단일 MySQL 서버는 아무리 스펙을 높여도 초당 30,000건의 QPS를 혼자 감당할 수 없다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/shard.png&quot; alt=&quot;데이터베이스 샤딩&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이 시스템의 거의 모든 쿼리가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hotel_id&lt;/code&gt;를 조건절로 달고 다니기 때문에, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hotel_id&lt;/code&gt;를 샤딩 키로 채택하는 것이 가장 자연스럽다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash(hotel_id) % 16&lt;/code&gt;과 같은 방식으로 데이터베이스 부하를 16개의 샤드로 균등하게 분산하면 각 샤드가 받는 부하는 아래와 같다.&lt;/p&gt;

\[\text{각 샤드(DB 서버 1대)가 받는 부하} = \frac{\text{총 트래픽 (30,000 QPS)}}{\text{샤드 개수 (16개)}} = 1,875 \text{ QPS}\]

&lt;p&gt;1,875 QPS는 MySQL 서버 1대로도 충분히 안정적인 범위 내로 부하가 통제된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-상세-설계-2단계-분산-환경에서의-동시성concurrency&quot;&gt;5. 상세 설계 2단계: 분산 환경에서의 동시성(Concurrency)&lt;/h1&gt;

&lt;p&gt;호텔 예약 시스템의 가장 거대한 도전 과제는 바로 &lt;strong&gt;이중 예약(Double Booking)의 원천 차단&lt;/strong&gt;이다.&lt;br /&gt;
동시성 문제는 크게 두 가지 시나리오로 나누어 쪼개 보아야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;51-시나리오-a-동일-사용자가-예약-버튼을-여러-번-누르는-경우&quot;&gt;5.1. 시나리오 A: 동일 사용자가 예약 버튼을 여러 번 누르는 경우&lt;/h2&gt;

&lt;p&gt;네트워크가 지연될 때 사용자는 초조해하며 ‘예약하기’ 버튼을 연타하게 되고, 동일한 주문 요청이 중복해서 들어온다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/double.png&quot; alt=&quot;같은 고객의 이중 예약&quot; /&gt;&lt;/p&gt;

&lt;p&gt;클라이언트 측에서 버튼을 비활성화하는 조치는 자바스크립트 조작 등으로 쉽게 우회당하므로, 반드시 서버 측에서 &lt;strong&gt;멱등성 API&lt;/strong&gt;로 방어해야 한다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    actor 사용자 as 사용자
    participant 예약 서비스 as 예약 서비스

    activate 사용자
    사용자-&amp;gt;&amp;gt;예약 서비스: ① 예약 주문 생성
    activate 예약 서비스
    예약 서비스--&amp;gt;&amp;gt;사용자: ② 예약 주문서 표시 (reservation_id)
    deactivate 예약 서비스

    사용자-&amp;gt;&amp;gt;예약 서비스: ③-a 예약 제출 (reservation_id)
    activate 예약 서비스
    사용자-x예약 서비스: ③-b 예약 제출 (reservation_id)
    deactivate 예약 서비스
    
    %% Note to indicate the error condition and its cause %%
    Note right of 예약 서비스: 유일성 조건 위반 (reservation_id)
    deactivate 사용자
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;① 예약 주문 생성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;고객이 투숙 날짜, 인원 등의 세부 정보를 입력하고 ‘다음 단계’ 버튼을 누르면, 예약 서비스는 최종 결제 전에 &lt;strong&gt;예약 주문(Draft)을 생성&lt;/strong&gt;한다.&lt;/li&gt;
      &lt;li&gt;이 시점(=최종 예약 완료 버튼을 누르기 전)에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation&lt;/code&gt; 테이블에 데이터가 Insert 된다.&lt;/li&gt;
      &lt;li&gt;다만, 아직 결제를 하기 전이므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status&lt;/code&gt; 필드의 값은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PENDING_PAY(결제 대기)&lt;/code&gt; 혹은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DRAFT(초안)&lt;/code&gt; 상태로 저장된다.&lt;/li&gt;
      &lt;li&gt;이 과정이 선행되어야만 안전한 식별자를 발행할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;② 전역 유일 식별자 발행 및 주문서 반환&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;고객이 검토할 수 있도록 예약 주문서 정보를 반환하는데, 이 때 응답 데이터에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation_id&lt;/code&gt;를 함께 실어서 보낸다.&lt;/li&gt;
      &lt;li&gt;이 식별자는 분산 환경 전체에서 중복이 없는 전역적 유일성(Globally Unique)을 보장하는 ID여야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;③-a 검토 완료 후 예약 요청 전송(멱등 키 활용)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;고객이 주문서를 최종 확인하고 ‘예약 완료(결제)’ 버튼을 누르면, 2단계에서 받아 두었던 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation_id&lt;/code&gt;가 요청에 함께 포함되어 서버로 전송된다.&lt;/li&gt;
      &lt;li&gt;이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation_id&lt;/code&gt;가 바로 예약 테이블의 PK이자, 중복을 걸러내는 &lt;strong&gt;멱등 키&lt;/strong&gt; 역할을 하게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;③-b 중복 클릭 시 DB 유일성 제약 조건 위반 방어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;네트워크 지연으로 인해 사용자가 예약 완료 버튼을 한 번 더 누르는 바람에 똑같은 요청이 서버로 다시 전송되었다고 하자.&lt;/li&gt;
      &lt;li&gt;하지만 이미 ③-a에 의해 DB에는 해당 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation_id&lt;/code&gt;를 가진 레코드가 정상적으로 처리되고 있거나 상태가 변경된 상태이다.&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation_id&lt;/code&gt;는 예약 테이블의 PK이므로, 두 번째 요청이 들어와 Insert를 시도하는 순간 기본키 유일성 조건(Unique Constraint Violation)을 위반하게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/not_unique.png&quot; alt=&quot;유일성 조건 위반&quot; /&gt;&lt;/p&gt;

&lt;p&gt;결과적으로 DB 엔진 레벨에서 새로운 중복 레코드가 생성되는 것을 원천 차단해주기 때문에, 시스템은 아무리 버튼을 연타해도 단 한 번의 예약만 안전하게 성공시키는 
멱등성을 완벽히 보장할 수 있다.&lt;/p&gt;

&lt;p&gt;기술 표준 관점에서 &lt;strong&gt;멱등 키&lt;/strong&gt;는 ‘해당 요청이 고유한 의도를 가졌음’을 증명하는 독립적인 토큰(예: UUID)이면 무엇이든 가능하기 때문에 꼭 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation_id&lt;/code&gt;를 
멱등키로 사용하지 않아도 된다.&lt;br /&gt;
예를 들어 HTTP 헤더에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X-Idempotency-Key: f81d4fae-...&lt;/code&gt; 같은 임의의 UUID를 실어 보내고, 서버가 이 키를 Redis 같은 분산 캐시에 먼저 조회하여 
중복 요청을 차단하는 방식으로 설계해도 된다.&lt;br /&gt;
즉, 멱등성 구현 방법은 다양하지만 본 아키텍처에서는 &lt;strong&gt;어차피 생성해야 하는 엔티티의 PK(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation_id&lt;/code&gt;)가 존재하므로, 이를 멱등키로 재활용하는 것이 비용 효율적이고 직관적&lt;/strong&gt;이기 
때문에 선택한 것뿐이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;52-시나리오-b-여러-사용자가-같은-객실을-동시에-예약하려-하는-경우&quot;&gt;5.2. 시나리오 B: 여러 사용자가 같은 객실을 동시에 예약하려 하는 경우&lt;/h2&gt;

&lt;p&gt;동시성 제어에서 가장 해결하기 까다로운 영역은 바로 여러 사용자가 ‘동시에’ 같은 자원을 선점하려고 경쟁할 때 발생하는 경쟁 조건(Race Condition)이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/race.png&quot; alt=&quot;Race Condition&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이해를 돕기 위해 데이터베이스 트랜잭션 격리 수준이 가장 높은 수준인 &lt;a href=&quot;https://en.wikipedia.org/wiki/Serializability&quot;&gt;직렬화 가능 수준(Serializable)&lt;/a&gt;으로 설정되어 있지 않은 일반적인 환경을 가정해보자.&lt;br /&gt;
현재 상황은 total_inventory = 100, total_reserved = 99 라고 가정한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;① 사용자 1과 2가 동시에 예약 프로세스에 진입한다.&lt;/li&gt;
  &lt;li&gt;② 트랜잭션 2가 재고를 검증하기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(total_reserved + room_to_book) &amp;lt;= total_inventory&lt;/code&gt;인지 검사한다.
    &lt;ul&gt;
      &lt;li&gt;99+1 &amp;lt;= 100 이므로 True가 반환된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;③ 거의 동시에 트랜잭션 1도 동일한 재고 검증 쿼리를 실행한다.
    &lt;ul&gt;
      &lt;li&gt;트랜잭션 2가 아직 커밋을 안했기 때문에 데이터베이스 기준으로는 여전히 방이 하나 남은 상태이다.&lt;/li&gt;
      &lt;li&gt;따라서 트랜잭션 1에게도 True가 반환된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;④ 트랜잭션 1이 객실 예약을 처리하고 객실 예약 현황(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;total_reserved&lt;/code&gt;)을 1 증가시켜 값을 &lt;strong&gt;100&lt;/strong&gt;으로 갱신한다.&lt;/li&gt;
  &lt;li&gt;⑤ 그 직후 트랜잭션 2도 해당 객실 예약을 진행한다.
    &lt;ul&gt;
      &lt;li&gt;데이터베이스 ACID 속성 중 I(Isolation, 격리성)에 의하면, 모든 트랜잭션은 다른 트랜잭션과 무관하게 독립적으로 작업을 완료해야 한다.&lt;/li&gt;
      &lt;li&gt;즉, &lt;strong&gt;트랜잭션 1이 데이터를 변경했더라도 최종 완료(Commit)되기 전까지는 트랜잭션 2의 눈에 이 변경사항이 보이지 않는다.&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;트랜잭션 1의 변경 내용을 모르는 트랜잭션 2는 자신이 읽었던 데이터(99개 예약됨)를 기반으로 예약을 완료하고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;total_reserved&lt;/code&gt;의 값을 다시 &lt;strong&gt;100&lt;/strong&gt;으로 갱신한다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;결과적으로 2명의 예약이 겹치는 이중 예약이 발생&lt;/strong&gt;한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;⑥ 트랜잭션 1이 변경 사항을 성공적으로 Commit 한다.&lt;/li&gt;
  &lt;li&gt;⑦ 트랜잭션 2도 자신의 변경 사항을 Commit 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;이 문제를 해결하려면 어떤 형태로든 Lock을 활용해야 한다.&lt;br /&gt;
해결책을 알아보기 전에 객실 예약에 사용되는 SQL 질의문의 pseudo code부터 살펴 보자.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;단계&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;예약&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;가능&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;객실&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;현황&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;확인&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_inventory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_inventory&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;roomTypeId&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hotel_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hotelId&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;between&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startDate&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;endDate&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;단계에서&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;반환되는&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;모든&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;객실에&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;다음&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;사항&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;확인&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;numberOfRoomsToReserve&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;110&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_inventory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;Rollback&lt;/span&gt;
  &lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;#&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;단계&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;객실&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;예약&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_inventory&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;numberOfRoomsToReserve&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;roomTypeId&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;between&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;startDate&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;endDate&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;Commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 쿼리 흐름이 동시 다발적으로 호출될 때 데이터가 뒤틀리는 것을 막기 위해 데이터베이스 격리 수준에만 의존하지 말고, 
비관적 락, 낙관적 락, 혹은 데이터베이스 제약 조건을 아키텍처에 명시적으로 도입하여 이 타임라인의 뒤틀림을 강제로 바로 잡아주어야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;트랜잭션-격리-수준의-종류와-직렬화-가능-수준이란&quot;&gt;💡트랜잭션 격리 수준의 종류와 ‘직렬화 가능 수준’이란?&lt;/h3&gt;

&lt;p&gt;트랜잭션 격리 수준은 동시에 여러 트랜잭션이 처리될 때, 특정 트랜잭션이 변경한 데이터를 다른 트랜잭션이 볼 수 있도록 허용할지 말지 결정하는 규칙이다.&lt;/p&gt;

&lt;p&gt;ANSI/ISO SQL 표준 기준으로 트랜잭션 격리 수준(Isolation Level)은 크게 4가지로 나뉜다.&lt;br /&gt;
내려갈수록 격리성은 엄격해지지만 동시 처리 성능은 떨어진다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Read Uncommitted(미커밋 읽기)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;다른 트랜잭션이 커밋하지 않은 데이터도 읽을 수 있다.&lt;/li&gt;
      &lt;li&gt;존재하지 않는 데이터를 읽는 &lt;a href=&quot;https://assu10.github.io/dev/2023/09/10/springboot-database-4/#31-read-uncommitted-%EC%99%80-dirty-read&quot;&gt;Dirty Read&lt;/a&gt; 문제가 발생한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Read Committed(커밋 읽기)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;커밋이 완료된 데이터만 읽을 수 있다.&lt;/li&gt;
      &lt;li&gt;일반적인 RDBMS의 기본값이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#repeatable-read반복-가능-읽기&quot;&gt;&lt;strong&gt;Repeatable Read(반복 가능 읽기)&lt;/strong&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;하나의 트랜잭션 내에서 동일한 데이터를 여러 번 조회해도 항상 같은 결과를 보장한다.&lt;/li&gt;
      &lt;li&gt;MySQL InnoDB의 기본 격리 수준이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#serializable직렬화-가능-수준&quot;&gt;&lt;strong&gt;Serializable(직렬화 가능)&lt;/strong&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;가장 엄격하고 안전한 격리 수준이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;repeatable-read반복-가능-읽기&quot;&gt;💡Repeatable Read(반복 가능 읽기)&lt;/h3&gt;

&lt;p&gt;하나의 트랜잭션을 열고 똑같은 조회를 두 번 했는데, 그 사이에 값이 바뀌는 것이 이상할 수도 있다.&lt;br /&gt;
아래 Read Committed 일 때의 상황을 보자.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/repeatable.png&quot; alt=&quot;Read Committed&quot; /&gt;&lt;/p&gt;

&lt;p&gt;사용자 A는 분명 하나의 트랜잭션 안에서 똑같은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT&lt;/code&gt; 문을 두 번 날렸는데 T2 시점에는 &lt;strong&gt;1개&lt;/strong&gt;였던 재고가, T4 시점에는 &lt;strong&gt;0개&lt;/strong&gt;로 바뀌어버렸다.&lt;/p&gt;

&lt;p&gt;이처럼 다른 트랜잭션이 중간에 데이터를 변경하고 커밋을 해버리면, 내 트랜잭션 안으로 그 변경사항이 실시간으로 스며드는 현상을 &lt;strong&gt;반복 불가능한 읽기(Non-Repeatable Read)&lt;/strong&gt; 현상이라고 한다.&lt;/p&gt;

&lt;p&gt;Repeatable Read는 이 문제를 MVCC(Multi-Version Concurrency Control, 다중 버전 동시성 제어)로 해결한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;사용자 A가 트랜잭션을 시작한 순간(T1), 데이터베이스는 그 시점의 스냅샷을 찍어둔다.&lt;/li&gt;
  &lt;li&gt;중간에 사용자 B가 해당 데이터를 변경하여 &lt;strong&gt;커밋해도&lt;/strong&gt;, 사용자 A가 두 번째 조회(T4)를 할 때 DB는 롤백 세그먼트(Undo 로그)에 저장된 이전 버전의 데이터를 뒤져서 
처음과 똑같은 결과인 1개를 강제로 보여준다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;그럼 B가 데이터를 변경했는데도 A가 계속 1개 남았다고 잘못된 데이터를 보는 건 아닌가? 하는 생각이 들 수 있다.&lt;br /&gt;
맞다. 하지만 하나의 트랜잭션 안에서 &lt;strong&gt;데이터의 일관성&lt;/strong&gt;을 유지하기 위해 데이터베이스가 하는 격리 장치이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;serializable직렬화-가능-수준&quot;&gt;💡Serializable(직렬화 가능 수준)&lt;/h3&gt;

&lt;p&gt;여러 트랜잭션이 동시에 실행되더라도, 마치 &lt;strong&gt;동시성이 전혀 없이 한 줄로 서서 한 번에 하나씩 순차적으로 실행(Serial)되는 것과 같은 결과&lt;/strong&gt;를 보장하는 수준이다.&lt;br /&gt;
데이터베이스가 트랜잭션이 접근하는 모든 영역에 읽기/쓰기 Lock을 걸어버리기 때문에 데이터 정합성은 완벽하지만, 초당 처리량은 매우 낮아져서 대규모 트래픽을 처리해야 하는 
상용 아키텍처에서는 거의 사용되지 않는다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;521-방안-1-비관적-락pessimistic-lock&quot;&gt;5.2.1. 방안 1: 비관적 락(Pessimistic Lock)&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://www.ibm.com/docs/en/rational-clearquest/10.0.9?topic=clearquest-optimistic-pessimistic-record-locking&quot;&gt;비관적 락&lt;/a&gt;은 
데이터 충돌이 무조건 발생할 것이라고 가정하고, 레코드를 읽는 순간부터 다른 트랜잭션이 접근하지 못하도록 먼저 Lock을 거는 방식이다.&lt;br /&gt;
데이터베이스 레벨에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT ... FOR UPDATE&lt;/code&gt; 구문을 사용하여 구현한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/pessimistic.png&quot; alt=&quot;비관적 락&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림처럼 트랜잭션 1이 먼저 실행되었다면 다른 트랜잭션은 트랜잭션 1이 종료되기를 기다려야 한다.&lt;br /&gt;
트랜잭션 1이 끝나고 나면 예약된 객실 수는 100이 되므로 사용자 2는 객실 예약이 불가하다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;-- 트랜잭션 시작과 동시에 해당 일자 레코드에 배타적 락(X-Lock) 획득&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;START&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;TRANSACTION&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_inventory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_inventory&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hotel_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;111&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;RT01&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;2026-07-01&apos;&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;FOR&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- 애플리케이션에서 재고 확인 후 업데이트 진행 (위 pseudo code의 2단계)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_inventory&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hotel_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;111&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;RT01&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;2026-07-01&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COMMIT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;확실한 데이터 보호&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;내 트랜잭션(애플리케이션)이 수정하려고 변경 중이거나 이미 수정을 마친 데이터를, 다른 트랜잭션이 중간에 끼어들어 무단으로 조작하거나 갱신하는 일을 데이터베이스 레벨에서 원천 차단할 수 있음&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;쉬운 구현 난이도&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;모든 갱신 연산을 데이터베이스 엔진 레벨에서 직렬화(Serialization)하여 충돌을 막아주므로, 복잡한 실패 재시도 로직을 구현할 필요가 없다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;높은 경합 상황에 유리&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;데이터에 대한 경합(Contention)이 매우 심할 때, 낙관적 락처럼 롤백 후 재시도하는 비용을 쓰지 않고 차라리 한 줄로 세우는 것이 성능상 유용하다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Deadlock 위험&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;여러 레코드에 동시에 Lock을 걸면 트랜잭션 간에 자원을 물고 늘어지는 DeadLock이 발생할 수 있으며, 이를 피하는 애플리케이션 코드를 작성하는 것은 생각보다 까다롭다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;낮은 확장성&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;하나의 트랜잭션이 너무 오랫동안 락을 해제하지 않고 있으면, 동일한 자원에 접근하려는 모든 다른 트랜잭션은 대기 상태에 빠져 무한정 멈추게 된다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;성능 저하&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;특히 트랜잭션의 수명이 길거나(외부 API 연동 등) 많은 엔티티(여러 날짜의 객실 재고)에 연관된 경우, 커넥션 풀이 고갈되고, 데이터베이스 전체 성능에 심각한 영향을 준다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이러한 치명적인 성능 확장성 제약과 DeadLock 위험성 때문에, &lt;strong&gt;읽기 부하가 압도적인 호텔 예약 시스템 아키텍처에서 비관적 락 메커니즘은 권장하지 않는다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;비관적 락은 자원 경쟁이 치열하고, 결제 정합성이 무엇보다 중요하며, 실패 시 재시도 비용이 너무 막대한 시스템(예: 은행 계좌 이체, 증권 거래 시스템)에 적합하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;deadlock이-생기지-않는-애플리케이션-코드를-어떻게-짜야-할까&quot;&gt;💡DeadLock이 생기지 않는 애플리케이션 코드를 어떻게 짜야 할까?&lt;/h4&gt;

&lt;p&gt;비관적 락을 사용할 때 단점으로 지적된 교착 상태를 완벽히 차단하려면, 애플리케이션 레이어에서 아래 3가지 핵심 규칙을 준수해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Lock 획득 순서의 전역적 일관성 강제(Lock Ordering)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;교착 상태의 근본 원인은 서로 다른 트랜잭션이 자원을 서로 반대 순서로 점유하려고 하기 때문이다.&lt;/li&gt;
      &lt;li&gt;예) 트랜잭션 A는 락 1 → 락 2 시도, 트랜잭션 B는 락 2 → 락 1 시도&lt;/li&gt;
      &lt;li&gt;이를 막으려면 연박 예약(예: 1박 2일) 등으로 여러 레코드에 락을 걸 때, 애플리케이션 코드가 리스트를 &lt;strong&gt;항상 동일한 기준(예: 날짜 오름차순, 혹은 ID 정렬 순)으로 Sort 한 뒤 순차적으로 쿼리를 실행&lt;/strong&gt;하도록 강제해야 한다.&lt;/li&gt;
      &lt;li&gt;사용자 A는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7/1 → 7/2 → 7/3&lt;/code&gt; 순서로 락을 시도하고, 사용자 B는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7/3 → 7/2 → 7/1&lt;/code&gt; 순서로 역방향 락을 시도하면 서로가 서로의 자원을 물고 늘어지는 교착 상태에 빠진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Lock Timeout 지정 및 비차단 옵션 활용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;락을 얻지 못했을 때 무한정 대기하도록 두면 DeadLock 시 시스템이 완전히 마비된다.&lt;/li&gt;
      &lt;li&gt;데이터베이스 쿼리에 타임아웃을 명시하거나, MySQL의 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FOR UPDATE NOWAIT&lt;/code&gt;(락이 걸려있으면 즉시 에러 반환) 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FOR UPDATE SKIP LOCKED&lt;/code&gt;(이미 락이 걸린 행은 건너뜀) 같은 모던 SQL 옵션을 주어 애플리케이션이 제어권을 잃지 않도록 해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;트랜잭션 범위(Scope)와 점유 시간 최소화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;START TRANSACTION&lt;/code&gt;을 켜고 비관적 락을 획득한 뒤, 그 안에서 결제 PG사 API를 호출하거나 무거운 비즈니스 연산을 처리하는 것은 DeadLock을 유발하는 행위이다.&lt;/li&gt;
      &lt;li&gt;데이터 계산, 파싱, 외부 통신 등은 트랜잭션 외부에서 미리 다 끝내놓고, &lt;strong&gt;오직 DB 레코드를 검증하고 업데이트하는 ms 단위의 찰나의 순간에만 트랜잭션을 열고 락을 건 뒤 즉시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COMMIT&lt;/code&gt;으로 자원을 반환&lt;/strong&gt;해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;522-방안-2-낙관적-락optimistic-lock&quot;&gt;5.2.2. 방안 2: 낙관적 락(Optimistic Lock)&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Optimistic_concurrency_control&quot;&gt;낙관적 락&lt;/a&gt;은 여러 사용자가 동시에 같은 자원을 갱신하려 시도하는 것을 허용한다.&lt;/p&gt;

&lt;p&gt;데이터베이스에 물리적인 Lock을 걸지 않는 대신, 애플리케이션 레벨에서 데이터의 버전 번호(version)나 타임스탬프를 비교하여 정합성을 유지하는 방식이다.&lt;br /&gt;
분산 시스템에서 서버 간 시계가 미세하게 어긋날 수 있으므로 일반적으로 버전 번호를 사용하는 것이 훨씬 안정적이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/optimistic.png&quot; alt=&quot;낙관적 락&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;버전 열 추가&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;테이블에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;version&lt;/code&gt;이라는 새 열(Column)을 추가한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;버전 읽기&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 레코드를 수정하기 전에, 애플리케이션은 해당 레코드의 잔여 객실 수와 함께 버전 번호를 먼저 읽어온다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;버전 가산&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 레코드를 갱신할 때, 애플리케이션은 읽어왔던 버전 번호에 1을 더한 후 테이블에 기록할 준비를 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;유효성 검사 및 커밋&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;업데이트 시점에 유효성 검사를 수행한다.&lt;/li&gt;
      &lt;li&gt;즉, 내가 기록하려는 다음 버전 번호가 데이터베이스에 저장된 현재 버전보다 정확히 1만큼 큰 값이어야 한다.&lt;/li&gt;
      &lt;li&gt;만일 이 유효성 검사가 실패하면 트랜잭션은 rollback되고, 사용자는 2단계(데이터 다시 읽기)부터 모든 절차를 처음부터 재시도해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;-- 1. 먼저 현재 버전 번호를 조회 (예: 당시 조회된 version이 1이라고 가정)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_inventory&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;version&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_inventory&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hotel_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;111&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;RT01&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;2026-07-01&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;-- 2. 조건절에 내가 조회했던 당시의 버전 번호를 명시하며 갱신 시도&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;UPDATE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_inventory&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SET&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hotel_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;111&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;RT01&apos;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;2026-07-01&apos;&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;-- 원자적 검증 구간&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;애플리케이션 레벨의 정합성 보장&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;애플리케이션이 이미 유효하지 않게 된(다른 사람이 선점한) 구버전 데이터를 기반으로 편집하고 갱신하는 일은 막아준다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;자원 점유 최소화&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;데이터베이스에 무겁게 Lock을 걸 필요가 없다.&lt;/li&gt;
          &lt;li&gt;버전 번호를 통해 데이터 일관성을 유지할 책임이 온전히 애플리케이션에 있기 때문에 DB 엔진의 가용성이 높아진다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;낮은 경쟁 상황에 최적&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;데이터에 대한 경쟁이 치열하지 않은 상황에 매우 적합하다. 락을 관리하는 오버헤드가 전혀 없기 때문이다.
            &lt;ul&gt;
              &lt;li&gt;반면, 대형 콘서트 티켓팅처럼 경쟁이 치열한 상황에서는 수많은 트랜잭션이 무한 재시도를 하므로 애플리케이션 자원을 고갈시키기 때문에 절대 권장하지 않는다.&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;높은 경쟁 상황에서의 성능 저하&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;데이터에 대한 경쟁이 치열한 성수기나 핫딜 타임에는 연쇄적인 롤백과 재시도 폭풍 때문에 시스템 성능이 기하급수적으로 떨어진다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;호텔 예약 시스템은 초당 최종 예약 처리(3 QPS) 자체의 경쟁률이 주식 거래나 콘서트 티켓팅처럼 초 단위로 치열하게 엉키는 도메인이 아니기 때문에, 
락 오버헤드가 없는 낙관적 락이 아주 훌륭한 선택지가 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;🚨동시성 수준이 아주 높을 때 낙관적 락이 성능이 급격히 나빠지는 이유&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;낙관적 락은 일반적으로 비관적 락보다 빠르다. 데이터베이스 자체에 무거운 Lock을 걸지 않기 때문이다.&lt;br /&gt;
하지만 &lt;strong&gt;동시성 수준이 아주 높으면(High Contention) 성능이 급격하게 나빠지는 치명적인 부작용&lt;/strong&gt;이 있다. 왜 그럴까?&lt;/p&gt;

&lt;p&gt;많은 클라이언트가 동일한 객실을 동시에 예약하려고 몰려드는 상황을 생각해보자.&lt;br /&gt;
낙관적 락은 읽기 연산을 제한하지 않으므로, &lt;strong&gt;모든 클라이언트는 아무런 제약 없이 똑같은 잔여 객실 수와 똑같은 버전 번호(예: version=1)를 동시에 취득&lt;/strong&gt;하게 된다.&lt;/p&gt;

&lt;p&gt;그러나 실제로 데이터베이스에 먼저 도달하여 &lt;strong&gt;버전 번호 갱신(version=2)에 성공하는 클라이언트는 오직 하나뿐&lt;/strong&gt;이다.&lt;br /&gt;
그 직후 도달한 다른 모든 클라이언트들은 데이터베이스의 버전이 이미 2로 올라가 버렸기 때문에 ‘버전 번호 검사 실패’ 메시지를 받으며 트랜잭션이 rollback 된다.&lt;/p&gt;

&lt;p&gt;실패한 클라이언트들은 예약을 완료하기 위해 처음부터 다시 데이터를 읽고 재시도해야 한다.&lt;br /&gt;
하지만 다음번 역시 단 한 명의 클라이언트만 성공하고, 나머지 클라이언트들은 또 다시 실패하여 재시도 궤도에 진입한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💥결과&lt;/strong&gt;&lt;br /&gt;
최종 결과 자체는 데이터 뒤틀림 없이 정확하겠지만, 백엔드에서는 수많은 쓰기 실패와 무한 재시도 루프(Retry Storm)가 발생하여 서버 자원을 극심하게 고갈시킨다.&lt;br /&gt;
유저 역시 화면이 뱅글뱅글 돌며 반복되는 재시도를 겪어야 하므로 &lt;strong&gt;매우 불쾌하고 짜증나는 경험&lt;/strong&gt;을 하게 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;523-데이터베이스-제약-조건db-constraint&quot;&gt;5.2.3. 데이터베이스 제약 조건(DB Constraint)&lt;/h3&gt;

&lt;p&gt;이 접근법은 데이터베이스 엔진 자체의 물리적인 무결성 제약 조건을 활용하여 동시성을 제어하는 방식으로, 낙관적 락과 아주 유사한 메커니즘으로 동작한다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_type_inventory&lt;/code&gt; 테이블에 아래와 같이 잔여 객실 수가 음수가 되지 않도록 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CHECK&lt;/code&gt; 제약 조건을 추가하여 구현한다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;-- 테이블 생성 시 제약 조건 명시&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;room_type_inventory&lt;/span&gt; 
&lt;span class=&quot;k&quot;&gt;ADD&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;CONSTRAINT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;check_room_count&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;CHECK&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;total_inventory&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;total_reserved&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/db.png&quot; alt=&quot;데이터베이스 제약 조건&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림처럼 사용자 1과 사용자 2가 동시에 남은 1개의 객실을 예약하려고 경합하는 상황을 예로 들어보자.&lt;br /&gt;
사용자 1의 트랜잭션이 먼저 성공하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;total_reserved&lt;/code&gt;가 100이 된 상태에서, 곧바로 사용자 2의 트랜잭션이 똑같은 수정을 시도하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;total_reserved&lt;/code&gt;가 101이 되려고 할 것이다.&lt;/p&gt;

&lt;p&gt;이 때 데이터베이스 엔진은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;total_inventory(100) - total_reserved(101) &amp;gt;= 0&lt;/code&gt; 이라는 제약 조건을 위배했음을 감지하고, 
&lt;strong&gt;사용자 2의 트랜잭션을 중단시키고 롤백&lt;/strong&gt;처리한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;매우 쉬운 구현&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;애플리케이션 레이어에서 복잡한 버전 관리나 Lock을 관리할 필요 없이, DB에 제약 조건 한 줄만 선언하면 되므로 구현이 압도적으로 쉽다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;낮은 경쟁 상황에 유용&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;낙관적 락과 마찬가지로 데이터에 대한 경쟁이 심하지 않을 때 깔끔하고 가볍게 잘 동작한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;치열한 경쟁 시 사용자 경험 저하&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;낙관적 락이 가진 문제점과 동일하게, 데이터 경합이 심한 상황에서는 실패하여 튕겨나가는 연산 수가 엄청나게 늘어난다.&lt;/li&gt;
          &lt;li&gt;사용자는 화면 상에서 분명 객실이 남아있는 것을 보고 시도했음에도, 정작 최종 단계에서 ‘객실이 없다’는 롤백 응답을 마주하게 되어 서비스 경험이 나빠질 수 있다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;버전 통제(형상 관리)의 어려움&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;데이터베이스 제약 조건은 애플리케이션 소스 코드와 달라서 버전을 통제하기가 매우 까다롭다.
            &lt;ul&gt;
              &lt;li&gt;여기서 말하는 버전은 낙관적 락의 version 컬럼이 아니라, 애플리케이션 소스 코드의 ‘Git Version Control’과 ‘형상 마이그레이션’을 뜻한다.&lt;/li&gt;
              &lt;li&gt;코드로 비즈니스 로직을 짜면 Git에 커밋 이력이 남아 문제가 생겼을 때 이전 코드로 롤백하기가 쉽다.&lt;/li&gt;
              &lt;li&gt;하지만 비즈니스 검증 규칙(재고가 0 이상이어야 한다는 룰)을 데이터베이스 엔진 내부의 CHECK 제약 조건으로 고정해 버리면, 이 규칙을 바꾸고 싶을 때마다 DB에 직접 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALTER TABLE&lt;/code&gt; DDL 문을 날려 마이그레이션을 해야 한다.&lt;/li&gt;
              &lt;li&gt;만약 서버는 구버전인데 DB 제약 조건만 신버전으로 먼저 반영되거나, 혹은 반대의 상황이 발생하면 인프라 배포 동기화가 깨지면서 대형 장애로 이어질 수 있다.&lt;/li&gt;
              &lt;li&gt;&lt;strong&gt;“비즈니스 로직의 통제권이 Git 중심의 애플리케이션 레이어를 벗어나 DB 서버로 넘어가 버리기 때문에 버전 관리 및 인프라 통제가 어려워진다.”&lt;/strong&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;데이터베이스 이식성 저하&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;모든 DBMS 제품군이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CHECK&lt;/code&gt; 제약 조건을 완벽하게 허용하거나 동일하게 지원하는 것은 아니다.&lt;/li&gt;
          &lt;li&gt;따라서 향후 비즈니스 확장으로 인해 데이터베이스 제품군을 다른 제품으로 교체하려고 할 때 이 마이그레이션 제약이 걸림돌이 될 수 있다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 접근법은 구현이 매우 쉽고, 호텔 예약 시스템의 특성상 평상시에는 데이터에 대한 경쟁이 그리 심하지 않기 때문에 &lt;strong&gt;호텔 대규모 분산 시스템 환경에서 도입을 고려하기에 좋은 선택지 중 하나&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-상세-설계-3단계-시스템-규모-확장&quot;&gt;6. 상세 설계 3단계: 시스템 규모 확장&lt;/h1&gt;

&lt;h2 id=&quot;61-데이터베이스-샤딩&quot;&gt;6.1. 데이터베이스 샤딩&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;#43-데이터-볼륨-관리와-수평-확장sharding&quot;&gt;4.3. 데이터 볼륨 관리와 수평 확장(Sharding)&lt;/a&gt; 참고.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;62-redis-캐시-계층-도입과-데이터-동기화-아키텍처&quot;&gt;6.2. Redis 캐시 계층 도입과 데이터 동기화 아키텍처&lt;/h2&gt;

&lt;p&gt;호텔 잔여 객실 데이터는 오직 ‘현재와 미래의 데이터’만 의미가 있다.&lt;br /&gt;
고객이 과거의 날짜를 예약하려 하지는 않기 때문이다.&lt;br /&gt;
따라서 영구 저장할 필요가 없고, 일정 시간이 지나면 자동으로 사라져도 좋은 가용 객실 데이터의 특성상 &lt;strong&gt;인메모리 캐시 시스템인 Redis&lt;/strong&gt;를 도입하기에 좋다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;redis의-lru란&quot;&gt;💡Redis의 LRU란?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;LRU(Least Recently Used)는 ‘가장 오랫동안 사용되지 않은 데이터를 메모리에서 우선적으로 퇴출하는 알고리즘’&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;캐시 메모리가 가득 찼을 때, Redis는 최근에 사용(조회/수정)된 적이 있는 데이터들은 그대로 보존하고, 수일간 아무도 이용하지 않은 낡은 호텔 재고 데이터부터 자동으로 
삭제하여 빈 공간을 확보한다.&lt;br /&gt;
여기에 &lt;strong&gt;TTL&lt;/strong&gt; 설정을 결합하여 과거의 날짜 데이터를 자동으로 소멸시키면, 안정된 메모리 자원을 실시간으로 가장 인기 있는 핵심 데이터 위주로 최적화하여 유지할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/cache.png&quot; alt=&quot;캐시&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;컴포넌트별 핵심 역할 분담&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;예약 서비스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시스템의 메인 인프라로서 아래와 같은 핵심 &lt;strong&gt;잔여 객실 관리 API&lt;/strong&gt;를 백엔드에 제공한다.
        &lt;ul&gt;
          &lt;li&gt;지정된 호텔, 객실 유형, 주어진 날짜 범위에 대해 현재 이용 가능한 객실의 수를 질의하는 엔드포인트 제공&lt;/li&gt;
          &lt;li&gt;고객이 예약을 완료하면 가용 재고를 확인한 후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_type_inventory&lt;/code&gt; 테이블의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;total_reserved&lt;/code&gt; &lt;strong&gt;값을 1 증가&lt;/strong&gt;시키는 트랜잭션 처리&lt;/li&gt;
          &lt;li&gt;고객이 예약을 취소하면 즉시 취소 이벤트를 수신하여 잔여 객실 수를 다시 원복(갱신)하는 로직 수행&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;잔여 객실 서비스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;잔여 객실 관리에 필요한 모든 읽기(조회) 질의를 데이터베이스가 아닌 &lt;strong&gt;Redis 인메모리 캐시 계층&lt;/strong&gt;으로 완전히 이관하여 처리&lt;/li&gt;
      &lt;li&gt;빠른 조회를 위해 2년 이내의 모든 미래 날짜에 대한 가용 객실 데이터를 사전에 레디스 캐시에 저장해두어야 한다.
        &lt;ul&gt;
          &lt;li&gt;Key: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hotelId_roomtypeId_{date}&lt;/code&gt;, 예) 111_RT001_20260701&lt;/li&gt;
          &lt;li&gt;Value: 주어진 hotelId, 객실 유형 id, 날짜에 맞는 &lt;strong&gt;잔여 객실 수&lt;/strong&gt;로 실시간 차감 및 복구 대상&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;잔여 객실 데이터베이스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;최종 재고 데이터의 영속성을 보장하는 마스터 저장소(SSOT, Single Source of Truth)이다.&lt;/li&gt;
      &lt;li&gt;캐시가 감당하지 못하는 최종 쓰기 트랜잭션과 유효성 검증을 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;잔여 객실 캐시 서비스 및 데이터 접근 패턴&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;캐시 데이터 구조&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Key: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hotelId_roomtypeId_{date}&lt;/code&gt;, 예) 111_RT001_20260701&lt;/li&gt;
      &lt;li&gt;Value: 주어진 hotelId, 객실 유형 id, 날짜에 맞는 &lt;strong&gt;잔여 객실 수&lt;/strong&gt;로 실시간 차감 및 복구 대상&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;조회 흐름&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;호텔 예약 시스템의 읽기 연산 빈도는 쓰기보다 훨씬 압도적이다.&lt;/li&gt;
      &lt;li&gt;잔여 객실을 조회하는 99%의 요청은 메인 DB가 아닌 메모리(Redis)단에서 즉시 반환되어 높은 성능을 보장한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;갱신 흐름(동기화 이슈 해결)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 객실을 최종 예약하면 데이터베이스의 재고 데이터가 먼저 갱신되고, 캐시 시스템에는 &lt;strong&gt;비동기적으로 변경 내역이 반영&lt;/strong&gt;된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;cdc와-debezium이란&quot;&gt;💡CDC와 Debezium이란?&lt;/h3&gt;

&lt;p&gt;캐시 계층을 추가하면 시스템의 확장성과 처리량은 대폭 증가하지만, 데이터베이스와 캐시 사이의 데이터 일관성 유지에 관한 문제를 직면하게 된다.&lt;br /&gt;
데이터베이스와 캐시 간의 일관성을 맞추기 위해 애플리케이션 코드에 ‘DB 저장 후 Redis 수정’ 로직을 집어넣으면 소스 코드가 복잡해지고 트랜잭션이 무거워진다.&lt;br /&gt;
&lt;a href=&quot;https://docs.oracle.com/cd/B10500_01/server.920/a96520/cdc.htm&quot;&gt;&lt;strong&gt;CDC(Change Data Capture) 메커니즘&lt;/strong&gt;&lt;/a&gt;은 이를 해결해준다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;예약 서비스] ──&amp;gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;잔여객실 DB]
                         │ &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Binlog 기록&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
                         ▼
                  &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Debezium 커넥터] ──&amp;gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Kafka] ──&amp;gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;Redis 캐시 갱신]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;보편적으로 많이 사용되는 오픈소스 솔루션인 &lt;a href=&quot;https://debezium.io/&quot;&gt;드베지움(Debezium)&lt;/a&gt;을 활용하여 데이터베이스의 바이너리 로그(Binlog) 변화를 실시간으로 감지(Capture)한다.&lt;br /&gt;
가용 재고가 차감되는 순간 이 변경 이벤트를 메시지 큐(Kafka)를 통해 Redis 캐시 시스템에 전파하여 자동으로 싱크를 맞춘다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;캐시-지연으로-인한-데이터-불일치의-해답&quot;&gt;💡캐시 지연으로 인한 데이터 불일치의 해답&lt;/h3&gt;

&lt;p&gt;비동기 갱신 특성상 DB에는 방이 없는데 캐시에는 아직 방이 1개 남아있다고 나오는 짧은 찰나의 ‘불일치 구간’이 발생할 수 있다.&lt;br /&gt;
결론부터 말하자면 &lt;strong&gt;비즈니스적으로 전혀 문제가 되지 않는다. 최종 방어선인 메인 데이터베이스가 존재하기 때문&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;캐시 불일치를 보고 사용자가 ‘예약하기’ 버튼을 누르면 요청이 메인 데이터베이스(샤드)에 도달한다.&lt;br /&gt;
이 때 DB 레벨에서 앞서 구현한 &lt;a href=&quot;#52-시나리오-b-여러-사용자가-같은-객실을-동시에-예약하려-하는-경우&quot;&gt;5.2. 시나리오 B: 여러 사용자가 같은 객실을 동시에 예약하려 하는 경우&lt;/a&gt; 절의 
유효성 검사(낙관적 락 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CHECK&lt;/code&gt; 제약 조건)가 다시 한번 수행된다.&lt;br /&gt;
실제 남은 객실이 없음이 확인되면 트랜잭션은 즉시 롤백되고, 클라이언트에게 ‘남은 객실 없음’ 오류 메시지를 반환해주어 데이터의 정합성이 유지된다.&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;캐시 계층 도입&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;읽기 질의를 메모리에서 초고속으로 실행하므로 폭발적인 대규모 트래픽 속에서도 높은 성능과 높은 확장성을 보장&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;DB와 캐시 사이의 일관성이 아주 잠깐 깨질 수 있으므로, 이러한 불일치 상황이 사용자 경험에 미칠 영향을 신중하게 고려하여 최종 DB 레벨의 밸런싱 검증 코드를 반드시 매핑해두어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;63-마이크로서비스-간-데이터-일관성-제어&quot;&gt;6.3. 마이크로서비스 간 데이터 일관성 제어&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://microservices.io/patterns/monolithic.html&quot;&gt;MSA&lt;/a&gt;를 설계할 때 가장 흔히 범하는 오류는 ‘교조주의적(Dogmatic) 접근’에 빠지는 것이다.&lt;br /&gt;
교조주의적 MSA란 ‘마이크로서비스는 무조건 자신만의 독자적인 독립 데이터베이스를 가져야만 해’ 라고 기계적으로 아키텍처를 분리해 버리는 것을 말한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/msa.png&quot; alt=&quot;모노리스 vs MSA&quot; /&gt;&lt;/p&gt;

&lt;p&gt;만일 이 원칙을 고집하여 ‘예약 서비스’와 ‘잔여 객실 서비스’의 데이터베이스를 물리적으로 쪼개버리면, 논리적으로는 하나의 원자적 연산이어야 할 예약 처리가 여러 데이터베이스에 걸쳐 
분산 실행되는 &lt;strong&gt;분산 트랜잭션&lt;/strong&gt;이 생겨버린다.&lt;/p&gt;

&lt;p&gt;전통적인 모노리스 아키텍처의 경우, 아래 그림처럼 데이터베이스를 공유하므로 단일 트랜잭션 하나로 완벽한 ACID 속성을 만족시킬 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/mono.png&quot; alt=&quot;모노리스 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;p&gt;하지만 교조주의적 MSA 환경에서는 아래 그림처럼 하나의 예약 요청을 처리하기 위해 여러 서비스의 독립 DB가 엮이게 된다.&lt;br /&gt;
하나의 트랜잭션으로 데이터 일관성을 보증하는 기법을 사용할 수 없다는 뜻이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/msa2.png&quot; alt=&quot;MSA 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;p&gt;만약 예약 서비스 DB 갱신을 성공했는데 잔여 객실 서비스 DB 갱신 연산이 네트워크 장애로 실패한다면 어떻게 될까?&lt;br /&gt;
잔여 객실 DB에 기록된 예약 객실 수는 원래 값으로 롤백되어야 하지만, 단일 트랜잭션이 아니기 때문에 자동으로 되돌릴 방법이 없다.&lt;br /&gt;
실패 시 데이터 불일치 문제가 발생할 수 있는 실행 경로가 너무 많아지는 것이다.&lt;/p&gt;

&lt;p&gt;이를 해결하기 위해 분산 환경에서 업계가 주로 사용하는 두 가지 트랜잭션 관리 기법을 알아보자.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;631-방안-1-2단계-커밋2-phase-commit-2pc&quot;&gt;6.3.1. 방안 1: 2단계 커밋(2-Phase Commit, 2PC)&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Two-phase_commit_protocol&quot;&gt;2PC&lt;/a&gt;는 여러 물리적인 서버 노드에 걸쳐진 분산 데이터베이스 환경에서 전체 트랜잭션의 
원자성을 보장하기 위해 고안된 전통적인 프로토콜이다.&lt;br /&gt;
전체 과정을 조율하는 조정자(Coordinator)의 지휘 아래 1단계(준비-Prepare)와 2단계(커밋-Commit)로 나누어 모든 노드가 다 함께 성공하든가, 다 함께 실패하도록 강제한다.&lt;/p&gt;

&lt;p&gt;여기서 말하는 노드는 분산 네트워크 아키텍처상에서 독립적으로 실행되는 ‘개별 서버’ 혹은 ‘개별 데이터베이스 인스턴스’를 뜻한다.&lt;/p&gt;

&lt;p&gt;2PC는 비중단 실행(Non-Blocking)이 가능한 프로토콜이 아니다.(Blocking Protocol)&lt;br /&gt;
‘비중단 실행’이란 어떤 자식 노드 하나가 갑자기 장애가 나도, 남은 시스템들은 멈추지 않고 자기 할 일을 계속 이어나갈 수 있는 구조를 말한다.&lt;/p&gt;

&lt;p&gt;반면, 2PC는 치명적인 &lt;strong&gt;블로킹 프로토콜&lt;/strong&gt;이다.&lt;br /&gt;
1단계 준비를 마치고 2단계 최종 승인을 기다리던 도중, 조정자 서버가 갑자기 다운되어 버리면, 모든 자식 노드(데이터베이스 서버들)는 커밋을 할지 롤백을 할지 결정을 
내리지 못한 채 Lock을 쥔 상태로 영원히 대기 상태에 멈춰버린다.(Blocking)&lt;br /&gt;
이 장애가 복구될 때까지 다른 모든 사용자들의 예약 요청도 중단되므로 성능상 커다란 단점이 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;632-방안-2-사가-패턴saga-pattern&quot;&gt;6.3.2. 방안 2: 사가 패턴(Saga Pattern)&lt;/h3&gt;

&lt;p&gt;2PC의 성능 한계를 극복하기 위해 모던 MSA에서 가장 애용되는 기법이 바로 &lt;a href=&quot;https://microservices.io/patterns/data/saga.html&quot;&gt;&lt;strong&gt;사가(Saga) 패턴&lt;/strong&gt;&lt;/a&gt;이다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;a href=&quot;https://assu10.github.io/dev/2024/10/05/ddd-communication-pattern/#22-%EC%82%AC%EA%B0%80saga-%EC%97%AC%EB%9F%AC-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%97%90-%EA%B1%B8%EC%B9%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81&quot;&gt;2.2. 사가(saga): 여러 트랜잭션에 걸친 비즈니스 로직&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;사가는 분산 트랜잭션을 하나의 커다란 덩어리로 묶지 않고, 각 노드에 국지적으로 발생하는 로컬 트랜잭션을 체인처럼 순차적으로 연결한 비즈니스 흐름이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;단계 1: 예약 생성] ── &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;성공 이벤트 발행&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; ──&amp;gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;단계 2: 재고 차감]
        │                                      │
        └─ &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;만약 실패 시!&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &amp;lt;── &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;보상 트랜잭션 실행] ──┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;각각의 로컬 트랜잭션은 자신의 데이터베이스에 값을 반영한 뒤 ‘완료 이벤트’를 발행하여 다음 단계의 서비스를 트리거한다.&lt;/p&gt;

&lt;p&gt;만약 마지막 단계인 재고 차감 도중 에러가 발생하면, 사가는 시스템을 거꾸로 거슬러 올라가며 이전에 성공했던 단계들의 결과를 전부 물리적으로 되돌리는 보상 트랜잭션들을 
순차적으로 실행하여 데이터를 원복한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;분산 트랜잭션을 완벽하게 제어하기 위해 2PC나 사가 패턴을 도입하면 아키텍처의 설계 복잡성과 인프라 관리 비용이 기하급수적으로 늘어난다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“과연 마이크로서비스의 독립 DB 원칙을 지키기 위해 이 거대한 복잡성을 감당할 가치가 있는가?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;본 설계안에서는 &lt;strong&gt;그만한 가치가 없다&lt;/strong&gt;고 판단한다.&lt;br /&gt;
대신 교조주의적 MSA의 고정관념을 깨고 예약 서비스가 예약 처리와 잔여 객실 재고 관리를 모두 전담하도록 설계하였다.&lt;br /&gt;
그리고 예약 테이블(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation&lt;/code&gt;)과 재고 테이블(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_type_inventory&lt;/code&gt;)을 동일한 RDBMS(샤드 데이터베이스) 공간에 함께 저장하는 ‘실용적인 하이브리드 접근법’을 선택하였다.&lt;/p&gt;

&lt;p&gt;이렇게 구조를 모아두는 것만으로 복잡한 분산 트랜잭션 기술 없이도 단일 RDBMS 고유의 강력한 ACID 속성과 로컬 트랜잭션을 온전히 활용할 수 있게 되며, 
분산 환경에서 발생하는 수많은 데이터 일관성 난제들을 깔끔하게 해결할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;7-마무리-및-요약&quot;&gt;7. 마무리 및 요약&lt;/h1&gt;

&lt;h2 id=&quot;71-호텔-예약-시스템-핵심-아키텍처-요약&quot;&gt;7.1. 호텔 예약 시스템 핵심 아키텍처 요약&lt;/h2&gt;

&lt;p&gt;이번 설계에서 도출한 핵심 아키텍처 결정 사항(Design Decisions)은 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0701/mindmap.png&quot; alt=&quot;마인드 맵&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;도메인 중심의 데이터 모델 개선&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_id&lt;/code&gt; 기반의 물리적 예약 구조에서 벗어나, 비즈니스 현실에 맞춘 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_type_id&lt;/code&gt; 중심의 재고 관리 테이블(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;room_type_inventory&lt;/code&gt;)을 도출하고 10% 초과 예약 공식 처리&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;철저한 중복 요청 방어(Idempotency)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자 단의 ‘따닥 클릭’으로 인한 이중 결제를 막기 위해, 주문서 생성 시점부터 전역 유일 식별자인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reservation_id&lt;/code&gt;를 발행하여 데이터베이스 유일성 제약 조건(PK)과 매핑하는 멱등성 API 구축&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;동시성 선점 경쟁 제어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;마지막 남은 객실 하나를 두고 수많은 트랜잭션이 충돌할 때, 확장성을 저해하는 비관적 락 대신 락 오버헤드가 없는 낙관적 락(Version 검증)과 DB 제약 조건(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CHECK&lt;/code&gt; 무결성)을 조합하여 시스템 가용성 극대화&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;하이브리드 MSA와 데이터 일관성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;서비스 간 데이터 정합성을 맞추기 위해 무리하게 분산 트랜잭션(2PC, Saga 패턴)을 도입하는 교조주의적 접근을 지양&lt;/li&gt;
      &lt;li&gt;대신 &lt;strong&gt;예약과 재고 데이터를 동일한 RDBMS 샤드 내에 배치&lt;/strong&gt;하여 단일 트랜잭션(ACID)의 이점을 온전히 누리는 실용주의적 아키텍처 선택&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;초고속 캐싱과 CDC 동기화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;OTA 연동으로 인한 수만 QPS의 읽기 폭탄을 방어하기 위해 &lt;strong&gt;Redis 인메모리 캐시 계층&lt;/strong&gt;을 앞단에 세우고, CDC(Change Data Capture) 솔루션인 드베지움(Debezium)을 통해 DB와 캐시 간의 싱크를 비동기로 연결&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;72-상용-솔루션-시장-동향&quot;&gt;7.2. 상용 솔루션 시장 동향&lt;/h2&gt;

&lt;p&gt;이런 설계로 만들어진 시스템이 이미 상용화되어 기성 제품으로 팔고 있는 것이 있을까? 당연히 있다.&lt;/p&gt;

&lt;p&gt;여기서 설계한 핵심 엔진들을 &lt;strong&gt;PMS(Property Management System, 자산 관리 시스템)&lt;/strong&gt; 및 &lt;strong&gt;CRS(Central Reservation System, 중앙 예약 시스템)&lt;/strong&gt; 라는 도메인 용어로 부르며, 
&lt;strong&gt;이미 전 세계 표준으로 자리 잡은 거대 상용 플랫폼들이 시장을 지배&lt;/strong&gt;하고 있다.&lt;/p&gt;

&lt;p&gt;직접 구축하지 않고 엔터프라이즈급 기성 제품(COTS, Commercial Off-The-Shelf)이나 SaaS를 도입할 때 검토할 수 있는 대표적인 글로벌 상용 제품은 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Oracle Hospitality OPERA Cloud (오라클 오페라)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시장 위치: 글로벌 5성급 체인 호텔(메리어트, 하얏트, 힐튼 등)의 약 80% 이상이 사용하는 전 세계 PMS 시장의 절대 강자이자 표준&lt;/li&gt;
      &lt;li&gt;특징
        &lt;ul&gt;
          &lt;li&gt;여기서 고민했던 대규모 분산 객실 재고 관리, 체크인 시점의 실시간 방 배정(Room Assignment) 알고리즘, 그리고 강력한 데이터베이스 정합성 기능이 
오라클 DB의 강력한 트랜잭션 엔진 위에서 완벽하게 구현되어 있음&lt;/li&gt;
          &lt;li&gt;다만 시스템이 다소 무겁고 구축 비용이 상상을 초월한다는 단점이 있음&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Sabre SynXis / Amadeus CRS (세이버 신지스 / 아마데우스)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시장 위치: 전 세계 항공권 및 호텔 가용 재고를 실시간으로 중계하는 글로벌 유통 시스템(GDS) 연동의 핵심 코어이자 대표적인 중앙 예약 시스템(CRS) 솔루션&lt;/li&gt;
      &lt;li&gt;특징
        &lt;ul&gt;
          &lt;li&gt;“부킹닷컴, 아고다, 익스피디아 등 수많은 글로벌 OTA 채널과 실시간으로 연동되어 발생하는 초고부하 트래픽”을 실제로 완벽하게 받아내고 분산 처리해 주는 엔진들&lt;/li&gt;
          &lt;li&gt;채널 매니저(CMS) 기능을 내장하여 실시간 오버부킹 제어와 유동적 요금제(Dynamic Pricing) 동기화를 완벽하게 지원&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Mews (뮤즈) / Cloudbeds (클라우드베드)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시장 위치: 스타트업이나 부티크 호텔, 모던 크루즈 등에서 최근 폭발적으로 도입하고 있는 SaaS 기반의 클라우드 네이티브 PMS 선두 주자들&lt;/li&gt;
      &lt;li&gt;특징
        &lt;ul&gt;
          &lt;li&gt;오라클 오페라 같은 전통적인 대형 제품들과 달리, 현대적인 REST/gRPC 기반의 Open API 아키텍처가 예술적으로 잘 구축되어 있음&lt;/li&gt;
          &lt;li&gt;만약 대규모 예약을 처리하는 자체 플랫폼 아키텍처를 바닥부터 엔지니어링하기 부담스러운 기업들은, 이 Mews나 Cloudbeds의 SaaS 인프라를 백엔드로 두고 
제공되는 가용 재고 API만 가져와 앞단의 웹/앱 서비스만 가볍게 구현하는 방식을 적극적으로 채택하고 있음&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬, 산 람 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000211656186&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links_vol2.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://grpc.io/docs/what-is-grpc/introduction/&quot;&gt;gRPC Official Documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Database_transaction_schedule&quot;&gt;Wikipedia: Serializability (직렬화 가능성)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.ibm.com/docs/en/rational-clearquest/10.0.9?topic=clearquest-optimistic-pessimistic-record-locking&quot;&gt;IBM Docs: Optimistic and Pessimistic Record Locking&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Optimistic_concurrency_control&quot;&gt;Wikipedia: Optimistic Concurrency Control (낙관적 동시성 제어)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/cd/B10500_01/server.920/a96520/cdc.htm&quot;&gt;Oracle 가이드: Change Data Capture (CDC)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://debezium.io/&quot;&gt;Debezium 공식 사이트&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://microservices.io/patterns/monolithic.html&quot;&gt;Microservices.io: Monolithic Architecture&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Two-phase_commit_protocol&quot;&gt;Wikipedia: Two-Phase Commit Protocol (2PC)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://microservices.io/patterns/data/saga.html&quot;&gt;Microservices.io: Saga Pattern&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assu10.github.io/dev/2024/10/05/ddd-communication-pattern/#22-%EC%82%AC%EA%B0%80saga-%EC%97%AC%EB%9F%AC-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%97%90-%EA%B1%B8%EC%B9%9C-%EB%B9%84%EC%A6%88%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81&quot;&gt;2.2. 사가(saga): 여러 트랜잭션에 걸친 비즈니스 로직&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 01 Jul 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/07/01/architecture-hotel-reservation-system/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/07/01/architecture-hotel-reservation-system/</guid>
        
        <category>architecture</category>
        
        <category>system-design</category>
        
        <category>msa</category>
        
        <category>concurrency-control</category>
        
        <category>caching</category>
        
        <category>database-sharding</category>
        
        <category>optimistic-lock</category>
        
        <category>동시성제어</category>
        
        <category>마이크로서비스</category>
        
        <category>대규모시스템설계</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(1) - 처리율 제한 장치(Rate Limiter)의 설계</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#처리율-제한-장치rate-limiter란&quot;&gt;처리율 제한 장치(Rate Limiter)란?&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#처리율-제한-장치를-도입해야-하는-이유&quot;&gt;처리율 제한 장치를 도입해야 하는 이유&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#1-요구사항-파악&quot;&gt;1. 요구사항 파악&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-요구-사항&quot;&gt;1.1. 요구 사항&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-처리율-제한-장치는-어디에-둘-것인가&quot;&gt;2. 처리율 제한 장치는 어디에 둘 것인가?&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-클라이언트-측에-두는-방안&quot;&gt;2.1. 클라이언트 측에 두는 방안&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-서버-측에-두는-방안&quot;&gt;2.2. 서버 측에 두는 방안&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-미들웨어-또는-api-게이트웨이에-두는-방안&quot;&gt;2.3. 미들웨어 또는 API 게이트웨이에 두는 방안&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#24-아키텍처-위치-선정을-위한-가이드라인&quot;&gt;2.4. 아키텍처 위치 선정을 위한 가이드라인&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-처리율-제한-알고리즘&quot;&gt;3. 처리율 제한 알고리즘&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-토큰-버킷token-bucket-알고리즘&quot;&gt;3.1. 토큰 버킷(Token Bucket) 알고리즘&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#ip는-수억-개가-있고-어떤-게-들어올지-모르는데-그걸-다-어떻게-버킷으로-만드나&quot;&gt;💡IP는 수억 개가 있고 어떤 게 들어올지 모르는데, 그걸 다 어떻게 버킷으로 만드나?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#시스템-전체-제한초당-10000개와-유저별-기능-제한하루-포스팅-1회-친구-추가-100회-등을-동시에-구현하려면-버킷을-어떻게-구성해야-할까&quot;&gt;💡시스템 전체 제한(초당 10,000개)와 유저별 기능 제한(하루 포스팅 1회, 친구 추가 100회 등)을 동시에 구현하려면 버킷을 어떻게 구성해야 할까?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#토큰-버킷이-메모리-사용-측면에서-효율적인-이유는-뭘까&quot;&gt;💡토큰 버킷이 ‘메모리 사용 측면에서 효율적’인 이유는 뭘까?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-누출-버킷leaky-bucket-알고리즘&quot;&gt;3.2. 누출 버킷(Leaky Bucket) 알고리즘&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#누출-버킷은-큐에-요청이-쌓이면-바로-처리하는-것이-아니라-지정된-시간마다-한-번에-모아서-처리하는-것인가&quot;&gt;💡누출 버킷은 큐에 요청이 쌓이면 바로 처리하는 것이 아니라 지정된 시간마다 한 번에 모아서 처리하는 것인가?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#단시간에-트래픽이-몰리면-최신-요청들이-버려지는-것은-토큰-버킷도-동일한-것-아닌가&quot;&gt;💡단시간에 트래픽이 몰리면 최신 요청들이 버려지는 것은 토큰 버킷도 동일한 것 아닌가?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-고정-윈도-카운터fixed-window-counter-알고리즘&quot;&gt;3.3. 고정 윈도 카운터(Fixed Window Counter) 알고리즘&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#윈도가-닫히는-시점에-카운터를-초기화하는-방식이-특정한-트래픽-패턴-처리에-적합한-이유는&quot;&gt;💡윈도가 닫히는 시점에 카운터를 초기화하는 방식이 특정한 트래픽 패턴 처리에 적합한 이유는?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#34-이동-윈도-로깅sliding-window-logging-알고리즘&quot;&gt;3.4. 이동 윈도 로깅(Sliding Window Logging) 알고리즘&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#요청이-거부되어도-타임스탬프는-로그에-남는다의-의미는-이-거부된-요청들이-나중에-처리되는-걸까&quot;&gt;💡’요청이 거부되어도 타임스탬프는 로그에 남는다’의 의미는 이 거부된 요청들이 나중에 처리되는 걸까?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#35-이동-윈도-카운터sliding-window-counter-알고리즘&quot;&gt;3.5. 이동 윈도 카운터(Sliding Window Counter) 알고리즘&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#이전-시간대의-평균-처리율에-따라-상태를-계산하는-것과-짧은-시간에-몰리는-트래픽버스트에-잘-대응하는-것은-무슨-상관일까&quot;&gt;💡’이전 시간대의 평균 처리율에 따라 상태를 계산하는 것’과 ‘짧은 시간에 몰리는 트래픽(버스트)에 잘 대응하는 것’은 무슨 상관일까?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-개략적-아키텍처&quot;&gt;4. 개략적 아키텍처&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-처리율-제한-장치-상세-설계&quot;&gt;5. 처리율 제한 장치 상세 설계&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#51-처리율-제한-규칙&quot;&gt;5.1. 처리율 제한 규칙&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#52-처리율-한도-초과-트래픽의-처리&quot;&gt;5.2. 처리율 한도 초과 트래픽의 처리&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#521-http-응답-헤더-설계-클라이언트와의-소통&quot;&gt;5.2.1. HTTP 응답 헤더 설계: 클라이언트와의 소통&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#53-종합-상세-설계-및-데이터-플로우&quot;&gt;5.3. 종합 상세 설계 및 데이터 플로우&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#54-분산-환경에서의-처리율-제한-장치&quot;&gt;5.4. 분산 환경에서의 처리율 제한 장치&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#541-경쟁-조건race-condition&quot;&gt;5.4.1. 경쟁 조건(Race Condition)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#542-동기화-이슈&quot;&gt;5.4.2. 동기화 이슈&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#543-성능-최적화&quot;&gt;5.4.3. 성능 최적화&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#544-모니터링&quot;&gt;5.4.4. 모니터링&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-마무리&quot;&gt;6. 마무리&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;처리율-제한-장치rate-limiter란&quot;&gt;처리율 제한 장치(Rate Limiter)란?&lt;/h1&gt;

&lt;p&gt;처리율 제한 장치는 클라이언트 또는 서비스가 보내는 트래픽의 송신율(Rate)을 제어하기 위한 인프라 및 애플리케이션 보안 장치이다.&lt;/p&gt;

&lt;p&gt;시스템이 사전에 정의한 임계치(Threshold)를 넘어서는 과도한 HTTP 요청을 전면에서 차단하거나 지연시켜 &lt;strong&gt;시스템 전체의 가용성과 안정성을 보장&lt;/strong&gt;하는 역할을 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;사용자는 초당 2회 이상 글을 올릴 수 없음&lt;/li&gt;
  &lt;li&gt;동일한 IP 주소로는 하루에 10개 이상 계정 생성 불가&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;처리율-제한-장치를-도입해야-하는-이유&quot;&gt;처리율 제한 장치를 도입해야 하는 이유&lt;/h2&gt;

&lt;p&gt;대규모 시스템 아키텍처에서 처리율 제한 장치는 단순히 ‘요청을 막는 것’ 이상의 기술적, 비즈니스적 이점을 제공한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.cloud.google.com/architecture/infra-reliability-guide/traffic-load?hl=ko&quot;&gt;&lt;strong&gt;DoS(Denial of Service, 서비스 거부 공격)/DDos(Distributed Denial of Service, 분산 서비스 거부 공격) 공격에 의한 자원 고갈 방지&lt;/strong&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;가장 본질적인 목표이다.&lt;/li&gt;
      &lt;li&gt;악의적인 Bot이나 해커가 서버의 CPU, 메모리, 네트워크 대역폭을 고갈시키기 위해 무차별적인 요청을 할 때, 이를 유저 인입 단계에서 걸러낸다.&lt;/li&gt;
      &lt;li&gt;실제 Google Docs API의 경우, 내부 인프라 자원 보호를 위해 사용자당 분당 300회의 Read 요청만 허용하도록 처리율 제한을 적용하고 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;인프라 비용 절감&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;처리율 제한 장치를 두면 비정상적인 트래픽을 선제적으로 걷어낼 수 있어 불필요한 서버 Scale-out을 방지한다.&lt;/li&gt;
      &lt;li&gt;Third-party API 과금 방어:
        &lt;ul&gt;
          &lt;li&gt;신용 확인, 본인 인증 등 호출 횟수당 과금되는 외부 서비스를 연동할 때 필수적이다.&lt;/li&gt;
          &lt;li&gt;처리율 제한이 없다면 버그나 공격으로 인해 순식간에 비용이 증가할 수 있다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;서버 과부하 차단 및 가용성 확보&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;트래픽 스파이크가 발생하거나 사용자의 잘못된 패턴으로 유발된 트래픽을 통제한다.&lt;/li&gt;
      &lt;li&gt;이를 통해 핵심 비즈니스 로직을 처리하는 API 서버가 완전히 다운되는 상황을 막고, 우선순위가 높은 정상 유저의 요청에 자원을 할당한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-요구사항-파악&quot;&gt;1. 요구사항 파악&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;제한 장치 종류&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 어떤 종류의 처리율 제한 장치를 설계해야 하는가? 클라이언트 측인가, 서버 측인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 서버측 API를 보호하기 위한 처리율 제한 장치를 설계해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;제한 기준&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 어떤 기준을 사용하여 API 호출을 제어해야 하는가? IP 주소인가? 사용자 ID 인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; IP, 사용자 ID 등 다양한 형태의 제어 규칙을 유연하게 정의할 수 있는 시스템이어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;시스템 규모&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 시스템 규모는 어느 정도인가? 스타트업 수준인가, 대기업 제품 수준인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 대규모 요청을 안정적으로 처리할 수 있는 대기업급 인프라를 타깃으로 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;인프라 환경&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 분산 환경에서 동작해야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 그렇다. 여러 서버와 데이터 센터에 분산된 환경을 고려해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;아키텍처 형태&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 이 장치는 독립된 서비스인가, 애플리케이션 코드 내부에 포함되는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 설계 유연성과 유지보수성을 위해 애플리케이션 전면에서 트래픽을 제어하는 &lt;strong&gt;독립된 미들웨어(API 게이트웨이 형태)&lt;/strong&gt;로 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;사용자 알림&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 요청이 처리율 제한 장치에 의해 걸러진 경우, 사용자에게 그 사실을 알려야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 그렇다. 요청이 제한되었음을 HTTP 상태 코드와 헤더를 통해 명확하게 알려야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-요구-사항&quot;&gt;1.1. 요구 사항&lt;/h2&gt;

&lt;p&gt;위의 요구사항 파악을 바탕으로 도출된 기술적 요구사항들이다.&lt;br /&gt;
이 요구사항들은 본 설계 전체를 관통하는 핵심 KPI(Key Performance Indicator, 핵심 성과 지표)가 된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;정확한 트래픽 제한&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;설정된 처리율 임계치를 초과하는 요청은 한 치의 오차도 없이 정확하게 차단해야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;낮은 지연시간&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;처리율 제한 장치는 API 서버 바로 전면에 위치하므로, 유저의 HTTP 요청 처리에 주는 영향(오버헤드)이 최소화되어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;메모리 최적화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;대규모 유저를 커버해야 하므로, 제한 장치가 사용하는 메모리 양이 가능한 한 적어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;분산형 처리율 제한&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;여러 대의 제한 장치 서버와 프로세스가 존재하더라도 카운터 데이터 등을 실시간으로 공유하고 동기화할 수 있어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;높은 결함 감내성(Fault Tolerance)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;처리율 제한 장치 자체에 장애가 생기더라도, 전체 백엔드 시스템이 마비되어서는 안 됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-처리율-제한-장치는-어디에-둘-것인가&quot;&gt;2. 처리율 제한 장치는 어디에 둘 것인가?&lt;/h1&gt;

&lt;h2 id=&quot;21-클라이언트-측에-두는-방안&quot;&gt;2.1. 클라이언트 측에 두는 방안&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;실무에서는 거의 채택되지 않는 방식&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;서버 자원을 전혀 쓰지 않으므로 구현 비용이 낮지만, 클라이언트 측 요청은 위변조가 너무 쉽다.&lt;br /&gt;
위변조 클라이언트 패킷이나 악의적인 해커의 역공학(Reverse Engineering) 공격에 무방비로 노출되므로, 보안 목적의 처리율 제한에는 전혀 적합하지 않다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-서버-측에-두는-방안&quot;&gt;2.2. 서버 측에 두는 방안&lt;/h2&gt;

&lt;p&gt;처리율 제한 로직을 API 백엔드 서버 내부에 직접 구현하는 방식이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/serverside.png&quot; alt=&quot;서버 측에 처리율 제한 장치&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;장점
    &lt;ul&gt;
      &lt;li&gt;현재 애플리케이션의 비즈니스 로직 및 사용하는 기술 스택 안에서 세밀하고 정교한 제어가 가능하다.&lt;/li&gt;
      &lt;li&gt;데이터베이스나 내부 캐시와의 연동이 직관적이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;단점
    &lt;ul&gt;
      &lt;li&gt;처리율 제한을 위한 연산이 API 서버의 CPU와 메모리 자원을 사용하게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-미들웨어-또는-api-게이트웨이에-두는-방안&quot;&gt;2.3. 미들웨어 또는 API 게이트웨이에 두는 방안&lt;/h2&gt;

&lt;p&gt;API 서버 전면에 독립적인 미들웨어를 구축하여 API 서버로 향하는 모든 트래픽을 통제하는 방식이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/middleware.png&quot; alt=&quot;처리율 제한 미들웨어&quot; /&gt;&lt;/p&gt;

&lt;p&gt;대규모 시스템 아키텍처에서는 이 미들웨어 기능을 &lt;strong&gt;API 게이트웨이&lt;/strong&gt; 컴포넌트에 통합하여 처리하는 것이 일반적이다.&lt;br /&gt;
API 게이트웨이는 처리율 제한뿐만 아니라 아래와 같은 범용적인 기능을 지원한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;SSL 종단(Termination)&lt;/li&gt;
  &lt;li&gt;사용자 인증 및 인가(Authentication/Authorization)&lt;/li&gt;
  &lt;li&gt;IP 허용 목록(Whitelist) 및 차단 목록 관리&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;24-아키텍처-위치-선정을-위한-가이드라인&quot;&gt;2.4. 아키텍처 위치 선정을 위한 가이드라인&lt;/h2&gt;

&lt;p&gt;처리율 제한 장치를 어디에 둘지에 대한 절대적인 정답은 없다. 상황과 인프라 구조에 따라 유연하게 선택해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;현재 엔지니어링 리소스(인원)가 부족한가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;직접 처리율 제한 미들웨어를 개발하는 데는 시간이 많이 소요된다.&lt;/li&gt;
      &lt;li&gt;인력이 부족하다면 클라우드 사업자(AWS, GCP 등)가 제공하는 상용 API 게이트웨이를 도입하는 것이 바람직하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;회사의 아키텍처가 MSA인가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이미 사용자 인증, 로깅 등을 위해 전면에 API 게이트웨이를 두고 있다면, 처리율 제한 기능 역시 게이트웨이에 포함시키는 것이 관리 측면에서 유리하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;직접 알고리즘을 완벽하게 커스텀해야 하는가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;상용 게이트웨이를 쓰면 구현 속도는 빠르지만 선택할 수 있는 알고리즘이나 유연성이 다소 제한된다.&lt;/li&gt;
      &lt;li&gt;비즈니스 특성상 매우 독특한 처리율 제한 알고리즘이 필요하다면 서버 측이나 자체 미들웨어로 구현해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-처리율-제한-알고리즘&quot;&gt;3. 처리율 제한 알고리즘&lt;/h1&gt;

&lt;p&gt;처리율 제한을 실현하기 위한 알고리즘은 다양하다.&lt;br /&gt;
각 알고리즘은 작동 방식과 자원 사용량, 트래픽 패턴에 따른 대응 능력이 다르다.&lt;/p&gt;

&lt;p&gt;업계에서 많이 사용되는 5개 알고리즘을 간략히 살펴보자.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-토큰-버킷token-bucket-알고리즘&quot;&gt;3.1. 토큰 버킷(Token Bucket) 알고리즘&lt;/h2&gt;

&lt;p&gt;기업들이 가장 보편적으로 채택하고 있는 알고리즘이다.&lt;br /&gt;
AWS와 스트라이프(Stripe) 등이 API 요청 통제(Throttling)를 위해 이 알고리즘을 사용한다.&lt;/p&gt;

&lt;p&gt;토큰 버킷은 지정된 용량을 가진 가상의 컨테이너이다.&lt;br /&gt;
이 버킷에는 사전에 설정된 공급률(Refill Rate)에 따라 주기적으로 토큰이 채워지며, 버킷이 가득 차면 더 이상의 토큰은 채워지지 않고 버려진다.&lt;/p&gt;

&lt;p&gt;아래 그림은 용량이 4인 버킷이고, 토큰 공급기(refiller)는 이 버킷에 매초 2개의 토큰을 추가한다.&lt;br /&gt;
버킷이 가득 차면 추가로 공급된 토큰은 버려진다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/token.png&quot; alt=&quot;토큰 버킷&quot; /&gt;&lt;/p&gt;

&lt;p&gt;각 HTTP 요청은 처리될 때마다 하나의 토큰을 사용한다. 요청이 도착하면 버킷 내부를 확인한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;토큰이 충분한 경우:&lt;/strong&gt; 버킷에서 토큰을 하나 꺼낸 후 요청을 API 서버로 전달&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;토큰이 없는 경우:&lt;/strong&gt; 해당 요청은 즉시 버려지거나 거부됨&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/token2.png&quot; alt=&quot;토큰 버킷 알고리즘 동작 방식&quot; /&gt;&lt;/p&gt;

&lt;p&gt;아래 그림은 크기가 4이고, 공급률이 분당 4인 토큰 버킷의 실제 흐름이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/token3.png&quot; alt=&quot;토큰 버킷 알고리즘 동작 방식&quot; /&gt;&lt;/p&gt;

&lt;p&gt;토큰 버킷 알고리즘은 버킷 크기(최대 토큰 개수)와 토큰 공급률(초당 공급 개수)이라는 두 개의 인자를 받아 튜닝한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;‘버킷을 총 몇 개나 생성하고 어떻게 관리해야 하는가?’는 서비스의 ‘공급 제한 규칙(비즈니스 정책)’에 따라 달라진다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;API 엔드포인트 및 사용자별 독립 버킷(가장 보편적)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;통상적으로 유저가 행하는 액션의 종류(API 엔드포인트)마다 별도의 버킷을 할당한다.&lt;/li&gt;
      &lt;li&gt;예) 사용자마다 하루에 단 1번만 포스팅 가능, 친구 추가는 하루에 최대 100명만 가능, 좋아요 버튼은 분당 5번까지만 가능&lt;/li&gt;
      &lt;li&gt;이 경우 시스템은 &lt;strong&gt;사용자 한 명당 총 3개의 독립된 버킷&lt;/strong&gt;을 생성하고 관리해야 한다. 서비스에 가입한 유저가 100만 명이라면, 이론적으로 총 300만 개의 버킷이 동적으로 움직이게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;IP 주소별 처리율 제한&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 IP에서 DDoS 공격을 감행하거나 비정상적인 스크래핑을 시도할 때 이를 IP 기준으로 차단하는 방식이다.&lt;/li&gt;
      &lt;li&gt;이 경우 &lt;strong&gt;각 IP 주소마다 버킷을 하나씩 할당&lt;/strong&gt;해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;시스템 전체 기준 공유 버킷(Global Shared Bucket)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;보안이나 특정 유저의 어뷰징 방지가 아니라, &lt;strong&gt;백엔드 인프라 전체가 다운되지 않도록 방어막을 칠 때&lt;/strong&gt; 사용하는 방식이다.&lt;/li&gt;
      &lt;li&gt;어떤 유저든, 어떤 IP든 상관없이 &lt;strong&gt;모든 HTTP 요청이 단 하나의 대형 글로벌 버킷을 공유&lt;/strong&gt;하도록 설계한다.&lt;/li&gt;
      &lt;li&gt;이 공유 버킷의 토큰이 바닥나면 모든 요청을 응답을 거부한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;공급 제한 규칙별 버킷 예시&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;제한 기준&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;레디스 키(Key) 구조 예시&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;버킷 생성 개수&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;주요 목적&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;유저 + 기능별&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user:{userId}:action:post&lt;/code&gt;&lt;br /&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user:{userId}:action:like&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;유저 수 × 기능 수 (동적 관리)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;비즈니스 어뷰징 및 특정 기능 남용 방지&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;IP 주소별&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ip:{ipAddress}&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;현재 유입 중인 활성 IP 수 (TTL 자동 삭제)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;악의적인 봇(Bot), 디도스(DDoS) 공격 방어&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;시스템 전체&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;global:rate_limit&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;단 1개&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;백엔드 인프라 전반의 과부하 및 다운 방지&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;구현이 직관적이고 메모리 효율이 극대화된다.&lt;/li&gt;
      &lt;li&gt;버킷에 토큰이 남아있기만 하다면 단시간에 집중되는 트래픽 버스트도 유연하게 수용할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;버킷 크기와 공급률이라는 두 파라미터를 유기적으로 결합하여 적절하게 튜닝하는 과정이 까다롭다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;ip는-수억-개가-있고-어떤-게-들어올지-모르는데-그걸-다-어떻게-버킷으로-만드나&quot;&gt;💡IP는 수억 개가 있고 어떤 게 들어올지 모르는데, 그걸 다 어떻게 버킷으로 만드나?&lt;/h3&gt;

&lt;p&gt;전 세계 모든 IP의 버킷을 서버 메모리에 미리 만들어두는 것이 아니라, 레디스와 같은 인메모리 Key-Value 데이터베이스의 &lt;strong&gt;동적 생성(On-Demand)&lt;/strong&gt;과 &lt;strong&gt;TTL&lt;/strong&gt; 메커니즘을 활용하여 
이 문제를 우아하게 해결한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동적 생성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;1:00:00에 111.111.111.111이라는 IP에서 첫 요청이 들어오면, 처리율 제한 미들웨어는 레디스에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ratelimit:111.111.111.111&lt;/code&gt; 이라는 키가 있는지 조회&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;초기화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;키가 없다면 그 순간 레디스에 해당 키를 새로 생성하고, 최초 설정된 토큰 개수(예: 10개)와 함께 &lt;strong&gt;TTL을 1분이나 1일 등으로 짧게 설정&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;자동 삭제&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;만일 해당 IP가 공격을 멈추고 한동안 요청을 보내지 않아 설정된 TTL 시간이 지나면, 레디스는 &lt;strong&gt;해당 IP의 버킷 데이터를 자동으로 소멸&lt;/strong&gt;시킴&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;결과적으로 시스템은 &lt;strong&gt;현재 실시간으로 요청을 보내고 있는 Active IP의 버킷만 캐시에 유지&lt;/strong&gt;하므로 대규모 환경에서도 메모리를 소량만 사용하며 방어할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;시스템-전체-제한초당-10000개와-유저별-기능-제한하루-포스팅-1회-친구-추가-100회-등을-동시에-구현하려면-버킷을-어떻게-구성해야-할까&quot;&gt;💡시스템 전체 제한(초당 10,000개)와 유저별 기능 제한(하루 포스팅 1회, 친구 추가 100회 등)을 동시에 구현하려면 버킷을 어떻게 구성해야 할까?&lt;/h3&gt;

&lt;p&gt;이를 &lt;strong&gt;다중 레이어 처리율 제한(Multi-tier Rate Limiting)&lt;/strong&gt; 구조라고 한다.&lt;br /&gt;
단 하나의 버킷으로 해결하는 것이 아니라 하나의 요청이 통과해야 하는 &lt;strong&gt;버킷 체인&lt;/strong&gt;을 만든다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;1단계: 글로벌 버킷(전체 트래픽이 공유하는 크기 10,000 버킷)&lt;/li&gt;
  &lt;li&gt;2단계: 유저 ID별 버킷(특정 유저 전용 버킷)&lt;/li&gt;
  &lt;li&gt;3단계: 유저 ID + 액션 종류별 버킷(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user124:post&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user123:add_friend&lt;/code&gt; 등)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;클라이언트 요청은 이 모든 버킷에서 각각 토큰을 1개씩 확보해야만 최종적으로 API 서버에 도달할 수 있다.&lt;br /&gt;
중간에 하나의 버킷이라도 토큰이 고갈되면 HTTP 429 에러가 반환된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;토큰-버킷이-메모리-사용-측면에서-효율적인-이유는-뭘까&quot;&gt;💡토큰 버킷이 ‘메모리 사용 측면에서 효율적’인 이유는 뭘까?&lt;/h3&gt;

&lt;p&gt;뒤에 나올 ‘이동 윈도 로깅’ 알고리즘은 유저의 모든 요청 타임스탬프를 배열이나 리스트로 다 저장해야 한다.&lt;br /&gt;
반면 토큰 버킷은 레디스에 유저/IP 별로 &lt;strong&gt;현재 남은 토큰 개수(Integer)&lt;/strong&gt;와 &lt;strong&gt;마지막으로 토큰을 충전한 시간(Timestamp)&lt;/strong&gt;, &lt;strong&gt;딱 두 개의 데이터만 저장&lt;/strong&gt;하면 된다.&lt;br /&gt;
수백만 명의 유저가 인입되어도 Key당 수십 byte면 충분하므로 메모리 효율성이 압도적이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-누출-버킷leaky-bucket-알고리즘&quot;&gt;3.2. 누출 버킷(Leaky Bucket) 알고리즘&lt;/h2&gt;

&lt;p&gt;누출 버킷 알고리즘은 토큰 버킷과 유사해보이지만, &lt;strong&gt;트래픽의 출력 처리율이 고정되어 있다는 점&lt;/strong&gt;이 다르다.&lt;br /&gt;
주로 FIFO 큐를 이용하여 구현한다.&lt;/p&gt;

&lt;p&gt;글로벌 이커머스 플랫폼인 &lt;a href=&quot;https://shopify.dev/docs&quot;&gt;쇼피파이(Shopify)&lt;/a&gt;가 API의 안정적인 처리를 위해 이 누출 버킷 알고리즘을 사용하고 있다.&lt;/p&gt;

&lt;p&gt;누출 버킷 알고리즘은 시스템 튜닝을 위해 아래 2개의 인자를 사용한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;버킷 크기&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;큐의 사이즈와 같은 값이다.&lt;/li&gt;
      &lt;li&gt;큐에는 서버가 처리하기 전까지 대기해야 할 항목(요청)들이 보관된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;처리율(Outflow Rate)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;지정된 시간당 몇 개의 항목을 처리할지 지정하는 값이며, 보통 초 단위(포함된 요청 수/sec)로 표현한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;동작 원리&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;요청이 도착하면 큐가 가득 차 있는지 확인한다. 빈자리가 있다면 큐에 요청을 추가한다.&lt;/li&gt;
  &lt;li&gt;큐가 가득 차 있다면 새로운 요청은 즉시 버린다.&lt;/li&gt;
  &lt;li&gt;지정된 시간 주기마다 큐에서 일정한 개수의 요청을 꺼내어 백엔드로 전달하고 처리한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/leaky.png&quot; alt=&quot;누출 버킷 알고리즘 동작 방식&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;큐의 크기가 고정되어 있어 메모리가 안전하게 관리되며, 백엔드 서버가 감당할 수 있는 일정한 처리 속도(고정 출력률)를 강제할 수 있어 로드 밸런싱과 인프라 안정성에 매우 유리하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;단시간에 많은 트래픽이 몰릴 경우 큐에 오래된 요청들이 쌓여 응답 지연이 발생하고, 제때 처리되지 못한 최신 요청들이 무조건 드롭되는 현상이 발생한다.&lt;/li&gt;
      &lt;li&gt;또한, 토큰 버킷과 마찬가지로 두 개 인자를 올바르게 튜닝하기 어렵다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;누출-버킷은-큐에-요청이-쌓이면-바로-처리하는-것이-아니라-지정된-시간마다-한-번에-모아서-처리하는-것인가&quot;&gt;💡누출 버킷은 큐에 요청이 쌓이면 바로 처리하는 것이 아니라 지정된 시간마다 한 번에 모아서 처리하는 것인가?&lt;/h3&gt;

&lt;p&gt;‘한 번에 모아서 배치 처리한다’라기 보다는, &lt;strong&gt;출력의 속도를 일정하게 흐르도록 제어&lt;/strong&gt;하는 것이다.&lt;/p&gt;

&lt;p&gt;예를 들어 처리율이 초당 10개로 고정되어 있다면 백그라운드 프로세스가 100ms마다 큐에서 정확히 1개씩 요청을 꺼내 처리한다.&lt;br /&gt;
0.001초 사이에 100개의 요청이 한꺼번에 몰려와 큐에 쌓이더라도, 시스템으로 전달되는 요청은 정확히 100ms에 1개씩 고른 간격으로 ‘누출(Leak)’된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;단시간에-트래픽이-몰리면-최신-요청들이-버려지는-것은-토큰-버킷도-동일한-것-아닌가&quot;&gt;💡단시간에 트래픽이 몰리면 최신 요청들이 버려지는 것은 토큰 버킷도 동일한 것 아닌가?&lt;/h3&gt;

&lt;p&gt;핵심 차이는 &lt;strong&gt;이미 인입된 트래픽이 시스템에 주는 지연시간&lt;/strong&gt;과 &lt;strong&gt;버려지는 시점&lt;/strong&gt;에 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;토큰 버킷&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;순간적으로 100개의 요청이 몰려도 토큰이 100개 있으면 &lt;strong&gt;지연 시간 없이 100개 모두 즉시 서버로 전달&lt;/strong&gt;된다.&lt;/li&gt;
      &lt;li&gt;토큰이 다 떨어지는 순간 &lt;strong&gt;그 이후에 들어오는 최신 요청이 바로 버려진다.&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;누출 버킷&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;100개의 요청이 몰리면 일단 큐에 순서대로 쌓인다.&lt;/li&gt;
      &lt;li&gt;백엔드는 이를 고정 속도(예: 100ms에 2개)로 처리하므로, 큐의 맨 뒤에 있는 최신 요청은 &lt;strong&gt;앞의 요청들이 다 빠질 때까지 큐 안에서 대기해야 하므로 엄청난 응답 지연&lt;/strong&gt;을 겪게 된다.&lt;/li&gt;
      &lt;li&gt;그러다 큐의 용량(버킷 크기)을 초과하는 순간 들어오는 최신 요청이 버려진다.&lt;/li&gt;
      &lt;li&gt;즉, 누출 버킷은 서버를 안정적인 속도로 보호하는 대신 유저 요청의 가용성과 응답 속도를 일부 희생한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-고정-윈도-카운터fixed-window-counter-알고리즘&quot;&gt;3.3. 고정 윈도 카운터(Fixed Window Counter) 알고리즘&lt;/h2&gt;

&lt;p&gt;타임라인을 고정된 시간 간격(윈도)로 분할하고, 각 윈도마다 독립된 카운터를 배치하는 방식이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;요청이 접수될 때마다 현재 시간대가 속한 윈도의 카운터를 1씩 증가시킨다.&lt;/li&gt;
  &lt;li&gt;카운터 값이 한계치에 도달하면 해당 윈도가 닫히고 다음 시간대 윈도가 열릴 때까지 모든 요청을 거부한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;아래 그림의 타임라인의 시간 단위는 2초이다.&lt;br /&gt;
시스템은 초당 3개까지만 요청을 허용하며, 매초 열리는 윈도에 3개 이상의 요청이 밀려오면 초과분(회색 박스)은 버려진다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/window.png&quot; alt=&quot;고정 윈도 카운터 알고리즘&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메모리 효율이 좋다.&lt;/li&gt;
      &lt;li&gt;직관적이고 구현이 매우 간단하다.&lt;/li&gt;
      &lt;li&gt;윈도가 닫히는 시점에 카운터를 초기화하는 방식은 특정한 패턴을 처리하기에 적합하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;윈도 경계 부근에서 일시적으로 많은 트래픽이 몰려드는 경우, 기대했던 시스템의 처리 한도보다 많은 양의 요청을 처리하게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;윈도가-닫히는-시점에-카운터를-초기화하는-방식이-특정한-트래픽-패턴-처리에-적합한-이유는&quot;&gt;💡윈도가 닫히는 시점에 카운터를 초기화하는 방식이 특정한 트래픽 패턴 처리에 적합한 이유는?&lt;/h3&gt;

&lt;p&gt;비즈니스 요구사항 중에 &lt;strong&gt;특정 주기마다 명확하게 리셋되는 할당량(Quota) 정책&lt;/strong&gt;이 있는 트래픽 패턴에 완벽하게 들어맞기 때문이다.&lt;br /&gt;
예1) 모든 유저는 &lt;strong&gt;매달 1일 자정&lt;/strong&gt;에 무료 API 호출 크레딧 100회가 새로 충전된다.&lt;br /&gt;
예2) 보안을 위해 비밀번호 찾기 이메일은 &lt;strong&gt;매 정각마다&lt;/strong&gt; 최대 5번까지만 발송 가능하다.&lt;/p&gt;

&lt;p&gt;이러한 비즈니스 패턴은 정해진 시각에 카운터가 0으로 딱 떨어져야 정상이므로, 레디스의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EXPIRE&lt;/code&gt; 명령어로 특정 시간에 카운터를 만료시키는 고정 윈도 방식이 가장 
단순하고 부하가 적은 최적의 아키텍처 솔루션이 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;이 알고리즘은 &lt;strong&gt;경계면 트래픽 집중 문제&lt;/strong&gt;가 있다.&lt;/p&gt;

&lt;p&gt;구체적인 수치로 살펴보자.&lt;br /&gt;
아래 그림은 분당 최대 5개의 요청만을 허용하는 시스템이며, 카운터는 매분마다 초기화된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/window2.png&quot; alt=&quot;윈도에 할당된 양보다 많은 양의 요청 처리하게 되는 문제&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;2:00:30~2:01:00 사이에 5개의 요청이 들어왔고, 윈도가 바뀐 직후인 2:01:00~2:01:30 사이에 또 5개의 요청이 들어왔다.&lt;/li&gt;
  &lt;li&gt;각 고정 윈도 단위(2:00:00~2:01:00, 2:01:00~2:02:00)로 보면 각각 5개씩만 처리했으므로 제한 장치는 요청을 모두 통과시킨다.&lt;/li&gt;
  &lt;li&gt;하지만 윈도 위치를 조금 이동해서 &lt;strong&gt;두 윈도의 경계면인 2:00:30 ~ 2:01:30까지의 1분 동안&lt;/strong&gt;을 살펴보면, 시스템이 처리한 요청은 &lt;strong&gt;10개&lt;/strong&gt;이다.
    &lt;ul&gt;
      &lt;li&gt;이는 허용 한도의 2배를 처리하게 되어 백엔드 서버가 과부하로 다운될 수 있는 약점이 될 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;34-이동-윈도-로깅sliding-window-logging-알고리즘&quot;&gt;3.4. 이동 윈도 로깅(Sliding Window Logging) 알고리즘&lt;/h2&gt;

&lt;p&gt;고정 윈도 카운터의 ‘경계면 트래픽 집중 문제’를 원천적으로 해결하기 위해 등장한 알고리즘이다.&lt;/p&gt;

&lt;p&gt;이 알고리즘은 단순히 숫자를 올리는 카운터 대신 &lt;strong&gt;요청이 들어온 개별 ‘타임스탬프’를 로그로 기록하고 추적&lt;/strong&gt;한다.&lt;br /&gt;
일반적으로 레디스의 &lt;a href=&quot;https://engineering.classdojo.com/blog/2015/02/06/rolling-rate-limiter/&quot;&gt;정렬 집합(Sorted Set)&lt;/a&gt;을 활용한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;새로운 요청이 들어오면 현재 시간 기준으로 &lt;strong&gt;‘윈도 시작점보다 오래된 만료 타임스탬프’를 로그에서 완전히 제거&lt;/strong&gt;한다.&lt;/li&gt;
  &lt;li&gt;현재 요청의 타임스탬프를 로그에 추가한다.&lt;/li&gt;
  &lt;li&gt;로그의 전체 크기(요청 횟수)를 확인하여 허용 한도 이내이면 요청을 통과시키고, 초과하면 거부한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;아래 그림의 처리율 제한기는 &lt;strong&gt;분당 최대 2회의 요청&lt;/strong&gt;만 처리하도록 설정되어 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/move.png&quot; alt=&quot;이동 윈도 로깅 알고리즘&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;① 1:00:01 요청 도착&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;로그가 비어있는 상태이므로 요청이 허용된다.&lt;/li&gt;
      &lt;li&gt;타임스탬프 1:00:01이 로그에 기록된다. (로그 크기: 1)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;② 1:00:30 요청 도착&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;해당 타임스탬프가 로그에 추가된다.&lt;/li&gt;
      &lt;li&gt;추가 직후 로그의 크기는 2이며, 허용 한도(2회)보다 크지 않은 값이므로 요청이 허용된다. (로그 크기: 2)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;③ 1:00:50 요청 도착&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;해당 타임스탬프가 로그에 추가된다.&lt;/li&gt;
      &lt;li&gt;추가 직후 로그의 크기는 3이 되어 허용 한도보다 큰 값이 된다.&lt;/li&gt;
      &lt;li&gt;따라서 &lt;strong&gt;타임스탬프는 로그에 남지만 요청은 거부&lt;/strong&gt;된다. (HTTP 429 반환)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;④ 1:01:40 요청 도착&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;새로운 요청이 1:01:40에 도착하면서 현재 유효한 1분 윈도의 범위는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1:00:40~1:01:40&lt;/code&gt;으로 이동한다.
        &lt;ul&gt;
          &lt;li&gt;이 시점의 윈도 시작점인 1:00:40보다 이전의 타임스탬프들은 전부 만료된 값으로 판정된다.&lt;/li&gt;
          &lt;li&gt;따라서 로그에 쌓여있던 2개의 만료된 타임스탬프(1:00:01, 1:00:30)를 로그에서 삭제한다.&lt;/li&gt;
          &lt;li&gt;삭제 직후 로그에 남은 크기는 &lt;strong&gt;정확히 2(1:00:50 거부 내역 + 신규 1:01:40)&lt;/strong&gt;가 된다.&lt;/li&gt;
          &lt;li&gt;로그의 크기가 허용 한도(2회)와 같으므로 &lt;strong&gt;1:01:40의 신규 요청은 최종적으로 허용&lt;/strong&gt;된다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;허용 한도가 경계선에 구애받지 않고 유연하게 이동하므로, 어떤 임의의 시간 범위를 긁어서 보더라도 허용 처리율 제한을 절대 초과하지 않아 매우 정교하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;메모리 소모가 심각&lt;/strong&gt;하다.
        &lt;ul&gt;
          &lt;li&gt;거부된 요청을 포함하여 모든 타임스탬프를 저장해야 하므로, 대규모 시스템에서 분당 수만 건의 트래픽 폭주가 발생하면 레디스 메모리가 순식간에 고갈될 수 있다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;요청이-거부되어도-타임스탬프는-로그에-남는다의-의미는-이-거부된-요청들이-나중에-처리되는-걸까&quot;&gt;💡’요청이 거부되어도 타임스탬프는 로그에 남는다’의 의미는 이 거부된 요청들이 나중에 처리되는 걸까?&lt;/h3&gt;

&lt;p&gt;아니다. &lt;strong&gt;거부된 요청은 즉시 버려지며(HTTP 429 응답), 나중에 재처리되지 않는다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;로그에 타임스탬프를 남겨두는 이유는 &lt;strong&gt;무차별적인 공격을 차단하기 위함&lt;/strong&gt;이다.&lt;br /&gt;
만일 거부된 요청을 로그에 남기지 않는다면, 한도 초과 이후 악의적인 유저가 초당 1,000번씩 요청을 날려도 로그는 늘 깨끗한 상태를 유지하게 되어, 유저가 공격을 멈춘 직후 
0.001초 뒤에 보내는 요청은 바로 통과할 수 있게 된다.&lt;/p&gt;

&lt;p&gt;거부된 내역까지 로그에 남겨두어야만, 진정한 의미의 ‘이동 윈도 시간 범위’ 내에서의 엄격한 빈도 제어가 완성된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;35-이동-윈도-카운터sliding-window-counter-알고리즘&quot;&gt;3.5. 이동 윈도 카운터(Sliding Window Counter) 알고리즘&lt;/h2&gt;

&lt;p&gt;고정 윈도 카운터의 &lt;strong&gt;메모리 효율성&lt;/strong&gt;과 이동 윈도 로깅의 &lt;strong&gt;경계선 방어 능력&lt;/strong&gt;을 결합한 알고리즘이다.&lt;/p&gt;

&lt;p&gt;이 알고리즘은 과거의 모든 타임스탬프를 로깅하는 대신, ‘직전 윈도의 카운터 값’과 ‘현재 윈도의 카운터 값’을 기반으로 현재 시점의 트래픽을 수학적으로 추정한다.&lt;/p&gt;

&lt;p&gt;임의의 시점에 들어온 새 요청에 대해 현재 윈도 내부의 총 요청 수를 계산하는 공식은 아래와 같다.&lt;/p&gt;

\[\text{현재 윈도 내 추정 요청 수} = \text{현재 윈도의 카운터 값} + \left( \text{직전 윈도의 카운터 값} \times \text{이동 윈도와 직전 윈도가 겹치는 비율} \right)\]

&lt;p&gt;아래 그림은 허용 한도가 분당 7개인 시스템에서 직전 1분 동안 5개, 현재 1분 동안 3개의 요청이 들어왔고, 현재 윈도의 30% 지점에 새 요청이 인입된 경우이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/counter.png&quot; alt=&quot;이동 윈도 카운터 로깅 알고리즘&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;이동 윈도와 직전 1분이 겹치는 비율은 \(100% - 30% = 70%\)&lt;/li&gt;
  &lt;li&gt;3 + (5 * 70%) = 3 + 3.5 = 6.5&lt;/li&gt;
  &lt;li&gt;현재 추정치(6개)는 제한 한도(7개)보다 작으므로, 이번 신규 요청은 통과된다.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;하지만 그 직후에는 한도에 도달했기 때문에 더 이상의 요청은 받을 수 없을 것이다.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;메모리 효율이 좋다.&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;타임스탬프를 저장하지 않고, 직전 윈도와 현재 윈도의 카운터 숫자만 기억하므로  메모리 효율이 극도로 우수하며, 고정 윈도의 경계면 문제를 완벽히 보완한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;짧은 시간에 몰리는 트래픽에 효율적으로 대응&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;이전 시간대의 평균 처리율을 가중치로 반영하므로 경계면의 트래픽 버스트 충격을 유연하게 흡수한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;추정치가 다소 느슨하다.&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;직전 시간대에 도착한 요청들이 시간축 위에 ‘균등하게 분포’되어 있다고 가정한 상태에서 계산하기 때문에 실제 값과 약간의 오차가 있을 수 있다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;하지만 심각한 문제는 아니다.&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;글로벌 CDN 기업인 Cloudflare가 수행한 실험에 따르면, 40억 개의 요청 중 실제 트래픽 상태와 맞지 않게 오판하여 허용되거나 버려진 요청은 &lt;strong&gt;0.003%&lt;/strong&gt;에 불과할 정도로 미미하다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;이전-시간대의-평균-처리율에-따라-상태를-계산하는-것과-짧은-시간에-몰리는-트래픽버스트에-잘-대응하는-것은-무슨-상관일까&quot;&gt;💡’이전 시간대의 평균 처리율에 따라 상태를 계산하는 것’과 ‘짧은 시간에 몰리는 트래픽(버스트)에 잘 대응하는 것’은 무슨 상관일까?&lt;/h3&gt;

&lt;p&gt;고정 윈도처럼 경계면에서 카운터가 0으로 뚝 떨어지거나, 로깅 알고리즘처럼 수만 개의 데이터를 메모리에 올릴 필요 없이 
&lt;strong&gt;직전 트래픽의 흐름(가중치)이 현재 시간대로 부드럽게 이어지기 때문&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;만일 직전 시간대에 트래픽 폭주가 있었다면 가중치 분율(70%) 때문에 현재 시간대 초기에는 카운터가 높은 상태로 시작되어 버스트 트래픽을 자연스럽게 억제한다.&lt;br /&gt;
즉, 과거의 트래픽 기조를 기억하면서 메모리는 단 두 개의 카운터 숫자로만 유지하므로 대규모 스파이크 트래픽 상황에서 인프라의 연산 부담을 덜어준다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-개략적-아키텍처&quot;&gt;4. 개략적 아키텍처&lt;/h1&gt;

&lt;p&gt;지금까지 살펴본 처리율 제한 알고리즘들의 기본 아이디어는 단순하다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;얼마나 많은 요청이 접수되었는지를 추적할 수 있는 카운터를 추적 대상별(사용자별, IP별, API 엔드포인트별)로 두고, 이 카운터의 값이 임계치를 넘어서면 요청을 거부&lt;/strong&gt;하는 것이다.&lt;/p&gt;

&lt;p&gt;그렇다면 이 카운터 데이터는 어디에 보관해야 할까?&lt;br /&gt;
일반적인 RDB는 디스크 접근 오버헤드 때문에 연산 속도가 느려 대규모 트래픽 환경에서 절대 사용할 수 없다.&lt;br /&gt;
따라서 &lt;strong&gt;메모리상에서 동작하는 빠른 캐시 서버&lt;/strong&gt;를 사용해야 한다.&lt;/p&gt;

&lt;p&gt;실무에서는 처리율 제한 장치를 구현할 때 레디스를 자주 사용한다.&lt;br /&gt;
레디스는 메모리 기반 저장 장치이면서, 처리율 제한에 최적화된 두 가지 핵심 명령어를 지원하기 때문이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INCR&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;메모리에 저장된 카운터의 값을 1만큼 원자적으로 증가시킴&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EXPIRE&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;카운터 키에 만료 시간을 설정하여, 지정된 시간이 지나면 메모리에서 자동으로 삭제되도록 관리&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/overall.png&quot; alt=&quot;개략적 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;클라이언트가 처리율 제한 미들웨어에게 HTTP 요청을 보냄&lt;/li&gt;
  &lt;li&gt;미들웨어는 레디스의 해당 유저/IP 카운터를 조회하여 한도에 도달했는지 확인
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;한도에 도달했다면:&lt;/strong&gt; 요청을 즉시 거부하고 클라이언트에게 에러 반환&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;한도에 도달하지 않았다면:&lt;/strong&gt; 요청은 API 서버로 안전하게 전달됨, 이 때 미들웨어는 레디스에 보관된 카운터 값을 1만큼 증가시킨 후 다시 저장함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-처리율-제한-장치-상세-설계&quot;&gt;5. 처리율 제한 장치 상세 설계&lt;/h1&gt;

&lt;p&gt;개략적인 아키텍처를 잡았으니, 이제 구체적으로 알아보자.&lt;/p&gt;

&lt;p&gt;여기서는 아래 두 가지 핵심 질문에 대해 다룬다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;처리율 제한 규칙(Rule)은 어떻게 만들어지고 어디에 저장되는가?&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;처리가 제한된 한도 초과 트래픽들은 구체적으로 어떻게 처리되는가?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;51-처리율-제한-규칙&quot;&gt;5.1. 처리율 제한 규칙&lt;/h2&gt;

&lt;p&gt;실무에서 가장 널리 사용되는 리프트(Lyft)의 오픈소스 처리율 제한 컴포넌트인 &lt;a href=&quot;https://github.com/envoyproxy/ratelimit&quot;&gt;Envoy&lt;/a&gt;의 규칙 정의 방식을 보자.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# [규칙 1] 마케팅 메시지 발송 한도를 하루 최대 5개로 제한&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;messaging&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;descriptors&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;message_type&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;marketing&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;rate_limit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;day&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;requests_per_unit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;5&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# [규칙 2] 클라이언트의 분당 로그인 시도 횟수를 최대 5회로 제한&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;domain&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;auth&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;descriptors&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;auth_type&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;login&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;rate_limit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;unit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;minute&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;requests_per_unit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이런 비즈니스 제약 규칙들은 보통 &lt;strong&gt;디스크 내의 설정 파일 형태로 보관&lt;/strong&gt;한다.&lt;br /&gt;
서버가 매번 요청을 판정할 때마다 디스크를 읽으면 지연 시간이 치명적으로 늘어나므로, 백그라운드 워커가 이 규칙을 주기적으로 읽어 인메모리 캐시에 동기화해 두고 사용한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;52-처리율-한도-초과-트래픽의-처리&quot;&gt;5.2. 처리율 한도 초과 트래픽의 처리&lt;/h2&gt;

&lt;p&gt;사용자의 요청이 사전에 정의한 임계치를 넘어 제한에 걸리면, 시스템은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HTTP 429 Too Many Requests&lt;/code&gt; 응답을 클라이언트에게 반환한다.&lt;/p&gt;

&lt;p&gt;이 때 비즈니스 요구사항에 따라 한도 초과 트래픽을 처리하는 두 가지 옵션이 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;옵션 1: 요청 버림&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;한도를 초과한 즉시 요청을 드롭하고 에러 응답만 보냄&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;옵션 2: 메시지 큐 보관&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;당장 처리하지 못하더라도 나중에 반드시 처리해야 하는 중요한 요청(예: 주문, 결제, 이벤트 응모 등)이라면 메시지 큐(카프카, RabbitMQ 등)에 담아두고 백엔드가 순차적으로 소비하도록 구성&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;521-http-응답-헤더-설계-클라이언트와의-소통&quot;&gt;5.2.1. HTTP 응답 헤더 설계: 클라이언트와의 소통&lt;/h3&gt;

&lt;p&gt;클라이언트는 본인의 요청이 처리율 제한에 걸리고 있는지, 남은 쿼터가 얼마인지 알 수 있어야 유연하게 재시도 로직을 짤 수 있다.&lt;br /&gt;
여기서는 이를 위해 &lt;strong&gt;3개의 HTTP 응답 헤더&lt;/strong&gt;를 반환한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X-Ratelimit-Remaining&lt;/code&gt;: 현재 시간대(윈도) 내에 최대로 더 보낼 수 있는 남은 요청의 개수&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X-Ratelimit-Limit&lt;/code&gt;: 매 윈도 주기마다 클라이언트가 전송할 수 있는 최대 요청의 한도 개수&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X-Ratelimit-Retry-After&lt;/code&gt;: 한도 제한에서 벗어나 정상 요청을 보내기 위해 클라이언트가 &lt;strong&gt;몇 초 뒤에 재시도해야 하는지&lt;/strong&gt; 안내하는 타임아웃 정보&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;유저가 너무 많은 요청을 보내 한도를 초과하면, 제한 장치는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HTTP 429&lt;/code&gt; 오류 코드와 함께 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X-Ratelimit-Retry-After&lt;/code&gt; 헤더를 담아서 즉시 리턴한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;53-종합-상세-설계-및-데이터-플로우&quot;&gt;5.3. 종합 상세 설계 및 데이터 플로우&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/detail.png&quot; alt=&quot;상세 설계&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;규칙 로드&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;처리율 제한 규칙은 최초 디스크에 보관된다.&lt;/li&gt;
      &lt;li&gt;내부 백그라운드 작업 프로세스(Worker)는 이 규칙 파일을 수시로 읽어 &lt;strong&gt;캐시 서버&lt;/strong&gt;에 최신 규칙을 저장한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;요청 인입&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클라이언트가 API 서버로 HTTP 요청을 보내면, 이 요청은 서버에 닿기 전 전면의 &lt;strong&gt;처리율 제한 미들웨어&lt;/strong&gt;에 먼저 도달한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;카운터 및 규칙 조회&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;미들웨어는 캐시에서 해당 엔드포인트에 맞는 ‘제한 규칙’을 가져온다.&lt;/li&gt;
      &lt;li&gt;이와 동시에 &lt;strong&gt;레디스 캐시&lt;/strong&gt;로부터 해당 클라이언트의 ‘현재 카운터 값’과 ‘마지막 요청 타임스탬프’를 읽어온다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;판정 및 처리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;제한에 걸리지 않은 경우&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;미들웨어는 요청을 API 서버로 정상 패스한다. 이와 동시에 레디스의 카운터 값을 1 증가시켜 업데이트한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;제한에 걸린 경우&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;미들웨어는 API 서버로 요청을 보내지 않고 차단한 후, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HTTP 429&lt;/code&gt; 에러를 응답 헤더 메타데이터와 함께 클라이언트에게 반환한다.&lt;/li&gt;
          &lt;li&gt;이때 비즈니스 성격에 따라 요청을 완전히 &lt;strong&gt;버리거나(옵션 1)&lt;/strong&gt;, &lt;strong&gt;메시지 큐에 보관(옵션 2)&lt;/strong&gt;하여 사후 처리한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;54-분산-환경에서의-처리율-제한-장치&quot;&gt;5.4. 분산 환경에서의 처리율 제한 장치&lt;/h2&gt;

&lt;p&gt;처리율 제한 장치를 단일 서버가 아닌, 여러 대의 서버와 병렬 스레드를 지원하도록 대규모 분산 환경으로 확장할 때는 반드시 해결해야 할 문제들이 존재한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;경쟁 조건(Race Condition)&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;동기화&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;541-경쟁-조건race-condition&quot;&gt;5.4.1. 경쟁 조건(Race Condition)&lt;/h3&gt;

&lt;p&gt;처리율 제한 미들웨어가 분산 환경에서 다중 요청을 처리할 때의 로직은 대략 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;레디스에서 카운터의 값을 읽어옴&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;counter + 1&lt;/code&gt;의 값이 임계치를 넘는지 확인&lt;/li&gt;
  &lt;li&gt;넘지 않는다면 레디스에 보관된 카운터 값을 1만큼 증가시켜 저장&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;하지만 병행성(Concurrency)이 매우 심한 대규모 분산 환경에서는 아래 그림과 같은 &lt;strong&gt;경쟁 조건(Race Condition)&lt;/strong&gt; 이슈가 발생할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/race.png&quot; alt=&quot;경쟁 조건&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;문제 시나리오&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;원래 counter의 값이 3인 상태에서 두 개의 요청(요청 1, 요청 2)이 거의 동시에 인입&lt;/li&gt;
      &lt;li&gt;두 요청 스레드가 레디스로부터 counter 값을 읽었을 때 둘 다 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3&lt;/code&gt;을 읽게 됨&lt;/li&gt;
      &lt;li&gt;각자 임계치 체크를 통과한 후 값을 1씩 증가시키고 다시 레디스에 쓰는데, 두 스레드 모두 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;를 기록함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;결과&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;원래 두 개의 요청이 정상 처리되었으므로 counter의 값은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;5&lt;/code&gt;가 되어야 정상이지만, 경쟁 조건으로 인해 데이터 정합성이 깨져 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;로 기록됨&lt;/li&gt;
      &lt;li&gt;임계치보다 더 많은 트래픽이 시스템을 통과할 수 있는 허점이 생김&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;분산 환경에서 동시성 제어를 위해 가장 먼저 떠올리는 것은 Lock 메커니즘이지만, Lock은 시스템 전체의 처리 성능을 상당히 떨어뜨려 대규모 트래픽 미들웨어에는 적합하지 않다.&lt;br /&gt;
위와 같은 상황에서는 Lock 대신 아래 2가지 해결책을 쓸 수 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://gist.github.com/ptarjan/e38f45f2dfe601419ca3af937fff574d#request-rate-limiter&quot;&gt;루아 스크립트(Lua script)&lt;/a&gt;&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;레디스는 싱글 스레드로 명령어를 실행한다.&lt;/li&gt;
      &lt;li&gt;‘카운터 읽기 → 비교 → 카운트 증가’라는 일련의 로직을 루아 스크립트로 묶어서 레디스로 보내면, 레디스는 이 스크립트 전체를 하나의 원자적인 명령어로 실행하므로 Race Condition이 예방됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://engineering.classdojo.com/blog/2015/02/06/rolling-rate-limiter/&quot;&gt;Sorted set&lt;/a&gt;&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#34-이동-윈도-로깅sliding-window-logging-알고리즘&quot;&gt;이동 윈도 로깅 알고리즘&lt;/a&gt;에서 언급한 레디스의 Sorted Set 구조를 활용하면 원자적인 범위 연산이 가능해져 Race Condition 해결 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;542-동기화-이슈&quot;&gt;5.4.2. 동기화 이슈&lt;/h3&gt;

&lt;p&gt;분산 환경에서 수많은 요청을 감당하려면 처리율 제한 장치 서버를 여러 대 두어야 하고, 이 때 &lt;strong&gt;동기화&lt;/strong&gt; 문제가 필연적으로 발생한다.&lt;/p&gt;

&lt;p&gt;기본적으로 웹 계층은 &lt;strong&gt;Stateless 아키텍처&lt;/strong&gt;로 설계된다.&lt;br /&gt;
로드 밸런서는 유저의 요청을 여러 대의 제한 장치 서버로 분산하여 보낸다.&lt;br /&gt;
만약 동기화 처리를 하지 않는다면 아래 그림과 같은 문제가 발생한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/sync.png&quot; alt=&quot;동기화 이슈&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;문제 상황&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클라이언트 1이 첫 번째 요청을 ‘처리율 제한 장치 1’로 보내고, 두 번째 요청은 ‘처리율 제한 장치 2’로 보낼 수 있다.&lt;/li&gt;
      &lt;li&gt;이 때 두 제한 장치 간에 데이터 동기화가 없다면, 제한 장치 2는 클라이언트 1에 대해 아무것도 모르므로 처리율 제한을 올바르게 수행할 수 없음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이를 해결하기 위해 로드 밸런서 단에서 Sticky Session을 활용하여 동일한 클라이언트의 요청은 항상 동일한 처리율 제한 장치로 가도록 강제할 수 있다.&lt;br /&gt;
하지만 이 방법은 특정 서버에 트래픽이 몰릴 수 있고, 규모 확장성과 유연성을 저해하므로 추천하지 않는다.&lt;/p&gt;

&lt;p&gt;더 나은 해결책은 아래 그림과 같이 &lt;strong&gt;레디스와 같은 중앙 집중형 데이터 저장소를 공유&lt;/strong&gt;하는 것이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0621/sync2.png&quot; alt=&quot;동기화 이슈2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;모든 분산 처리율 제한 장치 서버들이 로컬 메모리에 카운터를 두지 않고 공통의 글로벌 레디스를 바라보며 데이터를 읽고 쓰기 때문에, 유저가 어떤 제한 장치 서버로 인입되더라도 
통제된 트래픽 제어가 가능해진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;543-성능-최적화&quot;&gt;5.4.3. 성능 최적화&lt;/h3&gt;

&lt;p&gt;중앙 집중형 공유 저장소 구조까지 완성했다면, 이제 글로벌 대규모 인프라 관점에서 아래 두 가지 지점의 성능 개선을 도모할 수 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;에지 서버(Edge Server) 도입을 통한 지연 시간 단축&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;전 세계 사용자를 대상으로 서비스를 운영할 때, 데이터 센터와 멀리 떨어진 사용자일수록 처리율 제한 장치를 거치는 과정에서 지연 시간이 증가할 수밖에 없다.&lt;/li&gt;
      &lt;li&gt;대형 클라우드 기업들은 전 세계 곳곳에 구축된 에지 서버(CDN 등)에 처리율 제한 장치를 전진 배치하여 사용자와 가장 가까운 엣지 단에서 트래픽을 선제 제어하도록 아키텍처를 최적화한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;최종 일관성 모델(Eventual Consistency Model) 채택&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;제한 장치 간 혹은 분산 저장소 간에 데이터를 실시간으로 동기화(강한 일관성)하려다 보면 동기화 네트워크 비용 때문에 병목이 생긴다.&lt;/li&gt;
      &lt;li&gt;분산 처리율 제한 장치에서는 실시간 정합성을 다소 양보하더라도 최종적으로 일관성이 맞춰지는 &lt;strong&gt;최종 일관성 모델&lt;/strong&gt;을 사용하여 동기화 오버헤드를 줄이고 전체 성능을 극대화한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;일관성 모델에 관해서는 추후 다룰 예정입니다. (p. 73)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;544-모니터링&quot;&gt;5.4.4. 모니터링&lt;/h3&gt;

&lt;p&gt;처리율 제한 장치를 실무 인프라에 성공적으로 배포한 후에는, 지속적인 모니터링을 통해 아래 두 가지 사항을 검증하고 튜닝해야 한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;현재 채택한 처리율 제한 알고리즘이 우리 서비스 트래픽 스타일에 정말 효과적인가?&lt;/li&gt;
  &lt;li&gt;정의한 처리율 제한 규칙(임계치 등)이 비즈니스 요구사항 대비 효과적인가?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;만일 모니터링 중 처리율 제한 규칙이 필요 이상으로 너무 빡빡하게 설정되어 있다는 점이 발견된다면, 수많은 정상 유저의 유효한 요청들이 처리되지 못하고 버려질 것이다.&lt;br /&gt;
모니터링 지표를 기반으로 규칙을 유연하게 조정하는 데이터 기반 튜닝 프로세스가 수반되어야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-마무리&quot;&gt;6. 마무리&lt;/h1&gt;

&lt;p&gt;대규모 시스템의 안정성을 책임지는 다양한 처리율 제한 장치 아키텍처와 알고리즘에 대해 알아보았다.&lt;br /&gt;
마지막으로 아키텍트로서 고려해야 할 실무 팁 두 가지를 공유한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;경성(Hard) 또는 연성(Soft) 처리율 제한의 선택&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;경성 처리율 제한&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;요청 개수가 사전에 정의한 임계치를 절대 단 한 번도 넘어설 수 없다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;연성 처리율 제한&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;트래픽이 일시적으로 스파이크를 치는 짧은 시간 동안만큼은 임계치를 잠시 넘어서는 것을 허용한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;클라이언트의 처리율 제한 회피 및 상생 전략&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시스템이 처리율 제한 장치를 꼼꼼히 설계한 만큼, &lt;strong&gt;클라이언트 측의 설계도 매우 중요&lt;/strong&gt;하다.&lt;/li&gt;
      &lt;li&gt;클라이언트 측 캐시를 적극적으로 활용하여 불필요한 API 호출 횟수 자체를 원천적으로 줄여야 한다.&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HTTP 429&lt;/code&gt; 제한 오류를 만났을 때 짧은 시간 동안 무차별적인 무한 연타 재시도를 보내지 않도록, 재시도 로직을 설계할 때는 서버의 부하를 덜어주기 위해 대기 시간을 지수적으로 늘리는 충분한 &lt;strong&gt;백오프(Exponential Backoff) 및 지터(Jitter)&lt;/strong&gt; 알고리즘을 도입한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;AWS, GCP 같은 퍼블릭 클라우드를 쓰고 있다면 설정값 입력만으로 분산 처리율 제한 장치가 완성된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Cloud Managed API Gateway&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;AWS API Gateway&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;내부적으로 &lt;strong&gt;토큰 버킷 알고리즘&lt;/strong&gt;을 사용하며, 클라이언트별(API Key 기반) 또는 스테이지별 초당 요청 수와 버스트 값을 넣으면 분산 환경 동기화까지 AWS가 알아서 처리해줌&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Azure API Management &amp;amp; Google Cloud Apigee&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Edge Security&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;트래픽이 우리 서버에 오기도 전에, 전 세계에 퍼져 있는 Edge 서버 단에서 악의적인 요청을 선제로 막아주는 제품들
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;Cloudflare Advanced Rate Limiting&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#35-이동-윈도-카운터sliding-window-counter-알고리즘&quot;&gt;앞서 오차율 0.003% 통계로 언급했던 바로 그 제품&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;웹 방화벽(WAF)과 결합하여 악의적인 크롤링 봇이나 DDoS 공격성 연타 트래픽을 백엔드 서버의 자원을 쓰지 않고 전면에서 차단해줌&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;실무에서는 보통 &lt;strong&gt;Cloudflare(보안/에지 제한) + AWS API Gateway(비즈니스/유저별 제한)&lt;/strong&gt;처럼 두 가지를 조합하는 Multi-tier 방식을 많이 사용한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001033116&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.cloud.google.com/architecture/infra-reliability-guide/traffic-load?hl=ko&quot;&gt;Google Cloud 공식 문서: 워크로드의 트래픽 및 부하 관리 가이드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://medium.com/@saisandeepmopuri/system-design-rate-limiter-and-data-modelling-9304b0d18250&quot;&gt;System Design — Rate limiter and Data modelling 심층 분석 가이드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/envoyproxy/ratelimit&quot;&gt;Envoy Proxy 공식 처리율 제한 오픈소스 리포지터리&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://gist.github.com/ptarjan/e38f45f2dfe601419ca3af937fff574d#request-rate-limiter&quot;&gt;Redis를 활용한 Request Rate Limiter 루아 스크립트 레퍼런스 및 예시&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 21 Jun 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/06/21/architecture-distributed-rate-limiter/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/06/21/architecture-distributed-rate-limiter/</guid>
        
        <category>architecture</category>
        
        <category>rate-limiting</category>
        
        <category>redis</category>
        
        <category>sliding-window</category>
        
        <category>token-bucket</category>
        
        <category>distributed-system</category>
        
        <category>race-condition</category>
        
        <category>lua-script</category>
        
        <category>대규모시스템설계</category>
        
        <category>처리율제한</category>
        
        <category>동시성제어</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(2) - 광고 클릭 이벤트 집계 시스템 아키텍처</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#대규모-광고-클릭-집계-시스템-왜-중요할까&quot;&gt;대규모 광고 클릭 집계 시스템, 왜 중요할까?&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#rtbreal-time-bidding-실시간-경매&quot;&gt;RTB(Real-Time Bidding, 실시간 경매)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#광고-성과-측정-지표-ctr-cvr&quot;&gt;광고 성과 측정 지표: CTR, CVR&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#집계-데이터는-rtb에-어떻게-사용되는-걸까&quot;&gt;💡집계 데이터는 RTB에 어떻게 사용되는 걸까?&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#1-요구사항-파악-및-설계-범위-확정&quot;&gt;1. 요구사항 파악 및 설계 범위 확정&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-요구사항-파악&quot;&gt;1.1. 요구사항 파악&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#12-기능-요구사항&quot;&gt;1.2. 기능 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#13-비기능-요구사항&quot;&gt;1.3. 비기능 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#14-개략적-추정치&quot;&gt;1.4. 개략적 추정치&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-개략적-설계안-비동기-스트림-파이프라인-수립&quot;&gt;2. 개략적 설계안: 비동기 스트림 파이프라인 수립&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-질의-api-설계&quot;&gt;2.1. 질의 API 설계&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#window란&quot;&gt;💡window란?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-데이터-모델&quot;&gt;2.2. 데이터 모델&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-db-선택-전략&quot;&gt;2.3. DB 선택 전략&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#컬럼형-데이터-형식columnar-data-format이란&quot;&gt;💡컬럼형 데이터 형식(Columnar Data Format)이란?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#24-개략적-설계안-동기식의-한계와-비동기-스트림-도입&quot;&gt;2.4. 개략적 설계안: 동기식의 한계와 비동기 스트림 도입&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#집계-결과를-왜-바로-db에-기록하지-않을까&quot;&gt;💡집계 결과를 왜 바로 DB에 기록하지 않을까?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#25-집계-서비스-맵리듀스mapreduce와-dag-모델&quot;&gt;2.5. 집계 서비스: 맵리듀스(MapReduce)와 DAG 모델&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#251-맵map-노드&quot;&gt;2.5.1. 맵(Map) 노드&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#데이터-정규화에-맵-노드가-필수일까&quot;&gt;💡데이터 정규화에 맵 노드가 필수일까?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#252-집계aggregation-노드&quot;&gt;2.5.2. 집계(Aggregation) 노드&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#253-리듀스-노드&quot;&gt;2.5.3. 리듀스 노드&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#254-맵리듀스와-dag모델-활용&quot;&gt;2.5.4. 맵리듀스와 DAG모델 활용&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#255-다차원-데이터-필터링을-위한-스타-스키마star-schema&quot;&gt;2.5.5. 다차원 데이터 필터링을 위한 스타 스키마(Star Schema)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#스타-스키마star-schema란&quot;&gt;💡스타 스키마(Star Schema)란?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-상세-설계-1-실시간-데이터-스트리밍과-시간의-제어&quot;&gt;3. 상세 설계 1: 실시간 데이터 스트리밍과 시간의 제어&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-스트리밍flink-vs-일괄-처리mapreduce&quot;&gt;3.1. 스트리밍(Flink) vs 일괄 처리(MapReduce)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#람다-아키텍처의-한계를-극복하는-카파kappa-아키텍처&quot;&gt;💡람다 아키텍처의 한계를 극복하는 카파(Kappa) 아키텍처&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-장애-복구를-위한-이력-데이터-재처리replay-파이프라인&quot;&gt;3.2. 장애 복구를 위한 이력 데이터 재처리(Replay) 파이프라인&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-기준-시간-설정의-트레이드-오프-이벤트-시각-vs-처리-시각&quot;&gt;3.3. 기준 시간 설정의 트레이드 오프: 이벤트 시각 vs 처리 시각&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#34-스트림-데이터-윈도window&quot;&gt;3.4. 스트림 데이터 윈도(Window)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#고정-윈도와-호핑-윈도란&quot;&gt;💡고정 윈도와 호핑 윈도란?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-상세-설계-2-정확히-한-번-전달-보장&quot;&gt;4. 상세 설계 2: ‘정확히 한 번’ 전달 보장&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-메시지-큐의-3가지-전달-방식&quot;&gt;4.1. 메시지 큐의 3가지 전달 방식&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#옐프yelp-사례와-아파치-플링크flink의-정확히-한-번-메커니즘&quot;&gt;💡옐프(Yelp) 사례와 아파치 플링크(Flink)의 ‘정확히 한 번’ 메커니즘&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#42-중복-데이터-처리-아키텍처&quot;&gt;4.2. 중복 데이터 처리 아키텍처&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#구글-가이드-기반-광고-사기위험-제어-컴포넌트의-역할&quot;&gt;💡구글 가이드 기반 광고 사기/위험 제어 컴포넌트의 역할&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#43-분산-트랜잭션이-필요한-이유&quot;&gt;4.3. 분산 트랜잭션이 필요한 이유&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-시스템-규모-확장-및-결함-내성-확보&quot;&gt;5. 시스템 규모 확장 및 결함 내성 확보&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#51-메시지-큐의-규모-확장&quot;&gt;5.1. 메시지 큐의 규모 확장&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#52-브로커broker의-규모-확장-및-데이터-라우팅-전략&quot;&gt;5.2. 브로커(broker)의 규모 확장 및 데이터 라우팅 전략&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#ad_id-를-해시-키가-아니라-메시지-key로-설정해도-항상-같은-파티션에-들어가지-않나&quot;&gt;💡ad_id 를 해시 키가 아니라 메시지 Key로 설정해도 항상 같은 파티션에 들어가지 않나?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#53-집계-서비스의-규모-확장&quot;&gt;5.3. 집계 서비스의 규모 확장&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#531-다중-스레드-모델-vs-다중-프로세싱리소스-프로바이더-모델&quot;&gt;5.3.1. 다중 스레드 모델 vs 다중 프로세싱(리소스 프로바이더) 모델&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#아파치-하둡-yarn-리소스-프로바이더의-다중-프로세싱-동작-원리&quot;&gt;💡아파치 하둡 YARN 리소스 프로바이더의 다중 프로세싱 동작 원리&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#54-카산드라cassandra-db의-가상노드-기반-자동-샤딩-구조&quot;&gt;5.4. 카산드라(Cassandra) DB의 가상노드 기반 자동 샤딩 구조&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#55-핫스팟hotspot-문제-해결&quot;&gt;5.5. 핫스팟(Hotspot) 문제 해결&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#flink-성능-튜닝의-핵심-전역-지역-집계global-local-aggregation&quot;&gt;💡Flink 성능 튜닝의 핵심: 전역-지역 집계(Global-Local Aggregation)&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-결함-내성fault-tolerance와-스냅숏-복구&quot;&gt;6. 결함 내성(Fault Tolerance)와 스냅숏 복구&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-데이터-모니터링-정확성-검증-및-대안적-설계&quot;&gt;7. 데이터 모니터링, 정확성 검증 및 대안적 설계&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#71-지속적-모니터링-지표&quot;&gt;7.1. 지속적 모니터링 지표&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#분산-커밋-로그는-무엇이며-왜-records-lag을-추적해야-할까&quot;&gt;💡분산 커밋 로그는 무엇이며, 왜 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;records-lag&lt;/code&gt;을 추적해야 할까?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#72-종단-간-조정reconciliation-프로세스&quot;&gt;7.2. 종단 간 조정(Reconciliation) 프로세스&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#73-대안적-아키텍처-설계&quot;&gt;7.3. 대안적 아키텍처 설계&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#hive란&quot;&gt;💡Hive란?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#클릭하우스clickhouse나-드루이드druid같은-olap-데이터베이스란&quot;&gt;💡클릭하우스(ClickHouse)나 드루이드(Druid)같은 OLAP 데이터베이스란?&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#8-마무리&quot;&gt;8. 마무리&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#아파치-플링크flink-vs-아파치-스파크spark&quot;&gt;💡아파치 플링크(Flink) vs 아파치 스파크(Spark)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#이-아키텍처를-그대로-제공하는-상용화된-오픈소스-제품군&quot;&gt;💡이 아키텍처를 그대로 제공하는 상용화된 오픈소스 제품군&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;대규모-광고-클릭-집계-시스템-왜-중요할까&quot;&gt;대규모 광고 클릭 집계 시스템, 왜 중요할까?&lt;/h1&gt;

&lt;p&gt;디지털 광고 생태계에서 ‘클릭 집계’는 단순히 숫자를 세는 것 이상의 의미가 있다.&lt;br /&gt;
이는 광고주에게 청구될 ‘돈’과 직결되며, 광고 캠페인의 성공 여부를 결정짓는 나침반 역할을 하기 때문이다.&lt;/p&gt;

&lt;p&gt;기술적인 세부 사항에 들어가기에 앞서, RTB(Real-Time Bidding)와 핵심 지표들을 먼저 살펴보자.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;광고 클릭 집계(Ad Click Aggregation)&lt;/strong&gt;란 분산된 여러 서버에서 발생하는 무수한 광고 클릭 로그를 수집하여, 특정 시간 단위(분, 시간 등)별로 광고 아이디(ad_id)당 
클릭 횟수를 정밀하게 계산해내는 프로세스이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;rtbreal-time-bidding-실시간-경매&quot;&gt;RTB(Real-Time Bidding, 실시간 경매)&lt;/h2&gt;

&lt;p&gt;사용자가 웹사이트나 앱을 여는 짧은 순간, 보이지 않는 곳에서는 거대한 경매가 일어난다.&lt;/p&gt;

&lt;p&gt;RTB는 광고 지면(Inventory)이 노출될 때마다 광고주들이 실시간으로 입찰 경쟁을 벌여 가장 높은 가격을 제시한 광고를 즉시 노출시키는 자동화된 거래 방식이다.&lt;/p&gt;

&lt;p&gt;사용자가 웹페이지를 클릭했을 때, 페이지 콘텐츠보다 광고가 늦게 뜬다면 사용자 경험이 급격히 저하된다.&lt;br /&gt;
만약 경매 프로세스가 길어져 1초를 넘기게 되면 사용자는 광고가 뜨기 전에 페이지를 이탈하거나 콘텐츠에만 집중하게 되어 광고 효과가 사라진다.&lt;br /&gt;
따라서 &lt;strong&gt;경매 시작부터 광고 노출까지 전 과정은 보통 100~300ms 내외&lt;/strong&gt;로 완료되어야 하며, 시스템 전체 지연 시간은 엄격하게 1초 미만으로 제한된다.&lt;/p&gt;

&lt;p&gt;RTB가 초저지연(Ultra-low latency)을 지향한다면, 집계 시스템은 데이터의 무결성에 더 무게 중심을 둔다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;광고-성과-측정-지표-ctr-cvr&quot;&gt;광고 성과 측정 지표: CTR, CVR&lt;/h2&gt;

&lt;p&gt;집계된 데이터를 바탕으로 광고주는 자신의 광고가 얼마나 효율적인지 판단한다. 이 때 가장 많이 활용되는 지표가 &lt;strong&gt;CTR(Click-Through Rate, 클릭률)&lt;/strong&gt;과 &lt;a href=&quot;https://support.google.com/google-ads/answer/2684489?hl=en&quot;&gt;&lt;strong&gt;CVR(Conversion Rate, 전환율)&lt;/strong&gt;&lt;/a&gt;이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;CTR(클릭률)
    &lt;ul&gt;
      &lt;li&gt;광고를 본 사람(노출) 중 몇 명이나 광고를 &lt;strong&gt;클릭&lt;/strong&gt;했는지 보여주는 지표&lt;/li&gt;
      &lt;li&gt;‘광고가 얼마나 매력적이어서 사람들의 눈길을 끌었는가?’&lt;/li&gt;
      &lt;li&gt;계산: (클릭 수 / 노출 수) * 100&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;CVR(전환율)
    &lt;ul&gt;
      &lt;li&gt;광고를 클릭해서 들어온 사람 중 몇 명이나 구매, 회원가입 등 &lt;strong&gt;최종 목표(전환)&lt;/strong&gt;를 달성했는지 보여주는 지표&lt;/li&gt;
      &lt;li&gt;여기서 ‘전환’은 단순 상품 구매뿐 아니라 웹사이트 가입, 뉴스레터 구독, 앱 설치 등 비즈니스 가치를 더하는 모든 행위&lt;/li&gt;
      &lt;li&gt;‘사이트에 들어온 사람들이 실제로 물건을 얼마나 샀는가?’&lt;/li&gt;
      &lt;li&gt;계산: (전환 수 / 클릭 수) * 100&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;집계-데이터는-rtb에-어떻게-사용되는-걸까&quot;&gt;💡집계 데이터는 RTB에 어떻게 사용되는 걸까?&lt;/h2&gt;

&lt;p&gt;집계 데이터는 RTB의 입찰 결정에 결정적인 영향을 미친다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;예산 소진 제어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 광고의 예산이 모두 소진되었다면 RTB 경매에서 해당 광고를 즉시 제외해야 하는데 이 때 실시간 집계 데이터가 필요함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;부정 클릭 차단&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 IP에서 비정상적인 클릭이 집계된다면, RTB 엔진은 해당 소스에서 오는 입찰 요청을 무시
        &lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;1. 사용자가 웹사이트 방문] 
   ↓
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;2. 매체가 RTB 엔진에 &lt;span class=&quot;s2&quot;&gt;&quot;입찰 요청(Bid Request)&quot;&lt;/span&gt; 전송]  ← &lt;span class=&quot;s2&quot;&gt;&quot;이 사용자 IP에 광고 보여줄 사람?&quot;&lt;/span&gt;
   ↓
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;3. RTB 엔진들이 경쟁하여 &lt;span class=&quot;s2&quot;&gt;&quot;입찰(Bid)&quot;&lt;/span&gt; 및 낙찰]
   ↓
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;4. 사용자 화면에 광고 노출&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Impression&lt;span class=&quot;o&quot;&gt;)]&lt;/span&gt;
   ↓
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;5. 사용자가 광고를 &lt;span class=&quot;s2&quot;&gt;&quot;클릭(Click)&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; -&amp;gt; ★ 바로 여기서 클릭 집계 서비스가 작동!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/li&gt;
      &lt;li&gt;특정 IP가 악의적인 매크로 봇이라고 가정하고 이 봇이 광고주들에게 돈을 물리려고 광고를 매 분 수백 번씩 누르고 있다고 하자.&lt;/li&gt;
      &lt;li&gt;집계 서비스는 이 비정상적인 패턴을 감지하고 해당 IP를 ‘광고 사기 봇 IP’로 분류하여 블랙 리스트에 저장&lt;/li&gt;
      &lt;li&gt;잠시 후 이 봇이 다른 사이트에 접속했을 때, 뉴스 사이트는 광고를 보여주기 위해 RTB 엔진에 입찰 요청을 보냄&lt;/li&gt;
      &lt;li&gt;이 때 RTB 엔진은 입찰하기 전에 집계 서비스가 업데이트해 둔 블랙리스트를 확인하여, 가짜 클릭이므로(= 광고비만 날리게 될 것이므로) 해당 입찰 요청을 무시함&lt;/li&gt;
      &lt;li&gt;즉, RTB 엔진은 특정 봇 IP가 웹서핑을 할 때 ‘저 봇에게는 광고를 팔지 않겠다’라며 경매 참여를 거절하는 것임&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;입찰가 최적화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;과거의 집계 데이터(CTR/CVR 추이)를 학습한 AI 모델이 현재 RTB 경매에서 얼마를 써야 가장 효율적인지 결정함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;과거에는 Cookie 기반의 개별 사용자 추적이 핵심이었으나, 최근 &lt;strong&gt;애플의 ATT(App Tracking Transparency) 정책&lt;/strong&gt;과 &lt;strong&gt;구글의 Privacy Sandbox&lt;/strong&gt; 도입으로 인해 
개별 사용자 추적보다는 &lt;strong&gt;집계된 데이터&lt;/strong&gt;를 활용한 성과 측정이 더욱 중요해지고 있다.&lt;br /&gt;
이제는 ‘누가’ 클릭했는지보다 ‘어떤 그룹’에서 ‘얼마나’ 집계되었는지가 시스템 설계의 핵심이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-요구사항-파악-및-설계-범위-확정&quot;&gt;1. 요구사항 파악 및 설계 범위 확정&lt;/h1&gt;

&lt;h2 id=&quot;11-요구사항-파악&quot;&gt;1.1. 요구사항 파악&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;입력 데이터의 형태&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 데이터는 어떤 방식으로 저장되고 인입되는가? 클릭 이벤트의 구체적인 속성은?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 데이터는 여러 애플리케이션 서버에 분산된 로그 파일 형태로 존재함&lt;br /&gt;
클릭 이벤트가 수집될 때마다 이 로그 파일의 끝에 단방향으로 추가(Append-only)됨.&lt;br /&gt;
각 클릭 이벤트 메시지에는 ad_id(광고 식별자), click_timestamp(클릭 시각), user_id(사용자 식별자), country(국가 코드) 등의 속성이 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 양&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 감당해야 할 전체 데이터 양과 트래픽 규모는?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 시스템적으로 매일 10억 개의 광고 클릭 이벤트가 발생하며, 광고는 하루에 약 2백만 회 게재된다고 가정&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;비즈니스 규모&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 비즈니스 성장세에 따른 규모 확장성도 고려해야 하는지?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 그렇다. 광고 클릭 이벤트 수는 매년 30%씩 증가한다고 가정&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;주요 질의 요구사항&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q1:&lt;/strong&gt; 가장 핵심적인 질의와 대시보드 기능은 무엇인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A1:&lt;/strong&gt; 첫째는 &lt;strong&gt;특정 광고에 대한 지난 N분 간의 클릭 이벤트 수&lt;/strong&gt;이고, 둘째는 &lt;strong&gt;지난 1분간 가장 많이 클릭된 상위 100개 광고 목록&lt;/strong&gt;임&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Q2:&lt;/strong&gt; 질의 조건이나 파라미터가 동적으로 변할 수도 있는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A2:&lt;/strong&gt; 그렇다. 질의 기간(N)과 추출할 광고 수(상위 M개)는 유연하게 변경 가능해야 하며, 집계 연산 자체는 매분 주기적으로 이루어져야 함.&lt;br /&gt;
또한 광고주나 데이터 과학자가 ip, user_id, country 등의 속성을 기준으로 위 두 가지 질의 결과를 자유롭게 필터링할 수 있어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;예외 상황 및 결함 내성(Edge case)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 네트워크 지연이나 장애 상황 등 엣지 케이스는 어디까지 고려해야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 예상보다 늦게 도착하는 이벤트, 네트워크 재시도로 인한 중복된 이벤트, 특정 집계 서버가 완전히 다운되는 시스템 장애 상황까지 모두 감내할 수 있도록 견고하게 설계해야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;지연 시간 요건&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 최종 응답 지연 요구사항은 얼마나 엄격한가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 실시간 경매(RTB) 시스템과 광고 클릭 집계 서비스의 지연 시간 요건은 완전히 다름
        &lt;ul&gt;
          &lt;li&gt;RTB는 사용자에게 즉시 광고를 보여줘야 하므로 1초 미만의 초저지연이 필수적임&lt;/li&gt;
          &lt;li&gt;반면, 광고 클릭 집계 시스템은 주로 광고 과금 및 대시보드 통계 보고에 사용되므로, 데이터의 무결성만 보장된다면 &lt;strong&gt;수 분 정도의 지연 처리는 충분히 허용&lt;/strong&gt;됨&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;12-기능-요구사항&quot;&gt;1.2. 기능 요구사항&lt;/h2&gt;

&lt;p&gt;시스템 사용자(광고주, 데이터 과학자 등)에게 제공해야 하는 핵심 기능은 아래와 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;기간별 클릭 수 집계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;지난 N분 동안 특정 광고(ad_id)에 발생한 클릭 이벤트 수를 실시간으로 집계&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;인기 광고 추출&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;매분 가장 많이 클릭된 상위 100개의 광고 아이디 목록 반환&lt;/li&gt;
      &lt;li&gt;질의 기간과 상위 M개 광고 수는 유연하게 변경 가능해야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;다차원 필터링 지원&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;집계된 결과를 IP, user_id 등 다양한 속성을 기준으로 필터링하여 조회할 수 있어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;13-비기능-요구사항&quot;&gt;1.3. 비기능 요구사항&lt;/h2&gt;

&lt;p&gt;대규모 과금 데이터와 연동되는 시스템인 만큼, 인프라의 안정성을 위한 엄격한 제약 조건이 필요하다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;데이터 정확성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;집계 데이터는 광고주에게 비용을 청구하고, RTB 엔진의 예산 소진 제어를 조절하는 기준이 되므로 데이터 누락이나 중복이 없어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;결함 내성(Fault Tolerance)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 집계 노드가 다운되거나 지연되더라도, 시스템은 멈추지 않고 중복없이 이벤트를 복구하여 처리할 수 있어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;허용 가능한 지연 시간&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;초저지연(1초 미만)을 요구하는 RTB 시스템과 달리, 광고 정산 및 보고용 클릭 집계 서비스는 데이터 무결성이 더 중요하므로 &lt;strong&gt;수 분 정도의 지연 시간은 허용&lt;/strong&gt;됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;14-개략적-추정치&quot;&gt;1.4. 개략적 추정치&lt;/h2&gt;

&lt;p&gt;시스템 규모 확장의 기준이 되는 대략적인 트래픽과 저장 용량을 산정해본다.&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;트래픽 추정치(QPS)&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;일간 능동 사용자(DAU):&lt;/strong&gt; 1,000,000,000(10억 명)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;일일 광고 클릭 수:&lt;/strong&gt; 모든 사용자가 하루 평균 1개 광고를 클릭한다고 가정하면, &lt;strong&gt;하루에 총 10억 건&lt;/strong&gt;의 광고 클릭 이벤트가 발생함&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;평균 광고 클릭 QPS:&lt;/strong&gt; \(\text{Average QPS} = \frac{10^9 \text{ 이벤트를}}{86,400 \text{ 초 (하루)}} \approx 11,574 \rightarrow \text{약 10,000 QPS}\)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;최대 광고 클릭 QPS:&lt;/strong&gt; 평균 QPS의 5배로 가정할 경우 &lt;strong&gt;50,000 QPS&lt;/strong&gt;를 감당해야 함&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;저장 용량 요구량&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;광고 클릭 이벤트 당 크기:&lt;/strong&gt; 0.1KB 가정&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;일일 저장소 요구량:&lt;/strong&gt; 0.1KB * 10억 = 100GB/일&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;월간 저장소 요구량:&lt;/strong&gt; 약 3TB&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;비즈니스 성장률:&lt;/strong&gt; 광고 클릭 이벤트 수는 매년 30%씩 증가한다고 가정하며, 시스템은 약 3년마다 트래픽이 2배로 증가하는 구조에 대응해야 함&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;목표 처리량:&lt;/strong&gt; 평균 10,000 QPS / 최대 50,000 QPS&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;일일 데이터 양:&lt;/strong&gt; 100GB&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;핵심 트레이드오프:&lt;/strong&gt; 초저지연보다는 &lt;strong&gt;정확히 한 번(Exactly-once)&lt;/strong&gt;의 데이터 무결성 확보&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 시스템은 막대한 양의 원시 로그를 유실 없이 받아내면서도 비즈니스 요구사항에 맞는 집계 데이터를 실시간으로 산출해야 하는 &lt;strong&gt;쓰기 중심(Write-heavy)의 빅데이터 파이프라인&lt;/strong&gt; 아키텍처가 필요하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-개략적-설계안-비동기-스트림-파이프라인-수립&quot;&gt;2. 개략적 설계안: 비동기 스트림 파이프라인 수립&lt;/h1&gt;

&lt;p&gt;여기서는 질의 API 설계, 데이터 모델, DB 선택, 비동기 맵리듀스(MapReduce) 프레임워크 연동 구조에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-질의-api-설계&quot;&gt;2.1. 질의 API 설계&lt;/h2&gt;

&lt;p&gt;클라이언트(대시보드를 이용하는 데이터 과학자, 광고주 등)가 데이터를 조회할 때 호출할 두 가지 핵심 API 이다.&lt;/p&gt;

&lt;p&gt;API 설계를 위해 기능 요구사항을 검토해보자.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;지난 N분 동안 ad_id에 발생한 클릭 수 집계&lt;/li&gt;
  &lt;li&gt;지난 N분 동안 가장 많은 클릭이 발생한 상위 M개 ad_id 목록 반환&lt;/li&gt;
  &lt;li&gt;다양한 속성을 기준으로 집계 결과를 필터링하는 기능&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;여기서는 2개의 API만 있으면 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;API 1: 지난 N분간 각 ad_id에 발생한 클릭 수 집계&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;HTTP Method:&lt;/strong&gt; GET /v1/ads/{:ad_id}/aggregated_count&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Query String:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;from: 집계 시작 시간(Long, 기본값: 현재 시각 기준 1분 전)&lt;/li&gt;
      &lt;li&gt;to: 집계 종료 시간(Long, 기본값: 현재 시각)&lt;/li&gt;
      &lt;li&gt;filter: 필터링 전략 식별자(예: filter=001)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Response:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;ad_id: 광고 식별자(String)&lt;/li&gt;
      &lt;li&gt;count: 집계된 클릭 횟수(Long)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;API 2: 지난 N분간 가장 많은 클릭이 발생한 상위 M개 ad_id 목록&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;HTTP Method:&lt;/strong&gt; GET /v1/ads/popular_ads&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Query String:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;count: 상위 몇 개의 광고를 반환할 것인가(Integer)&lt;/li&gt;
      &lt;li&gt;window: 분 단위로 표현된 집계 윈도 크기(Integer)&lt;/li&gt;
      &lt;li&gt;filter: 필터링 전략 식별자(Long)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Response:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;ad_ids: 광고 식별자 목록(Array)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;window란&quot;&gt;💡window란?&lt;/h3&gt;

&lt;p&gt;스트림 처리에서 &lt;strong&gt;윈도(Window)란 무한히 흘러들어오는 데이터 스트림을 시간 단위의 특정 덩어리(chunk)로 나누는 경계선&lt;/strong&gt;을 말한다.&lt;br /&gt;
예를 들어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window=5&lt;/code&gt;라면 ‘현재 시점부터 정확히 5분 전까지의 데이터 묶음’을 의미하며,&lt;br /&gt;
시스템은 이 시간 영역 안에 들어온 이벤트들만 보아서 상위 광고를 계산하게 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-데이터-모델&quot;&gt;2.2. 데이터 모델&lt;/h2&gt;

&lt;p&gt;여기서 다루는 데이터는 크게 원시 데이터(Raw data)와 집계 결과 데이터(Aggregated data)로 나뉜다.&lt;br /&gt;
이 두 데이터는 비즈니스적 목적과 읽기/쓰기 비율이 완전히 다르므로 별도의 보관 및 DB 전략이 필요하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;원시 데이터&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;아래는 로그 파일에 포함된 원시 데이터 예시이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; ad001, 2021-01-01 00:00:01, user 1, 207.148.22.22, USA
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 로그를 구조화된 형식으로 표현하면 아래와 같으며, 이런 데이터가 여러 애플리케이션 서버에 산재해있게 된다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;ad_id&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;click_timestamp&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;user_id&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;ip&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;country&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021-01-01 00:00:01&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;user1&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;207.148.22.22&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;USA&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad002&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021-01-01 00:00:02&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;user2&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;209.153.56.11&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;USA&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;집계 결과 데이터&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;광고 클릭 이벤트가 매 분 집계된다고 가정했을 때의 집계 결과 테이블은 ad_id, click_minute, count 가 있을 것이다.
아래는 광고 필터링을 지원하기 위해 filter_id를 추가하여, 같은 ad_id와 click_minute 값을 갖는 레코드를 filter_id 필터 적용 결과에 따라 집계한 결과이다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;ad_id&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;click_minute&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;filter_id&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;count&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021101010000&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0012&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021101010000&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0023&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021101010001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0012&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021101010001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0023&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;6&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;아래는 필터 테이블이다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;filter_id&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;region&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;ip&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;user_id&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0012&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;US&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0012&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;*&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0013&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;*&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;0023&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;123.1.2.3&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;지난 N분 동안 가장 많이 클릭된 상위 M개의 광고를 반환하는 질의를 위해서는 아래 구조를 이용한다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;most_clicked_ads&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt; &lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt; &lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;window_size&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;integer&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;분 단위로 표현된 집계 윈도 크기&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;update_time_minute&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;timestamp&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;마지막으로 갱신된 타임스탬프 (1분 단위)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;most_clicked_ads&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;array&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;JSON 형식으로 표현된 ID 목록&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;원시 데이터 vs 집계 결과 데이터 비교&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;구분&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;원시 데이터(Raw Data) 방안&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;집계 결과 데이터(Aggregated Data) 방안&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;개념&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;수집된 로그 그대로의 형태&lt;br /&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ad001, 2026-06-28 11:00:01, user1, USA&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;시간별로 계산/축약된 형태&lt;br /&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ad001, 202606281100, 105건&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;장점&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;• 원본 데이터의 손실이 없음&lt;br /&gt;• 언제든 필터링 변경 및 재계산 가능&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;• 데이터 용량이 획기적으로 절감됨&lt;br /&gt;• 대시보드 조회 시 질의 성능이 매우 빠름&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;단점&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;• 막대한 데이터 용량 (일 100GB 이상)&lt;br /&gt;• 전체 데이터 쿼리 시 성능이 매우 낮음&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;• 원본 데이터가 유실/축약되므로 특정 상세 속성을 역추적하기 어려움&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;결론은 원시 데이터와 집계 결과 데이터, &lt;strong&gt;두 데이터 모델을 모두 저장하는 ‘하이브리드’ 전략&lt;/strong&gt;을 취해야 한다.&lt;br /&gt;
평소 대시보드 질의는 &lt;strong&gt;집계 결과 데이터&lt;/strong&gt;를 통해 빠르게 수행하고,&lt;br /&gt;
시스템에 버그가 발생하여 데이터를 재계산해야 하거나 디버깅이 필요할 때는 &lt;strong&gt;원시 데이터&lt;/strong&gt;를 백업 데이터(Cold Storage) 삼아 복구하는 구조가 가장 이상적이다.&lt;/p&gt;

&lt;p&gt;원시 데이터를 Cold Storage로 옮겨 비용을 절감하고, 집계 결과 데이터는 활성 데이터(active data) 구실을 하는 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-db-선택-전략&quot;&gt;2.3. DB 선택 전략&lt;/h2&gt;

&lt;p&gt;DB 선택을 위해 아래 4가지 핵심 질문을 반드시 던져야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;데이터의 형태는 어떠한가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;관계형 데이터, 문서 데이터, 혹은 대용량 BLOB 형태인가?&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;주요 작업 흐름은 어떠한가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;읽기 중심인가, 쓰기 중심인가, 혹은 둘 다 많이 발생하는가?&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;강력한 트랜잭션(ACID) 지원이 필수적인가?&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;질의 과정에서 SUM이나 COUNT같은 &lt;a href=&quot;https://docs.oracle.com/database/121/OLAXS/olap_functions.htm#OLAXS169&quot;&gt;온라인 분석 처리(OLAP)&lt;/a&gt; 함수를 헤비하게 사용하는가?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 기준을 바탕으로 원시 데이터와 집계 데이터 각각의 관점에서 DB 최적화 전략을 매핑해본다.&lt;/p&gt;

&lt;p&gt;여기서는 대규모 분산 쓰기 성능이 강력하게 검증된 &lt;strong&gt;카산드라(Cassandra)&lt;/strong&gt;를 원시 데이터 저장소로 최종 활용한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1) 원시 데이터 관점&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;원시 데이터는 일상적인 정기 서비스 작업에서는 거의 질의할 필요가 없다.&lt;br /&gt;
다만, 데이터 과학자나 머신 러닝 엔지니어들이 사용자 반응 예측 모델을 개발하거나 관련성 피드백 알고리즘을 연구할 때 매우 유용하게 쓰이는 백업 자산이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;워크로드 분석&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;개략적 추정치에서 산출했듯, 이 시스템의 &lt;strong&gt;평균 쓰기 트래픽은 10,000 QPS이며, 최대 피크 시에는 50,000 QPS&lt;/strong&gt;까지 치솟는다.&lt;/li&gt;
      &lt;li&gt;반면, 읽기 연산은 백업 및 재계산 시에만 간헐적으로 발생하므로 극단적인 &lt;strong&gt;쓰기 중심(Write-heavy) 시스템&lt;/strong&gt;이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;RDBMS의 한계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;전통적인 RDBMS로도 구축 자체는 가능하겠지만, 초당 50,000건의 고속 쓰기 연산이 지원되는 환경에서는 인덱스(B+ Tree) 갱신 오버헤드와 Lock 경합 때문에 디스크 I/O 병목이 발생한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;대안 아키텍처&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;쓰기 성능이 대단히 뛰어나고 시간 범위(Time-Range) 질의에 최적화된 NoSQL인 &lt;a href=&quot;https://assu10.github.io/dev/2026/06/05/architecture-nearby/#242-%EC%9C%84%EC%B9%98-%EC%9D%B4%EB%8F%99-%EC%9D%B4%EB%A0%A5-dbcassandra&quot;&gt;카산드라(Cassandra)&lt;/a&gt;나 시계열 전문 DB인 InfluxDB를 사용하는 것이 훨씬 바람직하다.&lt;/li&gt;
      &lt;li&gt;또는 데이터 유실 방지를 위해 &lt;a href=&quot;https://cwiki.apache.org/confluence/display/hive/languagemanual+orc&quot;&gt;ORC&lt;/a&gt;, &lt;a href=&quot;https://www.databricks.com/blog/what-is-parquet&quot;&gt;Parquet(파케이)&lt;/a&gt;, &lt;a href=&quot;https://www.ibm.com/think/topics/avro&quot;&gt;AVRO&lt;/a&gt; 같은 칼럼형 데이터 형식 가운데 하나를 사용하여 아마존 S3와 같은 객체 스토리지에 데이터를 직접 파일 형태로 저장하는 방식도 널리 쓰인다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;컬럼형-데이터-형식columnar-data-format이란&quot;&gt;💡컬럼형 데이터 형식(Columnar Data Format)이란?&lt;/h3&gt;

&lt;p&gt;Parquet(파케이), ORC, Avro 등은 대용량 빅데이터 분석 생태계에서 표준으로 사용되고 있는 데이터 저장 포맷이다.&lt;br /&gt;
일반적인 DB가 데이터를 저장하는 방식과 근본적인 차이가 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;행 지향(Row-oriented) 방식(RDBMS)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터를 저장할 때 첫 번째 사람의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[이름, 나이, 국가]&lt;/code&gt;, 두 번째 사람의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[이름, 나이, 국가]&lt;/code&gt; 순서로 행 전체를 가로로 묶어 디스크에 쓴다.&lt;/li&gt;
      &lt;li&gt;특정 유저 한 명의 전체 정보를 조회할 때(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT *&lt;/code&gt;)는 빠르지만, 수십억 건의 데이터 중 ‘나이의 평균’만 구하고 싶어도 필요없는 이름과 국가 데이터까지 디스크에서 전부 읽어야 하므로 성능이 느려진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;열 지향(Column-oriented) 방식(Parquet, ORC 등)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;디스크에 저장할 때 아예 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[모든 유저의 이름 모음]&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[모든 유저의 나이 모음]&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[모든 유저의 국가 모음]&lt;/code&gt;과 같이 세로(컬럼) 단위로 데이터를 묶어서 저장&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;행 지향 저장 구조] -&amp;gt; 가로로 저장
| 유저1_ID | 유저1_국가 | 유저1_시간 | 유저2_ID | 유저2_국가 | 유저2_시간 |

&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;열 지향 저장 구조] -&amp;gt; 세로&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;칼럼&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;로 묶어서 저장
| 유저1_ID, 유저2_ID... | 유저1_국가, 유저2_국가... | 유저1_시간, 유저2_시간... |
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;분석 시스템에서 컬럼형 형식이 강력한 이유&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;극적인 디스크 I/O 절감&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;‘지난 1시간 동안 한국에서 발생한 클릭 수’를 계산할 때, 시스템은 유저 id나 IP 컬럼이 저장된 디스크 영역은 보지 않고 딱 ‘국가’와 ‘시간’ 컬럼이 있는 물리 영역만 선별해서 읽는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;높은 압축률&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;같은 컬럼 안에는 동일한 데이터 타입만 존재하므로, 데이터의 중복성이 높아 압축 알고리즘을 적용했을 때 파일 용량이 최대 80~90%까지 줄어든다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2) 집계 데이터 관점&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;원시 데이터와 달리, 분 단위로 계산이 완료된 집계 결과 데이터는 성격이 완전히 다르다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;워크로드 분석&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;집계 데이터는 본질적으로 시간의 흐름에 따라 기록되는 시계열 데이터(Time-series Data)이다. (원시 데이터도 시계열 데이터임)&lt;/li&gt;
      &lt;li&gt;이 데이터를 처리하는 워크플로는 &lt;strong&gt;읽기 연산과 쓰기 연산이 둘 다 매우 많이 발생&lt;/strong&gt;하는 특징이 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;이유&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;매 분마다 집계 서비스가 연산한 최신 클릭 수 결과 데이터를 DB에 지속적으로 써야 하고, 동시에 수많은 광고주들이 자신의 대시보드를 새로고침하며 실시간 통계 수치를 끊임없이 조회하기 때문이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;통합 DB 전략&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;카산드라(Cassandra)는 고속 쓰기 뿐 아니라 시계열 형태의 인덱싱 및 다중 노드 기반 대용량 읽기 분산 처리 기능도 탁월하게 지원한다.&lt;/li&gt;
      &lt;li&gt;따라서 시스템의 복잡도를 낮추고 운영 효율성을 극대화하기 위해, &lt;strong&gt;원시 데이터와 집계 결과 데이터를 저장하는 데 동일한 유형의 데이터베이스(카산드라)를 활용&lt;/strong&gt;하는 것이 가능하며, 이를 강력히 추천한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;24-개략적-설계안-동기식의-한계와-비동기-스트림-도입&quot;&gt;2.4. 개략적 설계안: 동기식의 한계와 비동기 스트림 도입&lt;/h2&gt;

&lt;p&gt;초기 설계 시 생산자(로그 모니터)가 소비자(집계 서비스)로 데이터를 직접 푸시하는 &lt;strong&gt;동기식 파이프라인&lt;/strong&gt;을 구상하기 쉽다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/aggre.png&quot; alt=&quot;동기식 집계 워크 플로&quot; /&gt;&lt;/p&gt;

&lt;p&gt;하지만 트래픽이 몰리는 피크 타임에 소비자의 처리 용량을 넘어서는 순간 &lt;strong&gt;OOM&lt;/strong&gt; 오류로 시스템 전체가 다운될 수 있다.&lt;br /&gt;
이를 해결하기 위해 아파치 카프카같은 메시지 큐를 도입하여 &lt;strong&gt;비동기식 아키텍처&lt;/strong&gt;로 결합도를 끊어낸다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/exactly.png&quot; alt=&quot;비동기식 집계 워크 플로&quot; /&gt;&lt;/p&gt;

&lt;p&gt;로그 모니터, 집계 서비스, DB는 2개의 메시지 큐로 분리되어 있다.&lt;/p&gt;

&lt;p&gt;첫 번째 메시지 큐에는 아래와 같은 광고 클릭 이벤트 데이터가 기록된다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ad_id, click_timestamp, user_id, ip, country
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;두 번째 메시지 큐에는 2가지 유형의 데이터가 입력될 수 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;분 단위로 집계된 광고 클릭 수
    &lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ad_id, click_minute, count
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;분 단위로 집계한, 가장 많이 클릭한 상위 M개 광고
    &lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;update_time_minute, most_clicked_ads
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;집계-결과를-왜-바로-db에-기록하지-않을까&quot;&gt;💡집계 결과를 왜 바로 DB에 기록하지 않을까?&lt;/h3&gt;

&lt;p&gt;위의 비동기 아키텍처를 보면 집계 서비스 앞단과 뒷단에 각각 &lt;strong&gt;첫 번째 메시지 큐&lt;/strong&gt;와 &lt;strong&gt;두 번째 메시지 큐&lt;/strong&gt;가 위치하고, 이 영역이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;원자적 커밋(Atomic Commit)&lt;/code&gt;으로 묶여있다.&lt;/p&gt;

&lt;p&gt;이것은 스트림 처리 엔진(예: 아파치 Flink)이 제공하는 &lt;a href=&quot;https://flink.apache.org/2018/02/28/an-overview-of-end-to-end-exactly-once-processing-in-apache-flink-with-apache-kafka-too/&quot;&gt;&lt;strong&gt;‘정확히 한 번(Exactly-Once)’ 처리의 핵심 원리&lt;/strong&gt;&lt;/a&gt;이다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;집계 서비스는 첫 번째 큐에서 원시 이벤트를 꺼내온다.(Read)&lt;/li&gt;
  &lt;li&gt;메모리에서 1분간 집계를 수행한다.&lt;/li&gt;
  &lt;li&gt;그 결과를 두 번째 큐에 기록한다.(Write)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;연관성:&lt;/strong&gt; 1~3 과정을 하나의 &lt;strong&gt;분산 트랜잭션&lt;/strong&gt;으로 묶는다.&lt;br /&gt;
만일 3번 과정에서 에러가 나면, 첫 번째 큐에서 읽어왔던 위치(오프셋)도 읽지 않은 상태로 rollback하고, 두 번째 큐에 쓰려던 집계 결과도 취소한다.&lt;br /&gt;
두 메시지 큐 사이의 상태를 &lt;strong&gt;전부 성공하던가 전부 실패하게&lt;/strong&gt; 통제함으로써 데이터의 중복이나 누락을 완벽히 방어할 수 있게 된다.&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;25-집계-서비스-맵리듀스mapreduce와-dag-모델&quot;&gt;2.5. 집계 서비스: 맵리듀스(MapReduce)와 DAG 모델&lt;/h2&gt;

&lt;p&gt;수천만 건의 데이터를 매분 분산 처리하기 위해 시스템을 &lt;strong&gt;유향 비순환 그래프(DAG, Directed Acyclic Graph)&lt;/strong&gt; 모델 기반의 맵리듀스 패러다임으로 세분화한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;맵리듀스(MapReduce)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;대규모 데이터를 병렬로 분산 처리하기 위한 프로그래밍 모델&lt;/li&gt;
      &lt;li&gt;데이터를 쪼개고 변환하는 &lt;strong&gt;Map&lt;/strong&gt; 단계와, 변환된 데이터를 그룹화하여 합산하는 &lt;strong&gt;Reduce&lt;/strong&gt; 단계로 나눔&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DAG(유향 비순환 그래프)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;작업의 흐름이 단방향으로만 흘러가고, 절대 중간에 순환 회로(Loop)가 생기지 않는 구조적 모델&lt;/li&gt;
      &lt;li&gt;데이터가 맵 → 집계 → 리듀스 노드로 순차적으로 이동하며 병렬 처리됨을 보장함(= 맵리듀스 패러다임을 표현하기 위한 모델)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DAG 모델의 핵심은 아래 그림처럼 시스템은 맵/집계/리듀스 노드 등의 작은 컴퓨팅 단위로 세분화하여 &lt;strong&gt;각 노드는 한 가지 작업만 처리&lt;/strong&gt;하고 &lt;strong&gt;처리 결과를 다음 노드에 인계&lt;/strong&gt;하는 것이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/map.png&quot; alt=&quot;집계 서비스와 맵 연산&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ad_id % 2&lt;/code&gt; 에서 2는 집계 노드의 개수를 의미한다.&lt;br /&gt;
광고 종류가 5개든 500개든 상관없이, 분산 처리를 해주는 노드(집계 노드)가 3대라면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hash(ad_id)%3&lt;/code&gt; 공식을 써서 0번, 1번, 2번 서버로 데이터를 균등하게 분배(샤딩)하는 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;251-맵map-노드&quot;&gt;2.5.1. 맵(Map) 노드&lt;/h3&gt;

&lt;p&gt;입력 데이터를 읽어서 필터링하고 정규화하여 다운스트림으로 분배하는 역할을 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;데이터-정규화에-맵-노드가-필수일까&quot;&gt;💡데이터 정규화에 맵 노드가 필수일까?&lt;/h4&gt;

&lt;p&gt;과연 맵 노드가 필수일까? 카프카 파티션이나 태그를 구성한 후 집계 노드가 카프카를 직접 구독하도록 하면 안 되는 것일까?&lt;br /&gt;
그렇게 해도 되지만, 입력 데이터를 정리하거나 정규화해야 하는 경우에는 맵 노드가 필요하다.&lt;/p&gt;

&lt;p&gt;여러 서버에서 수집된 원시 로그는 포맷이 제각각이거나 불필요한 트래킹 파라미터가 섞여 있을 수 있다.&lt;br /&gt;
만일 집계 노드가 이 지저분한 데이터를 그대로 받으면 집계 속도가 급격히 떨어진다.&lt;/p&gt;

&lt;p&gt;맵 노드가 앞단에서 &lt;strong&gt;데이터 형식을 통일하고(정규화), 결측치(있어야 하는데 누락되어 비어있는 값)를 제거&lt;/strong&gt;하여 깔끔한 상태로 만들어서 넘겨주기 때문에 뒷단의 집계 노드는 오직 ‘연산’에만 집중하여 고성능을 낼 수 있다.&lt;/p&gt;

&lt;p&gt;또한, 광고 아이디 분배 제어권이 없을 때(= 데이터가 생성되는 방식에 대한 제어권이 없음) 동일한 ad_id가 다른 파티션으로 튀는 현상도 맵 노드가 해시 라우팅을 통해 
바로잡아 준다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;252-집계aggregation-노드&quot;&gt;2.5.2. 집계(Aggregation) 노드&lt;/h3&gt;

&lt;p&gt;ad_id별 광고 클릭 수를 매분 메모리 영역에서 1차적으로 결합(sum)한다.&lt;br /&gt;
맵리듀스 패러다임 관점에서는 리듀스 프로세스의 초기 단계에 해당한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;253-리듀스-노드&quot;&gt;2.5.3. 리듀스 노드&lt;/h3&gt;

&lt;p&gt;각 집계 노드가 메모리 내부 힙 구조를 통해 1차로 추려낸 상위 인기 광고 결과들을 최종적으로 취합하여, 시스템 전체 관점에서의 ‘지난 1분간 가장 많이 클릭된 상위 M개 광고’로 
최종 축약(Reduce)해 출력한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/reduce.png&quot; alt=&quot;리듀스 노드&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;254-맵리듀스와-dag모델-활용&quot;&gt;2.5.4. 맵리듀스와 DAG모델 활용&lt;/h3&gt;

&lt;p&gt;여기서 설계하는 시스템은 무한히 밀려드는 클릭 로그 스트림을 DAG 모델에 따라 분산처리한다.&lt;br /&gt;
대시보드가 요구하는 핵심 유스케이스 2가지를 이 모델 위에서 구현해보자.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1) 지난 N분간 ad_id 에 발생한 클릭 이벤트 수 집계&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;대량의 트래픽을 처리하기 위해 맵 노드가 데이터를 나누고 집계 노드가 이를 넘겨받아 처리한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/aggree2.png&quot; alt=&quot;클릭 이벤트 수 집계&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2) 지난 N분간 가장 많은 클릭이 발생한 상위 M개의 ad_id 집계&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;‘지난 N분간 가장 핫한 광고 상위 3개(혹은 100개)를 뽑아달라’는 요구사항을 처리하는 방식이다.&lt;br /&gt;
이 과정은 시스템 전체 자원을 효율적으로 쓰기 위해 2단계 축약 구조를 가진다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/dag.png&quot; alt=&quot;가장 많이 클릭된 상위 M개 광고 반환&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1)집계 노드의 1차 추출(Local Top N)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;각 집계 노드는 맵 노드로부터 자기가 담당한 ad_id들의 이벤트만 전달받음&lt;/li&gt;
  &lt;li&gt;이 노드들은 메모리 내부에서 &lt;strong&gt;힙(Heap) 데이터 구조&lt;/strong&gt;를 활용하여, 자기가 처리한 데이터 중에서 가장 많이 클릭된 상위 3개의 광고를 효율적으로 식별해 냄&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2)리듀스 노드의 최종 축약(Global Top N)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;집계 노드가 3대라면, 각 노드가 뽑은 3개씩의 결과가 모여 총 9개의 광고 후보가 리듀스 노드로 전송됨&lt;/li&gt;
  &lt;li&gt;리듀스 노드는 이 9개의 후보 데이터를 최종적으로 결합하고 비교하여, 시스템 전체 관점에서 지난 1분간 가장 많이 클릭된 진짜 상위 3개의 광고를 출력함&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;255-다차원-데이터-필터링을-위한-스타-스키마star-schema&quot;&gt;2.5.5. 다차원 데이터 필터링을 위한 스타 스키마(Star Schema)&lt;/h3&gt;

&lt;p&gt;광고주는 단순히 전체 클릭 수만 보는 것이 아니라 ‘한국에서 발생한 ad001 클릭 수만 필터링해서 보고 싶다’와 같은 복잡한 조건을 요구한다.&lt;br /&gt;
이를 실시간 스트림 환경에서 효율적으로 서빙하기 위해 &lt;a href=&quot;https://learn.microsoft.com/en-us/power-bi/guidance/star-schema&quot;&gt;&lt;strong&gt;스타 스키마(Star Schema)&lt;/strong&gt;&lt;/a&gt; 기법을 아키텍처에 반영한다.&lt;/p&gt;

&lt;p&gt;필터링 기준을 반영하여 분 단위로 미리 쪼개어 저장하는 &lt;strong&gt;집계 결과 데이터 테이블&lt;/strong&gt;의 구조는 다음과 같다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;ad_id&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;click_minute&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;country&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;count&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;202101010001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;USA&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;100&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;202101010001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;GPB&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;200&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;202101010001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;others&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3000&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad002&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;202101010001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;USA&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad002&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;202101010001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;GPB&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;25&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;ad002&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;202101010001&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;others&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;12&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;스타-스키마star-schema란&quot;&gt;💡스타 스키마(Star Schema)란?&lt;/h4&gt;

&lt;p&gt;데이터 웨어하우스(DW)와 비즈니스 인텔리전스(BI) 분석에서 가장 널리 쓰이는 정규화 모델이다.&lt;br /&gt;
중앙에 클릭 수나 통계 수치를 저장하는 대형 &lt;strong&gt;‘사실 테이블(Fact Table)’&lt;/strong&gt;을 배치하고, 그 주변에 필터링의 기준이 되는 &lt;strong&gt;‘차원 테이블(Dimension Table: 국가, 유저, 디바이스 등)’&lt;/strong&gt;들을 
별 모양(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*&lt;/code&gt;) 구조로 결합하여 쿼리 성능을 극대화하는 설계 기법이다.&lt;br /&gt;
데이터 분석가들이 사용하는 필터링 필드들을 바로 이 &lt;strong&gt;차원(Dimension)&lt;/strong&gt;이라고 부른다.&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;스타 스키마 접근법의 장점&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;직관적인 구조&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;모델 자체가 직관적이어서 데이터 파이프라인을 설계 및 구축하기 매우 쉬움&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;기존 서비스 재사용 가능&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;새로운 필터링 조건(예: 디바이스 종류 차원)이 추가되더라도, 기존 집계 서비스의 로직을 그대로 재사용하여 차원만 늘려주면 되므로 별도의 복잡한 컴포넌트가 필요 없음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;초고속 질의 응답&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저가 대시보드에서 필터를 걸고 조회 버튼을 누르는 순간, 이미 분 단위와 국가 단위로 쪼개져 계산이 완료된 데이터를 즉시 가져오므로 응답 속도가 매우 빠름&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 접근법은 치명적인 트레이드오프가 있다.&lt;br /&gt;
필터링하려는 기준(차원)이 많아질수록 미리 계산해서 저장해야 하는 데이터 조합의 수인 &lt;strong&gt;Bucket과 레코드가 기하급수적으로 증폭&lt;/strong&gt;된다는 점이다.&lt;/p&gt;

&lt;p&gt;예를 들어 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;광고 id(200만개) * 국가(200개) * 디바이스(5개) * 연령대(10개)&lt;/code&gt; 조합을 매 분마다 생성하게 되면,&lt;br /&gt;
클릭이 발생하지 않은 빈 버킷들까지 데이터베이스 레코드를 차지하게 되어 스토리지 부하가 커질 수 있으므로 꼭 필요한 핵심 차원 위주로 선별 설계해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-상세-설계-1-실시간-데이터-스트리밍과-시간의-제어&quot;&gt;3. 상세 설계 1: 실시간 데이터 스트리밍과 시간의 제어&lt;/h1&gt;

&lt;p&gt;여기서는 실시간 스트림 처리 엔진의 내부 동작 메커니즘에 대해 알아본다.&lt;br /&gt;
데이터의 실시간성을 확보하면서도 과거 데이터를 유연하게 다루는 법, 분산 시스템에서 가장 까다로운 요소인 ‘시간(Time)’을 제어하는 전략에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-스트리밍flink-vs-일괄-처리mapreduce&quot;&gt;3.1. 스트리밍(Flink) vs 일괄 처리(MapReduce)&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/exactly.png&quot; alt=&quot;비동기식(스트림 처리) 집계 워크 플로&quot; /&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;특성&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;서비스 (온라인 시스템)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;일괄 처리 시스템 (오프라인)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;스트리밍 시스템 (실시간에 가까운 처리)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;응답성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;클라이언트에게 초저지연으로 빠르게 응답&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;클라이언트에게 즉시 응답할 필요 없음&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;클라이언트에게 즉시 응답할 필요 없음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;입력 데이터&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;사용자 개별 요청 (유한함)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;유한한 크기를 갖는 큰 규모의 백업 데이터&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;입력에 경계가 없음 (&lt;strong&gt;무한 스트림&lt;/strong&gt;)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;출력 결과&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;클라이언트에 대한 직접 응답&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;구체화 뷰(Materialized View),&lt;br /&gt;집계 지표&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;구체화 뷰, 실시간 집계 결과 지표&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;성능 지표&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;가용성(Availability), 지연 시간&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;처리량(Throughput)&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;처리량과 지연 시간 모두 중요&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;대표 사례&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;온라인 쇼핑몰 주문 API&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;아파치 하둡 맵리듀스 (MapReduce)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;a href=&quot;https://flink.apache.org/&quot;&gt;&lt;strong&gt;아파치 플링크 (Apache Flink)&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;여기서는 스트림 처리와 일괄 처리 방식 모두를 사용한다.&lt;br /&gt;
스트림 처리는 데이터를 오는 대로 처리하고 거의 실시간으로 집계된 결과를 생성하는데 사용하며, 일괄 처리는 이력 데이터를 백업하기 위해 활용한다.&lt;/p&gt;

&lt;p&gt;일괄 및 스트리밍 처리 경로를 동시에 지원하는 시스템 아키텍처를 &lt;a href=&quot;https://www.databricks.com/blog/what-is-lambda-architecture&quot;&gt;람다(Lambda)&lt;/a&gt;라고 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;람다-아키텍처의-한계를-극복하는-카파kappa-아키텍처&quot;&gt;💡람다 아키텍처의 한계를 극복하는 &lt;a href=&quot;https://hazelcast.com/foundations/software-architecture/kappa-architecture/&quot;&gt;카파(Kappa) 아키텍처&lt;/a&gt;&lt;/h3&gt;

&lt;p&gt;실시간 대시보드와 과거 데이터 백업을 동시에 만족하기 위해 과거에는 &lt;strong&gt;람다(Lambda) 아키텍처&lt;/strong&gt;를 많이 사용했다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;람다 아키텍처 구조&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터를 실시간으로 빠르게 처리하는 ‘스피드 계층(스트리밍 엔진)’과, 모든 데이터를 저장해 두었다가 밤마다 돌리는 ‘배치 계층(일괄 처리 엔진)’을 동시에 두는 방식&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;람다 아키텍처의 단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;동일한 집계 로직을 &lt;strong&gt;하둡(배치용 코드)과 플링크(실시간용 코드)로 각각 두 벌씩 따로 개발하고 관리&lt;/strong&gt;해야 함&lt;/li&gt;
      &lt;li&gt;두 계층의 계산 결과가 미세하게 어긋나는 데이터 정합성 문제가 빈번히 발생&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 단점을 완벽히 해결한 것이 바로 이번 설계에 사용된 &lt;strong&gt;카파(Kappa) 아키텍처&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/kappa.png&quot; alt=&quot;람다 아키텍처와 카파 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;p&gt;카파 아키텍처의 원리는 ‘일괄 처리 엔진을 아예 없애고, &lt;strong&gt;실시간 스트리밍 엔진 하나만 활용&lt;/strong&gt;‘하는 것이다.&lt;br /&gt;
과거 데이터의 재처리가 필요하다면, 일괄 처리 코드를 새로 돌리는 것이 아니라 카프카 같은 대용량 메시지 큐의 오프셋을 과거 시점으로 돌려 &lt;strong&gt;실시간 스트리밍 파이프라인으로 다시 흘려보내는(Replay) 방식&lt;/strong&gt;이다.&lt;br /&gt;
코드 한 벌로 실시간과 이력 데이터 처리를 모두 해결할 수 있어 빅데이터 시스템의 표준으로 자리 잡았다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-장애-복구를-위한-이력-데이터-재처리replay-파이프라인&quot;&gt;3.2. 장애 복구를 위한 이력 데이터 재처리(Replay) 파이프라인&lt;/h2&gt;

&lt;p&gt;카파 아키텍처 모델을 기반으로 한 실제 &lt;strong&gt;데이터 재계산(Historical Data Replay) 흐름&lt;/strong&gt;은 아래와 같이 명확히 격리된 경로를 따른다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/recalc.png&quot; alt=&quot;재계산 서비스&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;원시 데이터 검색:&lt;/strong&gt; 재계산 서비스가 카산드라나 S3 같은 원시 데이터 저장소에서 버그가 발생했던 시점의 과거 로그를 일괄적으로 추출&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;전용 집계 서비스로 라우팅:&lt;/strong&gt; 추출된 과거 데이터는 현재 서비스 중인 실시간 스트리밍 파이프라인에 간섭하지 않도록, &lt;strong&gt;‘재계산 전용 집계 서비스’&lt;/strong&gt; 노드로 전송됨&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;최종 반영:&lt;/strong&gt; 재계산 전용 집계 서비스가 산출한 결과는 두 번째 메시지 큐를 거쳐 집계 결과 데이터베이스에 갱신됨&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-기준-시간-설정의-트레이드-오프-이벤트-시각-vs-처리-시각&quot;&gt;3.3. 기준 시간 설정의 트레이드 오프: 이벤트 시각 vs 처리 시각&lt;/h2&gt;

&lt;p&gt;무한한 스트림 데이터에서 ‘1분 단위’를 끊을 때, 기준이 되는 시간은 두 가지로 나뉜다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;이벤트 시각:&lt;/strong&gt; 광고 클릭이 사용자 디바이스에서 &lt;strong&gt;실제 발생한 시각&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;처리 시각:&lt;/strong&gt; 집계 서버에 로그가 도착하여 &lt;strong&gt;연산을 처리한 시스템의 시각&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;네트워크 지연이나 메시지 큐를 통한 비동기적 처리 환경에서는 ‘광고 클릭이 일어난 시각’과 ‘집계 서버가 이를 실제로 계산하는 시각’ 사이에 격차가 발생할 수 있다.&lt;br /&gt;
따라서 아래 두 방안의 장단점을 비교하여 시스템의 기준 시간을 결정해야 한다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt; &lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;장점&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;단점&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;이벤트 발생 시각&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;광고 클릭 시점을 정확히 알고 있는 클라이언트(디바이스)의 타임스탬프를 쓰므로 &lt;strong&gt;집계 결과가 비즈니스적으로 정확함&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;클라이언트가 생성한 정보에 의존하므로, 사용자 기기의 시각이 잘못 설정되어 있거나 악성 사용자가 &lt;strong&gt;타임스탬프를 고의로 조작(광고 사기 등)하는 문제&lt;/strong&gt;에서 자유로울 수 없음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;처리 시각&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;서버의 타임스탬프를 활용하므로 클라이언트보다 훨씬 안정적이며 오프셋 관리가 쉬움&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;네트워크 장애 등으로 이벤트가 시스템에 한참 뒤에 도착하는 경우, 엉뚱한 시간 버킷에 합산되므로 &lt;strong&gt;정산 및 통계 결과가 부정확해짐&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;광고 클릭 집계 시스템에서 &lt;strong&gt;데이터의 정확도는 그 어떤 가치보다 최우선&lt;/strong&gt;으로 고려되어야 한다.&lt;br /&gt;
처리 시각을 선택했다가 네트워크 장애로 클릭 로그가 몇 시간 뒤에 몰려 들어오면 광고주에게 엉뚱한 시간대의 비용을 청구하게 되는 치명적인 결함이 발생한다.&lt;/p&gt;

&lt;p&gt;따라서 여기에서는 악성 사용자의 타임스탬프 조작 리스크를 감수하더라도 &lt;strong&gt;이벤트 발생 시각을 사용할 것을 권장&lt;/strong&gt;한다.&lt;br /&gt;
(고의 조작이나 비정상 시각 이벤트는 파이프라인 앞단의 &lt;a href=&quot;https://www.google.com/ads/adtrafficquality/&quot;&gt;광고 사기/위험 제어 컴포넌트&lt;/a&gt;에서 필터링하도록 격리)&lt;/p&gt;

&lt;p&gt;이 때, 이벤트 발생 시각을 채택함으로써 필연적으로 발생하는 &lt;strong&gt;‘늦게 도착하는 이벤트’&lt;/strong&gt; 문제는 해결하기 위해 스트림 처리 생태계의 표준 기술인 &lt;strong&gt;워터마크(Watermark)&lt;/strong&gt;를 도입한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;아래 그림은 광고 클릭 이벤트를 1분 단위로 끊어지는 텀블링 윈도우(tumbling window)를 사용하여 집계하는 사례이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/window1.png&quot; alt=&quot;집계 윈도에 누락되는 이벤트&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이벤트 발생 시각을 기준으로 이벤트가 어떤 윈도에 속하는지 결정하면 윈도1은 이벤트2를 집계하지 못하게 되고, 윈도 3은 이벤트 5를 집계하지 못하게 된다.&lt;br /&gt;
이벤트가 집계 윈도가 끝나는 시점보다 살짝 늦게 도착하기 때문이다.&lt;/p&gt;

&lt;p&gt;윈도1이 지연된 이벤트를 버리고 윈도2가 집계하면 어떻게 될까?&lt;br /&gt;
만일 지연된 이벤트를 늦게 도착했다는 이유로 윈도2(다음 시간 버킷)가 흡수해버리면 데이터의 정합성이 완전히 깨진다.&lt;br /&gt;
광고주는 12시 정각 캠페인에 예산을 쏟았는데, 데이터가 12:01 윈도에 집계되면 과금 정산 오류가 발생하고 광고 사기(Ad Fraud) 분석가들이 시간별 패턴을 분석할 때 왜곡이 일어난다.&lt;br /&gt;
데이터는 반드시 실제 사실(이벤트 시각)에 기반하여 올바른 타임 버킷에 담겨야 한다.&lt;/p&gt;

&lt;p&gt;이 문제를 해결하는 워터마크의 구체적인 동작 방식은 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/watermark.png&quot; alt=&quot;워터마크&quot; /&gt;&lt;/p&gt;

&lt;p&gt;집계 윈도가 12:01에 끝났더라도 늦게 도착하는 이벤트가 있을 수 있으니 여분의 시간을 더 집계할 수 있도록 타임라인에 가상의 경계선(워터마크)을 흘려보낸다.&lt;br /&gt;
이 대기 시간 덕분에 12:01:05초에 도착한 이벤트2도 원래 제자리인 윈도1(12:00 버킷) 속으로 합류할 수 있게 된다.&lt;/p&gt;

&lt;p&gt;워터마크 기법으로도 시간이 한참 흐른 후에 시스템에 도달하는 이벤트는 처리할 수 없다.&lt;br /&gt;
발생 확률이 낮은 이벤트 처리를 위해 시스템을 복잡하게 설계하면 투자 대비 효능(ROI, Return On Investment)이 떨어지며, 사소한 데이터 오류는 하루치 데이터 처리를 
마감할 때 조정할 수 있다.&lt;/p&gt;

&lt;p&gt;워터마크 구간을 길게 잡으면 데이터의 정확도는 100%에 가까워지지만 시스템이 결과를 확정 짓기 위해 대기해야 하므로 전반적인 지연 시간이 늘어난다.&lt;br /&gt;
반대로 짧게 잡으면 응답은 빨라지지만 지연 도착한 데이터를 유실할 확률이 높아진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;34-스트림-데이터-윈도window&quot;&gt;3.4. 스트림 데이터 윈도(Window)&lt;/h2&gt;

&lt;p&gt;스트림 처리 엔진에서 무한한 데이터를 시간 단위로 쪼개는 윈도 전략은 크게 4가지가 존재한다.&lt;br /&gt;
기능 요구사항을 만족하기 위해 이들의 특징을 정확히 비교해야 한다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;윈도우 유형&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;시간 경계선&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;데이터 중첩 여부&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;주요 비즈니스 활용 사례&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;텀블링 윈도 (Tumbling)&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;고정된 크기 (예: 1분)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;중첩 없음&lt;/strong&gt; (서로 겹치지 않음)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;[요구사항 1]&lt;/strong&gt; 매 분 정각마다 발생한 순수 클릭 수 집계&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;슬라이딩 윈도 (Sliding)&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;시간에 따라 미끄러지듯 이동&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;중첩 발생&lt;/strong&gt; (서로 겹침)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;[요구사항 2]&lt;/strong&gt; 지난 5분간 가장 인기 있는 상위 광고 추출&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;고정 윈도 (Fixed)&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;특정 시각 기준으로 고정&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;중첩 없음&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;사실상 텀블링 윈도와 기술적 개념 동일&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;호핑 윈도 (Hopping)&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;윈도 크기와 전진 간격이 다름&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;설정에 따라 중첩 가능&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1시간짜리 윈도가 5분 간격으로 전진하며 통계 산출&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;텀블링 윈도는 시간을 같은 크기의 겹치지 않는 구간으로 분할한다. 따라서 매 분 발생한 클릭 이벤트를 집계하기에 적합하다. (요구사항 1)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/tumbling.png&quot; alt=&quot;텀블링 윈도&quot; /&gt;&lt;/p&gt;

&lt;p&gt;슬라이딩 윈도는 데이터 스트림을 미끄러져 나아가면서 같은 시간 구간 안에 있는 이벤트를 집계하는 방식으로, 서로 겹칠 수 있다.
따라서 두 번째 요구사항, 즉 지난 N분간 가장 많이 클릭된 상위 M개 광고를 알아내기에 적합하다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/sliding.png&quot; alt=&quot;슬라이딩 윈도&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;고정-윈도와-호핑-윈도란&quot;&gt;💡고정 윈도와 호핑 윈도란?&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;고정 윈도(Fixed Window)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;빅데이터 생태계에서 고정 윈도는 &lt;strong&gt;텀블링 윈도와 완전히 같은 개념&lt;/strong&gt;으로 통용된다.&lt;/li&gt;
      &lt;li&gt;시간 축을 1분, 5분 등 고정된 격자 모양으로 칼같이 쪼개며, 윈도와 윈도 사이에 빈틈이나 겹치는 영역이 전혀 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;호핑 윈도(Hopping Window)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;슬라이딩 윈도의 일종으로, 윈도의 크기와 전진하는 간격(Slide)을 다르게 설정하는 방식이다.&lt;/li&gt;
      &lt;li&gt;예) 윈도 크기는 10분으로 하되, 1분마다 앞으로 점프(Hop)&lt;/li&gt;
      &lt;li&gt;1분 주기로 지난 10분간의 누적 추이를 매끄럽게 모니터링하고 싶을 때 사용한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;호핑 윈도와 슬라이딩 윈도 차이&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;호핑 윈도&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;윈도우가 움직이는 기준이 ‘고정된 시계의 정각’이다.
        &lt;ul&gt;
          &lt;li&gt;예) 12:00, 12:01 처럼 딱딱 떨어지는 분 단위 정각 격자에 맞춰 Hop 한다. 데이터가 있든 없든 시간은 흘러간다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;슬라이딩 윈도&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;고정된 시간 격자가 없다. ‘데이터(이벤트)가 들어오는 그 순간’ 미끄러지듯 움직인다.&lt;/li&gt;
      &lt;li&gt;예) 광고 클릭이 12:00:15에 발생했다면, 그 순간을 기준으로 정확히 뒤로 3분을 넘겨서 11:57:15~12:00:15 범위의 윈도를 그 자리에서 만들어낸다.&lt;/li&gt;
      &lt;li&gt;데이터가 들어올 때만 윈도가 미끄러진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-상세-설계-2-정확히-한-번-전달-보장&quot;&gt;4. 상세 설계 2: ‘정확히 한 번’ 전달 보장&lt;/h1&gt;

&lt;p&gt;실시간 광고 클릭 집계 시스템에서 데이터 정합성은 비즈니스의 생존과 직결된다.&lt;br /&gt;
클릭 수의 오차는 광고주에게 수백만 달러의 부당 청구로, 매체사에는 정산 신뢰도 하락으로 이어지기 때문이다.&lt;/p&gt;

&lt;p&gt;분산 환경의 악조건 속에서도 데이터를 정확히 한 번(Exactly-Once) 처리하기 위한 end-to-end 무결성 아키텍처에 대해 알아보자.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;이벤트의 중복 처리를 어떻게 피할 수 있는가?&lt;/li&gt;
  &lt;li&gt;모든 이벤트의 처리를 어떻게 보장할 수 있는가?&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-메시지-큐의-3가지-전달-방식&quot;&gt;4.1. 메시지 큐의 3가지 전달 방식&lt;/h2&gt;

&lt;p&gt;카프카와 같은 분산 메시지 큐는 네트워크 장애와 대기 상태를 극복하기 위해 3가지 유형의 데이터 전달 방식을 지원한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/06/12/architecture-distributed-message-queue-architecture/#4101-%EC%B5%9C%EB%8C%80-%ED%95%9C-%EB%B2%88at-most-once&quot;&gt;최대 한 번(at-most once)&lt;/a&gt;&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메시지가 유실될 수는 있지만, 절대 중복되지는 않음.&lt;/li&gt;
      &lt;li&gt;따라서 광고 정산에서는 절대 사용될 수 없음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/06/12/architecture-distributed-message-queue-architecture/#4102-%EC%B5%9C%EC%86%8C-%ED%95%9C-%EB%B2%88at-least-once&quot;&gt;최소 한 번(at-least once)&lt;/a&gt;&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메시지가 절대 유실되지는 않지만, 네트워크 재시도로 인해 중복 처리될 수 있음&lt;/li&gt;
      &lt;li&gt;구현이 쉬워 대안으로 쓰이지만, 엄청난 돈의 정산 오차 리스크가 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/06/12/architecture-distributed-message-queue-architecture/#4103-%EC%A0%95%ED%99%95%ED%9E%88-%ED%95%9C-%EB%B2%88exactly-once&quot;&gt;정확히 한 번(exactly once)&lt;/a&gt;&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시스템에 어떤 장애가 발생하더라도, 메시지가 유실되거나 중복되지 않고 오직 한 번만 반영됨&lt;/li&gt;
      &lt;li&gt;본 시스템의 필수 요구사항&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;약간의 중복이 괜찮다면 대체로 ‘최소 한 번’이 적절하지만 여기서는 그렇지 않다.&lt;br /&gt;
데이터의 몇 퍼센트 차이가 수백만 달러 차이로 이어질 수 있으므로 ‘정확히 한 번’ 방식을 권장한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;옐프yelp-사례와-아파치-플링크flink의-정확히-한-번-메커니즘&quot;&gt;💡옐프(Yelp) 사례와 아파치 플링크(Flink)의 ‘정확히 한 번’ 메커니즘&lt;/h3&gt;

&lt;p&gt;옐프는 미국의 매우 유명한 지역 비즈니스 리뷰 및 매칭 광고 플랫폼 기업이다.&lt;br /&gt;
이들이 대규모 광고 과금 시스템을 운영하여 ‘정확히 한 번’을 어떻게 엔지니어링했는지 &lt;a href=&quot;https://www.youtube.com/watch?v=hzxytnPcAUM&quot;&gt;전 세계 기술 컨퍼런스에서 발표한 사례&lt;/a&gt;가 업계의 표준 레퍼런스로 통용된다.&lt;/p&gt;

&lt;p&gt;옐프와 같은 글로벌 대기업들이 end-to-end 정확히 한 번을 달성하기 위해 채택하는 핵심 기술이 바로 아파치 플링크(Flink)이다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://flink.apache.org/2018/02/28/an-overview-of-end-to-end-exactly-once-processing-in-apache-flink-with-apache-kafka-too/&quot;&gt;Apache Flink 공식 블로그 (2018년 2월 28일 자 포스트)&lt;/a&gt; 의 내용을 보면 아래와 같다.&lt;/p&gt;

&lt;p&gt;플링크는 데이터 스트림 사이에 체크포인트 배리어(Checkpoint Barrier)라는 특수한 가상 가이드라인을 주입한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;1단계: 예비 커밋(Pre-commit)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;첫 번째 메시지 큐(업스트림 카프카)에서 데이터를 읽어 집계를 수행하다가 ‘배리어’를 만나는 순간, 집계 노드는 현재까지의 상태(오프셋, 메모리 내부 클릭 수)를 S3/HDFS와 같은 지속성 스토리지에 스냅숏으로 임시 저장&lt;/li&gt;
      &lt;li&gt;동시에, 계산된 집계 결과를 두 번째 메시지 큐(다운스트림 카프카)에 던지면서 ‘아직 확정은 아니니 대기해’라는 상태로 ‘예비 커밋’ 트랜잭션을 걸어둠&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2단계: 최종 커밋(Commit)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;중앙의 코디네이터가 시스템 내부의 모든 분산 노드가 무사히 스냅숏 저장을 완료했음을 확인하면, 비로소 다운스트림 카프카에 ‘트랜잭션 확정(Commit)해’라고 신호를 보냄&lt;/li&gt;
      &lt;li&gt;이제 대시보드와 DB 기록 프로세스가 이 데이터를 읽어갈 수 있음&lt;/li&gt;
      &lt;li&gt;만일, 중간에 한 노드라도 오류나면 전체 과정을 롤백함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;42-중복-데이터-처리-아키텍처&quot;&gt;4.2. 중복 데이터 처리 아키텍처&lt;/h2&gt;

&lt;p&gt;데이터 중복은 크게 &lt;strong&gt;클라이언트 측 요인&lt;/strong&gt;과 &lt;strong&gt;서버 측 장애 요인&lt;/strong&gt; 두 가지 경로로 인입된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;클라이언트 측 요인: 광고 사기(Ad Fraud)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;동일한 사용자가 악의적인 목적으로 단시간에 클릭을 수천 번 매크로로 연타하는 경우이다.&lt;/li&gt;
      &lt;li&gt;악의적인 의도로 전송되는 중복 이벤트는 광고 사기/위험 제어 컴포넌트로 처리한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;서버 측 요인: 분산 노드 장애&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;집계 서비스 노드가 연산을 처리하던 도중 갑자기 다운되어 업스트림 서비스가 이벤트 메시지에 대한 응답을 받지 못했다면, 자신이 카프카에서 어디까지 읽었는지 기록하는 ‘오프셋’ 위치가 꼬이면서 중복 데이터가 발생한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;구글-가이드-기반-광고-사기위험-제어-컴포넌트의-역할&quot;&gt;💡구글 가이드 기반 광고 사기/위험 제어 컴포넌트의 역할&lt;/h3&gt;

&lt;p&gt;구글의 &lt;a href=&quot;https://www.google.com/ads/adtrafficquality/&quot;&gt;Ad Traffic Quality&lt;/a&gt; 가이드라인에 따르면, 시스템 초입에 &lt;strong&gt;위험성 통제 엔진(Risk Control Engine)&lt;/strong&gt;을 배치해야 한다.&lt;br /&gt;
이 컴포넌트는 비정상적인 클릭 스트림, 봇 트래픽, 실수로 인한 더블 클릭 등을 머신러닝 알고리즘으로 실시간 판별하여 집계 파이프라인에 들어가기 전에 차단하는 방어막 역할을 수행한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;43-분산-트랜잭션이-필요한-이유&quot;&gt;4.3. 분산 트랜잭션이 필요한 이유&lt;/h3&gt;

&lt;p&gt;집계 서비스 노드가 장애가 났을 때 데이터가 중복되거나 손실되는 과정을 시퀀스 다이어그램으로 추적해보자.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;업스트림 카프카: 로그 모니터로부터 가공되지 않은 로그를 받아내는 첫 번째 메시지 큐&lt;/li&gt;
  &lt;li&gt;다운스트림 카프카: 집계 서비스 노드가 분 단위 연산을 완료한 통계 결과 데이터를 받아내는 두 번째 메시지 큐&lt;/li&gt;
&lt;/ul&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
  participant U as 업스트림&amp;lt;br/&amp;gt;(카프카)
  participant S as 집계 서비스 노드
  participant D as 다운스트림&amp;lt;br/&amp;gt;(카프카)

%% 집계 서비스 노드의 긴 동작 블록 시작
  activate S
  S-&amp;gt;&amp;gt;U: 1. 이벤트 수집 (폴)

%% 업스트림의 동작 블록 시작
  activate U
  U-&amp;gt;&amp;gt;S: 2. 오프셋 100부터 소비
  S-&amp;gt;&amp;gt;S: 3. 100부터 110까지의 이벤트 집계

  S-&amp;gt;&amp;gt;D: 4. 집계 결과 전송
  D--&amp;gt;&amp;gt;S: 5. 수신 응답

  S-xU: 6. 110까지 소비하였음을 응답
%% 업스트림의 동작 블록 종료
  deactivate U

  Note over U,D: 6단계를 마치지 못하고 장애를 내면&amp;lt;br/&amp;gt;오프셋 100부터 다시 소비하여야 하므로&amp;lt;br/&amp;gt;데이터 중복이 발생
%% 집계 서비스 노드의 긴 동작 블록 종료
  deactivate S
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;이 노드는 업스트림 카프카에 오프셋을 저장하여 데이터 소비 상태를 관리한다.&lt;/p&gt;

&lt;p&gt;이 데이터 손실과 중복을 해결하려면 분산 파일 시스템(HDFS/S3)에 오프셋을 직접 관리하는 것이다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
  participant U as 업스트림&amp;lt;br/&amp;gt;(카프카)
  participant S as 집계 서비스 노드
  participant H as HDFS / S3
  participant D as 다운스트림&amp;lt;br/&amp;gt;(카프카)

# 1번 시작과 동시에 업스트림과 집계 서비스 노드 모두 활성화
  activate U
  activate S
  S-&amp;gt;&amp;gt;U: 1. 이벤트 수집 (폴)
  U-&amp;gt;&amp;gt;S: 2. 오프셋 100부터 소비

# 3.1에서 활성화되어 3.2에서 정확히 종료
  activate H
  S-&amp;gt;&amp;gt;H: 3.1 오프셋 확인
  S-&amp;gt;&amp;gt;S: 3. 100부터 110까지의 이벤트 집계
  S-&amp;gt;&amp;gt;H: 3.2 오프셋 저장
  deactivate H

  Note over S,H: 메시지 손실이 발생할 수도 있음

# 4번에서 활성화되어 5번에서 정확히 종료
  activate D
  S-&amp;gt;&amp;gt;D: 4. 집계 결과 전송
  D-&amp;gt;&amp;gt;S: 5. 수신 확인 응답 전송
  deactivate D

# 6번 응답이 완료되면서 업스트림과 집계 서비스 노드 모두 종료
  S-&amp;gt;&amp;gt;U: 6. 업스트림에 새 오프셋 110 응답
  deactivate U
  deactivate S
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;하지만 이 방안에도 문제는 있다.&lt;br /&gt;
집계 결과를 다운스트림으로 전송하기 전에 오프셋을 HDFS나 S3에 저장한 직후에 집계 서비스 노드에 장애가 발생하여 4단계를 완료하지 못하면, 메시지 유실이 발생한다.&lt;br /&gt;
따라서 데이터 손실을 막으려면 다운스트림에서 집계 결과 수신 확인 응답을 받은 후 오프셋을 저장해야 한다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Upstream as 업스트림&amp;lt;br/&amp;gt;(카프카)
    participant AggNode as 집계 서비스 노드
    participant Storage as HDFS / S3
    participant Downstream as 다운스트림&amp;lt;br/&amp;gt;(카프카)

    activate Upstream
    activate AggNode
    AggNode-&amp;gt;&amp;gt;Upstream: 1. 이벤트 수집 (폴)
    Upstream-&amp;gt;&amp;gt;AggNode: 2. 오프셋 100부터 소비
    
    AggNode-&amp;gt;&amp;gt;+Storage: 3.1 오프셋 확인
    deactivate Storage
    
    AggNode-&amp;gt;&amp;gt;AggNode: 3. 100부터 110까지의&amp;lt;br/&amp;gt;이벤트 집계
    
    rect rgb(255, 218, 224)
        note over AggNode, Downstream: 분산 트랜잭션
        AggNode-&amp;gt;&amp;gt;+Downstream: 4. 집계 결과 전송
        AggNode-&amp;gt;&amp;gt;+Storage: 5. 오프셋 저장
        deactivate Storage
        Downstream-&amp;gt;&amp;gt;-AggNode: 6. 수신 확인 응답 전송
    end
    
    AggNode-&amp;gt;&amp;gt;Upstream: 7. 업스트림에&amp;lt;br/&amp;gt;새 오프셋 110 응답
    deactivate AggNode
    deactivate Upstream
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동작 원리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터를 4번 과정으로 두 번째 큐에 던지는 행위와, 5번 과정으로 외부 스토리지에 완료 오프셋(110)을 기록하는 행위를 &lt;strong&gt;하나의 분산 트랜잭션&lt;/strong&gt;으로 묶는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장애 발생 시&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;4번은 성공했는데 5번 실행 직전에 노드가 다운된다면, 트랜잭션 매니저가 4번 전송 건을 다운스트림 카프카에서 롤백한다.&lt;/li&gt;
      &lt;li&gt;새로 살아난 노드는 스토리지에서 마지막 기록이었던 오프셋 100번부터 안전하게 데이터를 다시 읽어 처리하므로, &lt;strong&gt;단 하나의 데이터 유실도, 단 한 건의 데이터 중복도 발생하지 않는 ‘정확히 한 번’이 완벽하게 실현&lt;/strong&gt;된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-시스템-규모-확장-및-결함-내성-확보&quot;&gt;5. 시스템 규모 확장 및 결함 내성 확보&lt;/h1&gt;

&lt;p&gt;매년 30%씩 트래픽이 늘어나고 3년마다 규모가 2배가 되는 대용량 환경을 견디려면, 시스템의 전 계층이 독립적으로 Scale-out 할 수 있는 구조를 갖추어야 한다.&lt;br /&gt;
메시지 큐, 집계 서비스, DB의 세 가지 구성 요소를 확장하며 마주치는 이슈와 &lt;strong&gt;핫스팟(Hotspot)&lt;/strong&gt; 트래픽 제어법에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;51-메시지-큐의-규모-확장&quot;&gt;5.1. 메시지 큐의 규모 확장&lt;/h2&gt;

&lt;p&gt;카프카 패러다임에서 파이프라인의 양 끝단에 위치한 인스턴스들은 상호 결합도가 낮아 독립적인 확장이 용이하다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/06/12/architecture-distributed-message-queue-architecture/#494-producer%EC%9D%98-%ED%99%95%EC%9E%A5%EC%84%B1&quot;&gt;&lt;strong&gt;생산자(Producer)의 확장성&lt;/strong&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;로그를 수집해 카프카로 전달하는 생산자 인스턴스 수에는 아키텍처적 제한을 두지 않으므로, 필요할 때마다 서버를 늘리는 방식으로 확장성을 쉽게 달성할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/06/12/architecture-distributed-message-queue-architecture/#495-consumer%EC%9D%98-%ED%99%95%EC%9E%A5%EC%84%B1&quot;&gt;&lt;strong&gt;소비자(Consumer)의 확장성&lt;/strong&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;동일한 Consumer Group 내의 Rebalancing 메커니즘을 통해 노드의 추가 및 삭제만으로 시스템 처리 대역폭을 쉽게 늘릴 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;아래 그림은 소비자를 2개 더 추가하여, 리밸런싱을 통해 각 소비자가 오직 하나의 파티션에서만 전담하여 이벤트를 안정적으로 소비할 수 있도록 Scale-out하는 예시이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/rebalancing.png&quot; alt=&quot;소비자 추가&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️소비자 확장 시 운영 주의점&lt;/strong&gt;&lt;br /&gt;
시스템에 수백 개의 카프카 소비자가 물려 있는 경우, 리밸런싱 작업이 일어나는 동안 데이터 소비가 일시 중지되어 작업 완료까지 수 분 이상 소요될 수 있다.&lt;br /&gt;
따라서 소비자를 추가하는 작업은 &lt;strong&gt;시스템 사용량이 많지 않은 오프피크(Off-peak) 시간대에 실행&lt;/strong&gt;하여 비즈니스 영향을 최소화해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;52-브로커broker의-규모-확장-및-데이터-라우팅-전략&quot;&gt;5.2. 브로커(broker)의 규모 확장 및 데이터 라우팅 전략&lt;/h2&gt;

&lt;p&gt;카프카 브로커 계층의 확장성과 파티션 배치 전략은 데이터 정합성과 직결되므로 매우 정밀하게 제어해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;해시 키(Hash Key) 전략&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;목적&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;동일한 ad_id를 갖는 모든 클릭 이벤트를 무조건 카프카 브로커 내부의 &lt;strong&gt;동일한 파티션에 순서대로 저장&lt;/strong&gt;하기 위해 ad_id를 해시 키로 지정한다.&lt;/li&gt;
          &lt;li&gt;이렇게 하면 뒷단의 집계 서비스 노드가 엉뚱한 파티션을 헤맬 필요 없이, 특정 파티션 하나만 구독하여 해당 광고의 전량을 수집할 수 있다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;파티션 수 제어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;카프카 브로커 내의 토픽 파티션 수가 도중에 변경되면, 동일한 ad_id를 가진 이벤트가 다른 파티션에 기록되는 대재앙이 발생할 수 있다.&lt;/li&gt;
      &lt;li&gt;따라서 &lt;strong&gt;설계 사전에 미래 트래픽을 감당할 수 있는 충분한 파티션 수를 확보&lt;/strong&gt;하여, 운영 중에 파티션 수가 동적으로 일어나는 상황은 반드시 피해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;토픽의 물리적 샤딩&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;동일한 대형 토픽 하나에 파티션을 무한정 늘리는 대신, 비즈니스 성격에 따라 토픽 자체를 물리적으로 쪼개는 방식이다.
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;데이터를 여러 토픽으로 나누면 단일 토픽에 몰리는 부하를 분산시켜 시스템 전체 처리 대역폭을 높일 수 있다.&lt;/li&gt;
              &lt;li&gt;또한, 단일 토픽당 물리는 소비자 수가 줄어들기 때문에, 앞서 언급한 Consumer Group의 리밸런싱 시간도 단축된다.&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;관리해야 할 토픽이 늘어나므로 파이프라인의 복잡성이 증가하고 운영 유지 관리 비용이 증가한다.&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;ad_id-를-해시-키가-아니라-메시지-key로-설정해도-항상-같은-파티션에-들어가지-않나&quot;&gt;💡ad_id 를 해시 키가 아니라 메시지 Key로 설정해도 항상 같은 파티션에 들어가지 않나?&lt;/h3&gt;

&lt;p&gt;ad_id를 해시 키가 아니라 그냥 Key로 설정했을 경우에도, 파티션 수가 변했을 때 Key만 같으면 같은 파티션으로 들어가지 않을까?&lt;br /&gt;
이는 분산 시스템에서 흔하게 발생하는 오해이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;해시 키와 카프카 메시지 Key와의 관계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;카프카 메시지에서 Key(ad_id)를 지정하여 발행하면, 카프카 프로듀서 내부의 기본 파티셔너(주로 MurmurHash2 알고리즘)가 이 Key를 자동으로 &lt;strong&gt;해시 연산&lt;/strong&gt;하여 숫자로 바꾼다.&lt;/li&gt;
      &lt;li&gt;즉, &lt;strong&gt;‘Key를 지정한다’는 행위 자체가 내부적으로 해시 키를 사용해 라우팅한다는 뜻&lt;/strong&gt;과 같다.&lt;/li&gt;
      &lt;li&gt;해시 키 = 메시지 Key이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;파티션 수가 변할 때 생기는 일&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;카프카가 특정 Key를 어떤 파티션에 넣을지 결정하는 공식은 본질적으로 아래와 같다.&lt;/li&gt;
      &lt;li&gt;
\[\text{Target Partition} = \text{Hash}(ad\_id) \pmod{\text{Total Partitions}}\]
      &lt;/li&gt;
      &lt;li&gt;만일 파티션 수가 4개일 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hash(&apos;ad001&apos;) = 10&lt;/code&gt;이었다면, \(10 \pmod 4 = 2\)가 되어 &lt;strong&gt;2번 파티션&lt;/strong&gt;으로 들어간다.&lt;/li&gt;
      &lt;li&gt;하지만 파티션 수를 &lt;strong&gt;5개로 동적 변경&lt;/strong&gt;하는 순간 공식이 변경된다.&lt;/li&gt;
      &lt;li&gt;\(10 \pmod 5 = 0\)가 되므로, 동일한 ad001 광고의 로그가 이제부터 &lt;strong&gt;0번 파티션&lt;/strong&gt;으로 들어간다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;파티션 수가 도중에 변하면 과거 데이터와 현재 데이터의 목적지 파티션이 찢어지게 되어 집계 노드가 데이터를 누락하는 치명적인 정합성 오류가 발생한다.&lt;br /&gt;
따라서 &lt;strong&gt;초기에 파티션 수를 충분히 넉넉하게 확보해두고, 동적으로 파티션 개수를 늘리는 일은 절대 피해야 한다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;만일 대역폭 확장이 필수적이라면 토픽 자체를 지역/유형별로 쪼개는 토픽의 물리적 샤딩을 적용해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;53-집계-서비스의-규모-확장&quot;&gt;5.3. 집계 서비스의 규모 확장&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/aggree3.png&quot; alt=&quot;집계 서비스&quot; /&gt;&lt;/p&gt;

&lt;p&gt;집계 서비스의 규모는 노드의 추가/삭제를 통해 Scale-out이 가능하다.&lt;br /&gt;
집계 서비스의 처리 대역폭을 높이려면 어떻게 해야 할까?&lt;br /&gt;
메모리 내부에서 맵리듀스를 수행하는 집계 서비스의 처리 대역폭을 높이는 데는 두 가지 아키텍처 선택지가 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;531-다중-스레드-모델-vs-다중-프로세싱리소스-프로바이더-모델&quot;&gt;5.3.1. 다중 스레드 모델 vs 다중 프로세싱(리소스 프로바이더) 모델&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;방안 1. 다중 스레드 모델&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/thread.png&quot; alt=&quot;다중 스레드&quot; /&gt;&lt;/p&gt;

&lt;p&gt;하나의 집계 노드 서버 안에서 카프카 토픽의 여러 파티션을 할당받은 뒤, 각 ad_id마다 별도의 워커 스레드를 할당하여 병렬 처리하는 방식이다.&lt;br /&gt;
구현이 매우 직관적이고 가볍다는 장점이 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;방안 2. 다중 프로세싱 모델&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;집계 서비스 노드들은 아파치 하둡 YARN(Yet Another Resource Negotiator)이나 쿠버네티스와 같은 자원 관리자(Resource Provider) 위에 독립된 컨테이너 프로세스로 띄워 
물리 머신 단위로 확장하는 방식이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;아파치-하둡-yarn-리소스-프로바이더의-다중-프로세싱-동작-원리&quot;&gt;💡아파치 하둡 YARN 리소스 프로바이더의 다중 프로세싱 동작 원리&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;YARN(Yet Another Resource Negotiator)&lt;/strong&gt;은 수백, 수천 대의 서버를 하나의 거대한 컴퓨팅 자원 풀로 묶어 관리해주는 ‘데이터 센터용 운영체제’이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동작 원리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;집계 스트리밍 엔진(예: 아파치 Flink)을 YARN 클러스터에 배포하면, YARN의 ResourceManager가 여러 물리 서버에 분산된 메모리와 CPU 공간을 쪼개어 독립된 태스크 컨테이너들을 할당한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;왜 방안 2를 실무에서 더 많이 쓸까?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;방안 1(다중 스레드)은 단일 서버의 물리적 한계를 넘을 수 없으며, 하나의 스레드가 OOM을 내면 같은 프로세스 안의 모든 스레드가 함께 죽는 &lt;strong&gt;결함 격리(Isolation) 실패 문제&lt;/strong&gt;가 있다.&lt;/li&gt;
      &lt;li&gt;반면 YARN 기반의 다중 프로세싱 모델은 서버가 부족하면 물리 장비를 옆으로 계속 이어 붙여 가며 컨테이너(프로세스)를 수천 개로 늘릴 수 있다.&lt;/li&gt;
      &lt;li&gt;또한, 특정 프로세스가 죽어도 다른 독립된 프로세스들에 전혀 영향을 주지 않으므로 대규모 실시간 빅데이터 시스템의 표준 Scale-out 기법으로 활용된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;54-카산드라cassandra-db의-가상노드-기반-자동-샤딩-구조&quot;&gt;5.4. 카산드라(Cassandra) DB의 가상노드 기반 자동 샤딩 구조&lt;/h2&gt;

&lt;p&gt;원시 데이터와 집계 데이터를 받아내는 카산드라는 &lt;a href=&quot;https://assu10.github.io/dev/2026/06/05/architecture-nearby/#35-%EB%B6%84%EC%82%B0-%EB%A0%88%EB%94%94%EC%8A%A4-%ED%8E%8D%EC%84%AD-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0%EC%99%80-%EC%95%88%EC%A0%95-%ED%95%B4%EC%8B%9Cconsistent-hash-ring&quot;&gt;안정 해시(Consistent hash) 링&lt;/a&gt; 
알고리즘을 변형한 &lt;strong&gt;가상 노드(vnode)&lt;/strong&gt; 아키텍처를 통해 데이터베이스 계층의 무한 확장을 지원한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/vnode.png&quot; alt=&quot;가상 노드&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;가상 노드가 없는 구조의 문제점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하나의 물리 서버가 해시 링 위의 단 하나의 거대한 연속 구간 전체를 담당하므로, 서버를 추가하거나 제거할 때 데이터 재배치 부하가 특정 노드에만 집중되어 클러스터가 쉽게 불안정해진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;가상 노드&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;물리 노드 1대가 해시 링 위에 무작위로 파편화된 수많은 작은 토큰(가상 노드 위치)을 나누어 가진다.&lt;/li&gt;
      &lt;li&gt;예: 노드 1개가 128개의 가상 토큰 보유&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클러스터에 새 물리 서버(노드)를 추가하는 순간, 기존의 모든 물리 노드들이 자기가 가진 가상 노드 파편들을 조금씩 떼어내어 새 노드에게 자동으로 나눠준다.&lt;/li&gt;
      &lt;li&gt;데이터가 클러스터 전체에 완벽하게 균등 분산되므로, 엔지니어가 수동으로 DB 샤딩을 튜닝하거나 데이터 이관 스크립트를 짤 필요가 없이 Scale-out이 이루어진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;좀 더 상세한 내용은 &lt;a href=&quot;https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/architecture/archDataDistributeDistribute.html&quot;&gt;How data is distributed across a cluster (using virtual nodes)&lt;/a&gt;를 참고하면 된다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;55-핫스팟hotspot-문제-해결&quot;&gt;5.5. 핫스팟(Hotspot) 문제 해결&lt;/h2&gt;

&lt;p&gt;하이닉스나 삼성 같은 거대 글로벌 기업이 슈퍼볼 시즌에 수백만 달러짜리 광고를 집행하면, 해당 ad_id로 초당 수만 건의 클릭 이벤트가 단 하나의 집계 노드로만 일시에 몰리게 된다.&lt;br /&gt;
특정 노드에 트래픽이 과도하게 몰리는 이 현상을 &lt;strong&gt;핫스팟(Hotspot) 혹은 데이터 왜곡(Data Skew)&lt;/strong&gt; 문제라고 한다.&lt;/p&gt;

&lt;p&gt;이 문제를 Resource Manager와 연동하여 실시간으로 유연하게 대처하는 방법은 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/add.png&quot; alt=&quot;추가 집계 서비스 노드 할당&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;추가 자원 요청&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 ad_id의 클릭 스트림이 폭주하여 한 노드의 처리 한계치(예: 100건)를 초과(여기서는 300건 유입)하는 순간, 해당 노드가 시스템의 자원 관리자에게 추가 자원 요청(①)을 보냄&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;추가 자원 할당&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;자원 관리자는 클러스터 풀에서 여유 프로세스를 찾아 추가 자원을 할당(②)하여 전용 추가 집계 서비스 계층을 동적으로 구성함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;이벤트 분할(Sub-key 생성)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;원래의 집계 노드는 과부하를 막기 위해 들어온 300개의 이벤트를 100개씩 3개의 그룹으로 쪼개어, 새로 할당된 추가 집계 노드들로 이벤트를 분할 라우팅(③)함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;축약 결과 병합&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;추가 집계 노드들이 각자 100개씩의 데이터를 빠르게 연산하여 요약본을 만들면, 원래의 집계 노드가 그 축약 결과(④)를 받아 최종 합산하므로 노드가 OOM되지 않고 초고강도 트래픽 스파이크를 안정적으로 방어해낼 수 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;flink-성능-튜닝의-핵심-전역-지역-집계global-local-aggregation&quot;&gt;💡Flink 성능 튜닝의 핵심: 전역-지역 집계(Global-Local Aggregation)&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://nightlies.apache.org/flink/flink-docs-master/docs/dev/table/tuning/&quot;&gt;오픈소스 Flink의 공식 문서&lt;/a&gt;에서 말하는 근본적인 핫스팟 해결책에 대해 알아보자.&lt;/p&gt;

&lt;p&gt;매번 자원 관리자에게 서버를 요청하는 오버헤드를 줄이기 위해, 아파치 플링크는 &lt;strong&gt;전역-지역 집계(Global-Local Aggregation, 또는 2단계 집계)&lt;/strong&gt; 튜닝 옵션을 기본으로 제공한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동작 원리:&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;핫스팟이 발생하는 Key(예: hynix_ad)의 뒤에 무작위 숫자(Salt)를 붙여 hynix_ad_1, hynix_ad_2 형태로 강제 변환&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;1단계: 지역 집계(Local Aggregation)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;변형된 Key들은 서로 다른 파티션과 집계 노드로 분산되므로 여러 서버가 트래픽을 나누어 감당하게 됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2단계: 전역 집계(Global Aggregation)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;지역 집계 노드들이 분 단위 합산을 일차적으로 끝낸 뒤 뒷단으로 넘겨주면, 최종 리듀서가 뒤에 붙은 숫자를 제거하고 원래 하나의 Key(hynix_ad)로 뭉쳐서 저장&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 옵션을 켜는 것만으로도 특정 CPU 코어 하나만 100%를 찍으며 전체 시스템이 먹통이 되는 &lt;strong&gt;데이터 쏠림 현상을 완벽히 튜닝&lt;/strong&gt;할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-결함-내성fault-tolerance와-스냅숏-복구&quot;&gt;6. 결함 내성(Fault Tolerance)와 스냅숏 복구&lt;/h1&gt;

&lt;p&gt;광고 클릭 이벤트 집계 서비스는 기본적으로 초고속 연산을 위해 &lt;strong&gt;메모리(In-memory) 상에서 집계&lt;/strong&gt;가 이루어진다.&lt;br /&gt;
이 방식은 성능 면에서는 매우 유리하지만, 집계 노드에 장애가 발생하면 메모리에 있던 실시간 집계 결과도 손실된다는 단점이 있다.&lt;/p&gt;

&lt;p&gt;하지만 본 설계안은 앞단에 아파치 카프카를 두고 있으므로, 업스트림 브로커로부터 이벤트를 다시 받아와 재생(Replay)하면 유실된 데이터를 완벽하게 다시 만들어낼 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚨 ‘시스템 상태’의 스냅숏 저장 필요성&lt;/strong&gt;&lt;br /&gt;
장애가 났다고 해서 카프카 데이터를 처음 원점부터 다시 재생하여 집계하는 것은 트래픽 양이 너무 방대하여 시간이 오래 걸린다.&lt;br /&gt;
파이프라인의 실시간성이 무너지게 되는 것이다.&lt;/p&gt;

&lt;p&gt;따라서 주기적으로 &lt;strong&gt;업스트림 오프셋&lt;/strong&gt;같은 ‘시스템 상태’를 외부 저장소(S3/HDFS 등)에 스냅숏으로 저장하고, 장애 발생 시 가장 마지막으로 저장된 상태부터 빠르게 
복구해나가는 것이 바람직하다.&lt;/p&gt;

&lt;p&gt;이 때 ‘시스템 상태’에 해당하는 정보에는 업스트림 카프카의 오프셋 위치 뿐 아니라, &lt;strong&gt;지난 N분간 가장 많이 클릭된 광고 M개와 같은 비즈니스 핵심 통계 데이터&lt;/strong&gt;도 반드시 
시스템 상태의 일부로 함께 묶어서 저장해야 한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/snapshop.png&quot; alt=&quot;스냅샷 데이터&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이렇게 시스템 상태를 주기적으로 백업해 두면, 집계 서비스의 장애 복구가 매우 단순해진다.&lt;/p&gt;

&lt;p&gt;작동 중이던 특정 집계 서비스 노드 하나에 장애가 발생하면, 시스템은 복잡한 디버깅을 하는 대신 해당 노드를 새 것으로 즉시 교체한 후, 마지막 스냅숏 저장소에서 백업된 
데이터를 읽어와 메모리 상태를 복구한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/restore.png&quot; alt=&quot;집계 노드의 복구&quot; /&gt;&lt;/p&gt;

&lt;p&gt;마지막 스냅숏을 찍은 시점 이후에 카프카 브로커에 새로 도착한 이벤트들은, 메모리 복구를 마친 &lt;strong&gt;새로운 집계 서비스 노드가 카프카 브로커로부터 오프셋 이후의 분량만 읽어가서 처리&lt;/strong&gt;한다.&lt;br /&gt;
이 복구 아키텍처 덕분에 시스템은 데이터 유실 없이 수 초 내에 실시간 집계 상태로 복귀할 수 있게 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;7-데이터-모니터링-정확성-검증-및-대안적-설계&quot;&gt;7. 데이터 모니터링, 정확성 검증 및 대안적 설계&lt;/h1&gt;

&lt;p&gt;빅데이터 파이프라인 설계의 마지막 관문은 시스템의 상태를 실시간으로 감시하고, 계산된 데이터에 단 하나의 오차도 없음을 증명하는 정확성 검증이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;집계 결과는 RTB(Real-Time Bidding) 및 청구서 발행 목적으로 사용되므로 시스템이 정상적으로 동작하는지 모니터링하고 데이터 정확성을 보장하는 것은 아주 중요하다.
(궁금증) 집계 결과가 RTB에 어떤 식으로 사용이 되는 거야? RTB에서 성공한 수요자의 정보가 집계 결과에 쌓이는 거로 이해하고 있었는데 잘못 이해한 거야?&lt;/p&gt;

&lt;h2 id=&quot;71-지속적-모니터링-지표&quot;&gt;7.1. 지속적 모니터링 지표&lt;/h2&gt;

&lt;p&gt;시스템이 정상적으로 동작하는지 확인하기 위해 아래 3가지 지표를 골든 시그널로 삼아 대시보드에 구성해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;종단 간 지연시간&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터가 수집되는 첫 단계부터 최종 DB에 기록될 때까지 각 컴포넌트를 거칠 때마다 타임스탬프를 메시지에 추가한다.&lt;/li&gt;
      &lt;li&gt;기록된 시각 사이의 차이를 지연 시간 지표로 변환하여 파이프라인 중 어느 구간에서 병목이 발생하는지 추적한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;집계 노드의 시스템 자원&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메모리 내부 연산이 핵심이므로 집계 노드의 CPU 사용량, 디스크 I/O 속도, 그리고 자바 기반 엔진(Flink 등)의 JVM GC 지연 시간을 모니터링한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;메시지 큐의 크기 추적(Kafka Lag)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;대기열의 크기가 갑자기 늘어나면 소비자의 처리 용량이 한계에 도달했다는 신호이므로 집계 노드를 즉시 Scale-out 해야 한다.&lt;/li&gt;
      &lt;li&gt;카프카는 분산 커밋 로그 형태로 구현된 메시지 큐이므로, 이를 레코드 처리 지연 지표(records-lag)로 추적한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;분산-커밋-로그는-무엇이며-왜-records-lag을-추적해야-할까&quot;&gt;💡분산 커밋 로그는 무엇이며, 왜 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;records-lag&lt;/code&gt;을 추적해야 할까?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;분산 커밋 로그(Distributed Commit Log)&lt;/strong&gt;는 아파치 카프카의 근본적인 저장 메커니즘이다.&lt;br /&gt;
카프카는 데이터를 관계형 테이블처럼 저장하는 것이 아니라, 들어오는 순서대로 디스크의 끝에 고속으로 받아적는 &lt;strong&gt;추가 전용 로그파일(Commit Log)&lt;/strong&gt; 형태로 데이터를 보관하며, 
이를 여러 장비(Distributed)에 복제하여 분산 저장한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;records-lag&lt;/code&gt;을 추적하는 이유&lt;/strong&gt;&lt;br /&gt;
카프카 내부의 데이터는 절대 임의로 지워지지 않고 로그 끝에 계속 쌓인다.(Log End Offset)&lt;br /&gt;
집계 서비스(소비자)는 이 로그 위에서 자신이 어디까지 읽었는지 가리키는 포인터(Consumer Offset)를 앞으로 이동시키며 데이터를 소비한다.&lt;/p&gt;

&lt;p&gt;이 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Lag = Log End Offset - Consumer Offset&lt;/code&gt; 공식이 성립된다.&lt;br /&gt;
즉, 발행된 총 데이터 양과 내가 읽은 양의 격차를 뜻한다.&lt;br /&gt;
이 Lag 수치가 늘어난다는 것은 소비자가 들어오는 트래픽 속도를 따라가지 못해 시스템에 정체가 발생하고 있음을 뜻하므로, 모니터링의 최우선 지표가 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;72-종단-간-조정reconciliation-프로세스&quot;&gt;7.2. 종단 간 조정(Reconciliation) 프로세스&lt;/h2&gt;

&lt;p&gt;실시간 스트리밍 시스템은 메모리 위에서 흘러가는 데이터를 계산하므로, 노드 장애나 스냅숏 유실 시 미세한 데이터 오차가 발생할 리스크가 늘 존재한다.&lt;/p&gt;

&lt;p&gt;이 때 데이터 무결성을 보증하기 위해 서로 다른 경로로 계산된 두 데이터를 대조하는 &lt;strong&gt;조정(Reconciliation)&lt;/strong&gt; 기법을 도입해야 한다.&lt;br /&gt;
금융 시스템은 은행 거래 내역이라는 제3의 비교 대상이 있지만, 광고 시스템은 우리가 만든 데이터가 곧 기준이 되므로 시스템 내부에서 교차 검증 파이프라인을 구축해야 한다.&lt;/p&gt;

&lt;p&gt;아래는 조정 프로세스를 고려하여 수정한 설계안이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/final1.png&quot; alt=&quot;최종 설계안&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) 실시간 결함 내성 확보(스냅숏과 복구 메커니즘)&lt;/strong&gt;&lt;br /&gt;
&lt;a href=&quot;#6-결함-내성fault-tolerance와-스냅숏-복구&quot;&gt;# 6. 결함 내성(Fault Tolerance)와 스냅숏 복구&lt;/a&gt; 참고.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) Batch 조정을 통한 사후 검증&lt;/strong&gt;&lt;br /&gt;
동시 실시간 파이프라인과 별개로, 원시 데이터베이스에 쌓인 날것의 로그들을 &lt;strong&gt;매일 밤 이벤트 발생 시각 기준으로 완벽하게 정렬하여 일괄 처리하는 재계산 서비스&lt;/strong&gt;를 구동한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;대조 작업&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;일괄 처리를 통해 산출된 100% 무결한 ‘하루치 통계 결과’와, 낮 동안 실시간 집계 서비스가 두 번째 메시지 큐를 통해 실시간으로 밀어넣었던 ‘집계 결과 데이터베이스의 값’을 교차 검증&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;보정&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;만일 두 데이터 사이에 오차가 발견되면, 재계산 서비스의 결과물로 집계 결과 DB를 덮어써서 최종 정합성을 맞춘다.&lt;/li&gt;
      &lt;li&gt;정확도를 극도로 높이고 싶다면 하루 단위가 아니라 더 작은 집계 윈도(시간 단위 등)를 사용하여 조정 주기를 쪼개면 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;73-대안적-아키텍처-설계&quot;&gt;7.3. 대안적 아키텍처 설계&lt;/h2&gt;

&lt;p&gt;지금까지 다룬 아키텍처는 &lt;strong&gt;Kafka → Flink(집계) → Cassandra(저장)&lt;/strong&gt; 로 이어지는 직접 스트림 연산 커스텀 파이프라인이었다.&lt;br /&gt;
하지만 이렇게 직접 로직을 코딩하는 대신, 대용량 실시간 분석 전문 데이터베이스를 결합하는 대안적 설계안도 매우 활발하게 선택된다.&lt;/p&gt;

&lt;p&gt;로그 모니터가 수집한 데이터를 위험성 통제 엔진(Risk Control Engine)에서 정제한 뒤, 과거 데이터 백업용 &lt;strong&gt;하이브(Hive)&lt;/strong&gt; 계층과 실시간 분석용 &lt;a href=&quot;https://clickhouse.com/&quot;&gt;&lt;strong&gt;클릭하우스(ClickHouse)&lt;/strong&gt;&lt;/a&gt; 계층의 
양 갈래로 부어주는 구조이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/second.png&quot; alt=&quot;대안적 설계안&quot; /&gt;&lt;/p&gt;

&lt;p&gt;데이터 과학자는 하이브 위에 Elasticsearch를 얹어 복잡한 비정형 질의를 수행하고, 일반 광고주 대상 애널리스틱 대시보드는 클릭하우스가 직접 초고속으로 실시간 집계 
결과를 질의받아 서빙한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;hive란&quot;&gt;💡Hive란?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;하이브(Hive)&lt;/strong&gt;는 아파치 하둡 에코 시스템에서 대용량 데이터 웨어하우스를 구축할 때 사용하는 프레임워크이다.&lt;br /&gt;
분산 파일 시스템(HDFS, Hadoop Distributed File System)에 저장된 수 페타바이트(PB)급 대용량 원시 데이터를 자바 코딩 없이 SQL 문법(HiveQL)만으로 편하게 조회하고 일괄 처리할 수 있도록 도와주는 
대규모 데이터 보관소 역할을 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;클릭하우스clickhouse나-드루이드druid같은-olap-데이터베이스란&quot;&gt;💡클릭하우스(ClickHouse)나 &lt;a href=&quot;https://druid.apache.org/&quot;&gt;드루이드(Druid)&lt;/a&gt;같은 OLAP 데이터베이스란?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;OLAP(Online Analytical Processing) 데이터베이스&lt;/strong&gt;란 대규모 데이터에 대한 복잡한 대화형 분석 쿼리(SUM, COUNT 등)를 초고속으로 처리하기 위해 생긴 
특수 데이터베이스 엔진이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동작 원리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;앞서 설명한 &lt;a href=&quot;#컬럼형-데이터-형식columnar-data-format이란&quot;&gt;&lt;strong&gt;열 지향(Columnar) 저장 구조&lt;/strong&gt;&lt;/a&gt;를 극단적으로 고도화하여, 내부 인덱싱 시스템을 설계함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;지금까지 고생해서 설계한 맵 노드의 라우팅, 집계 노드의 메모리 합산 로직을 애플리케이션 코드로 짤 필요가 전혀 없다.&lt;/li&gt;
      &lt;li&gt;그냥 날 것의 이벤트를 클릭하우스로 그대로 전달하기만 하면, &lt;strong&gt;DB 자체 엔진이 수십억 건의 데이터를 단 몇 ms 만에 실시간으로 SUM, GROUP BY 연산하여 대시보드에 뿌려준다.&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;실시간 데이터 정합성과 인프라 단순화 측면에서 최근 업계에서 폭발적인 사랑을 받고 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;8-마무리&quot;&gt;8. 마무리&lt;/h1&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;원시 로그 인입] ➔ 50,000 Peak QPS
       │
       ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;카프카 업스트림 큐] ➔ ad_id 기반 고정 해시 키 라우팅 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;파티션 동적 변경 절대 금물&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
       │
       ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;집계 서비스 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Flink&lt;span class=&quot;o&quot;&gt;)]&lt;/span&gt; ➔ 이벤트 발생 시각 기준 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Event Time&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 윈도 분절
       │              ├─ 워터마크&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Watermark&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 도입으로 지연 이벤트&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;15초 대기&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 구제
       │              └─ 2단계 커밋&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;2PC&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 분산 트랜잭션으로 Exactly-Once 무결성 달성
       ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;카프카 다운스트림 큐] ➔ 분 단위 요약 및 힙&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Heap&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 구조 기반 상위 100개 인기 광고 축약
       │
       ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;카산드라 데이터베이스] ➔ 가상 노드&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;vnode&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 링 기반 자동 샤딩 스케일아웃 저장
       │
       ▼
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;대시보드 / 정산] ➔ 스타 스키마 차원&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;Dimension&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 기반 초고속 다차원 필터링 조회
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;광고 클릭 이벤트 집계 시스템은 전형적인 빅데이터 처리 시스템이다. 
아파치 카프카, 아파치 플링크, 아파치 스파크 같은 업계 표준 솔루션에 대한 사전 지식이나 경험이 있다면 이해하기 쉬울 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;아파치-플링크flink-vs-아파치-스파크spark&quot;&gt;💡아파치 플링크(Flink) vs 아파치 스파크(Spark)&lt;/h1&gt;

&lt;p&gt;실시간 스트리밍 엔지니어링의 양대 산맥인 두 프레임워크는 데이터를 다루는 패러다임이 다르다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;아파치 스파크(Spark Streaming / Structured Streaming)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;본질적으로 &lt;strong&gt;일괄 처리 중심&lt;/strong&gt; 엔진이다.&lt;/li&gt;
      &lt;li&gt;실시간 스트림을 다룰 때도 데이터를 아주 짧은 시간(예: 0.5초) 동안 모았다가 한꺼번에 처리하는 &lt;strong&gt;마이크로 배치&lt;/strong&gt; 방식을 사용한다.&lt;/li&gt;
      &lt;li&gt;처리량이 매우 거대하지만, 태생적으로 수백 밀리초 수준의 미세한 지연 시간이 깔려있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;아파치 플링크&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;본질이 &lt;strong&gt;순수 스트리밍 중심&lt;/strong&gt; 엔진이다.&lt;/li&gt;
      &lt;li&gt;데이터가 들어오는 즉시 한 건 한 건 실시간으로 흘려보내며 연산하는 Event-driven 구조이다.&lt;/li&gt;
      &lt;li&gt;지연 시간이 거의 제로에 가까우며, 본 설계의 핵심인 &lt;strong&gt;이벤트 시각 기준 워터마크 제어와 end-to-end 2단계 커밋 트랜잭션 기능&lt;/strong&gt;을 가장 완벽하게 지원한다.&lt;/li&gt;
      &lt;li&gt;실시간 금융/과금 집계 시스템에 플링크가 훨씬 최적화된 선택이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;이-아키텍처를-그대로-제공하는-상용화된-오픈소스-제품군&quot;&gt;💡이 아키텍처를 그대로 제공하는 상용화된 오픈소스 제품군&lt;/h1&gt;

&lt;p&gt;빅데이터 기업들은 인프라 직접 구축 오버헤드를 줄이기 위해 아래 솔루션들을 적극 도입한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Confluent Cloud&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;카프카 창시자들이 만든 기업으로, 완전 관리형 카프카 브로커뿐만 아니라 내부에 플링크 엔진을 내장하여 &lt;strong&gt;SQL 몇 줄만 쓰면 실시간 ‘정확히 한 번’ 집계 파이프라인을 완성해 주는 상용 플랫폼&lt;/strong&gt;을 제공한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Databricks&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;아파치 스파크 창시자들이 만든 플랫폼으로, 실시간 스트리밍 데이터를 Lakehouse 구조에 안전하게 적재하고 AI/머신러닝 분석까지 원스톱으로 지원하는 대기업 상용 제품이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AWS Kinesis Data Analytics / Google Cloud Dataflow&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클라우드 3사(아마존, 구글)가 아파치 플링크 엔진을 서버리스 형태로 완전 관리해주는 상용 서비스이다.&lt;/li&gt;
      &lt;li&gt;서버 인프라 관리 없이 클릭 몇 번으로 50,000 QPS 대규모 트래픽을 집계할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0620/mindmap.png&quot; alt=&quot;마인드 맵&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬, 산 람 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000211656186&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links_vol2.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.oracle.com/database/121/OLAXS/olap_functions.htm#OLAXS169&quot;&gt;[Oracle Docs] OLAP 표현식 구문 및 분석 함수(SUM, COUNT 등) 참조 가이드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://cwiki.apache.org/confluence/display/hive/languagemanual+orc&quot;&gt;[Apache Hive] ORC(Optimized Row Columnar) 파일 포맷 구조 및 공식 언어 매뉴얼&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.databricks.com/blog/what-is-parquet&quot;&gt;[Databricks] 대용량 데이터 분석의 마법, Apache Parquet(파케이) 칼럼형 포맷 완벽 가이드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.ibm.com/think/topics/avro&quot;&gt;[IBM Topics] 스키마 기반 분산 데이터 직렬화 시스템, Apache Avro 기술 백서&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://flink.apache.org/2018/02/28/an-overview-of-end-to-end-exactly-once-processing-in-apache-flink-with-apache-kafka-too/&quot;&gt;[Apache Flink] Flink와 Kafka를 연동한 엔드투엔드 ‘정확히 한 번(Exactly-Once)’ 처리 메커니즘 공식 블로그 백서&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/power-bi/guidance/star-schema&quot;&gt;[Microsoft Learn] Power BI 지침: 데이터 웨어하우스의 기초, 스타 스키마(Star Schema) 구조 설계 전략&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://flink.apache.org/&quot;&gt;[Apache Flink] 실시간 분산 스트림 처리 인프라, 플링크 공식 홈페이지 및 아키텍처 개요&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.databricks.com/blog/what-is-lambda-architecture&quot;&gt;[Databricks] 실시간과 배치를 동시에 다루는 람다 아키텍처(Lambda Architecture)의 개념과 한계&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://hazelcast.com/foundations/software-architecture/kappa-architecture/&quot;&gt;[Hazelcast Software Architecture] 단일 스트리밍 파이프라인의 정석, 카파 아키텍처(Kappa Architecture)의 기본 원리&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.google.com/ads/adtrafficquality/&quot;&gt;[Google Ads] 무효 트래킹 기술 및 광고 사기(Ad Fraud) 방어 기준 공식 가이드라인&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.datastax.com/en/cassandra-oss/3.0/cassandra/architecture/archDataDistributeDistribute.html&quot;&gt;[DataStax Data Distribution] Apache Cassandra 가상 노드(vnode) 기반 토큰 링 클러스터 자동 샤딩 아키텍처 가이드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://nightlies.apache.org/flink/flink-docs-master/docs/dev/table/tuning/&quot;&gt;[Apache Flink Tuning] 데이터 쏠림(Data Skew) 및 핫스팟 제어를 위한 Flink 성능 최적화 튜닝 가이드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 20 Jun 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/06/20/architecture-ad-click-event-aggregation/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/06/20/architecture-ad-click-event-aggregation/</guid>
        
        <category>architecture</category>
        
        <category>system-design</category>
        
        <category>big-data</category>
        
        <category>kafka</category>
        
        <category>apache-flink</category>
        
        <category>cassandra</category>
        
        <category>exactly-once</category>
        
        <category>kappa-architecture</category>
        
        <category>대규모시스템설계</category>
        
        <category>카프카</category>
        
        <category>실시간데이터집계</category>
        
        <category>분산시스템</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(1) - 시스템 설계 과정</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-문제-이해-및-설계-범위-확정&quot;&gt;1. 문제 이해 및 설계 범위 확정&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-개략적-설계안-제시-및-동의-구하기&quot;&gt;2. 개략적 설계안 제시 및 동의 구하기&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-상세-설계&quot;&gt;3. 상세 설계&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-마무리&quot;&gt;4. 마무리&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#정리하며&quot;&gt;정리하며…&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-문제-이해-및-설계-범위-확정&quot;&gt;1. 문제 이해 및 설계 범위 확정&lt;/h1&gt;

&lt;p&gt;엔지니어가 가져야 할 기술 중 하나는 바로 ‘올바른 질문을 던지는 것’이다.&lt;br /&gt;
시스템 설계에서 완벽한 초기 설계란 없다.&lt;br /&gt;
&lt;strong&gt;적절한 가정을 세우고, 시스템 구축에 필요한 핵심 정보를 수집하며 모호함을 제거&lt;/strong&gt;하는 것이 1단계의 핵심이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;구체적으로 어떤 기능들을 만들어야 하는가?&lt;/li&gt;
  &lt;li&gt;제품 사용자 수(트래픽 규모)는 얼마나 되는가?&lt;/li&gt;
  &lt;li&gt;주로 사용하는 기술 스택은 무엇인가?&lt;/li&gt;
  &lt;li&gt;설계를 단순화하기 위해 활용할 수 있는 사내 기존 서비스나 오픈소스가 있는가?&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;뉴스 피드 시스템 요구사항 도출하기&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Q. 지원해야 하는 플랫폼은 무엇인가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;A. 모바일 앱과 웹 앱 둘 다 지원&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Q. 가장 중요한 핵심 기능은 무엇인가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;A. 사용자가 새로운 포스트를 올리고, 다른 친구의 뉴스 피드를 볼 수 있어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Q. 뉴스 피드의 정렬 기준은 무엇인가?(예: 시간 역순, 알고리즘 기반 가중치 부여 등)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;의도:&lt;/strong&gt; 친한 친구의 포스트를 상단에 노출하는 등의 특별한 알고리즘이 필요한지 파악&lt;/li&gt;
      &lt;li&gt;A. 단순 시간 역순 정렬&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Q. 한 사용자는 최대 몇 명의 사용자와 친구를 맺을 수 있는가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;A. 최대 5,000명&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Q. 시스템의 트래픽 규모는 어느 정도인가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;A. DAU 기준 1,000만 명&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Q. 피드 포스트에 텍스트 외에 이미지나 비디오도 올라올 수 있는가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;A. 그렇다. 미디어 파일도 포함될 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-개략적-설계안-제시-및-동의-구하기&quot;&gt;2. 개략적 설계안 제시 및 동의 구하기&lt;/h1&gt;

&lt;p&gt;요구사항이 명확해졌다면, 이제 시스템 전체적인 설계안을 만들 차례이다.&lt;br /&gt;
이 단계에서는 완벽한 설계도를 그리기 보다, &lt;strong&gt;개략적인 청사진을 제시하고 동료의 동의를 구하는 것&lt;/strong&gt;에 초점을 맞춘다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;핵심 컴포넌트 다이어그램 시각화:&lt;/strong&gt; 종이나 화이트보드에 시스템을 구성하는 핵심 컴포넌트들을 배치
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;클라이언트 및 네트워크:&lt;/strong&gt; 클라이언트(웹/앱), API GW, 웹 서버&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;데이터 관리:&lt;/strong&gt; 주 데이터베이스(RDBMS/NoSQL), 캐시&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;미디어 처리:&lt;/strong&gt; 미디어 파일 저장을 위한 객체 스토리지 및 콘텐츠 전송 네트워크(CDN)&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;비동기 처리:&lt;/strong&gt; 메시지 큐를 활용한 비동기 작업 아키텍처&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;최초 설계안이 1단계에서 파악한 트래픽 규모와 제약사항을 만족할 수 있는지 개략적으로 계산해보자.&lt;br /&gt;
이 과정에서 병목 현상이 발생할 수 있는 지점을 미리 파악할 수 있다.&lt;/p&gt;

&lt;p&gt;설계한 아키텍처 위에서 주요 사용 사례를 따라가본다.&lt;br /&gt;
이는 개략적 설계안의 논리적 흐름을 잡아줄 뿐만 아니라, 미처 생각지 못한 에지 케이스를 발견하는데 도움이 된다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;실무에서는 메시지 큐, CDN, 캐시 등을 직접 인프라에 구축하기보다 AWS SQS, AWS CloudFront, ElastiCache 같은 
&lt;strong&gt;완전 관리형 클라우드 서비스&lt;/strong&gt;를 적극 활용하여 인프라 관리 복잡성을 낮추는 방식을 선호한다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-상세-설계&quot;&gt;3. 상세 설계&lt;/h1&gt;

&lt;p&gt;이 단계에 왔다면, 당신은 아래 조건들은 성공적으로 달성한 상태이다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;시스템 전반의 목표와 기능 범위 확정&lt;/li&gt;
  &lt;li&gt;전체 시스템 아키텍처의 개략적 청사진 완성&lt;/li&gt;
  &lt;li&gt;해당 청사진에 대한 동료의 동의 확보&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이제 설계안에서 &lt;strong&gt;가장 중요하거나 병목이 될 것이라고 예상되는 영역&lt;/strong&gt;을 선택하여 깊이 파고들어야 한다.&lt;br /&gt;
예를 들어 뉴스 피드 시스템이라면 ‘수백만 명의 팔로워를 가진 유명인의 포스트를 처리할 때 발생하는 &lt;a href=&quot;https://assu10.github.io/dev/2025/05/27/fanout/&quot;&gt;팬아웃(Fan-out)&lt;/a&gt; 문제를 어떻게 해결할 것인가?’와 
같은 핵심 기술적 과제에 집중하여 상세한 해결책을 제시한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-마무리&quot;&gt;4. 마무리&lt;/h1&gt;

&lt;p&gt;시스템이 ‘정상적으로 동작할 때’뿐만 아니라 &lt;strong&gt;‘장애가 발생했을 때’&lt;/strong&gt; 어떻게 대처할 것인지까지 고민해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장애 대응&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;네트워크 단절, 특정 서버 다운, DB 데드락 등 예기치 못한 오류가 발생했을 때 시스템은 어떻게 복구되는가?&lt;/li&gt;
      &lt;li&gt;예) 다중화, Circuit Breaker 도입 등&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;운영 이슈 및 가시성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;트래픽 및 시스템 메트릭은 어떻게 수집할 것인가?&lt;/li&gt;
      &lt;li&gt;분산 환경에서 로그 추적은 어떻게 할 것인가?&lt;/li&gt;
      &lt;li&gt;무중단 배포 전략은 어떻게 가져갈 것인가?&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;규모 확장성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;현재 DAU 1,000만 명을 넘어 1억 명으로 트래픽이 폭증한다면 데이터베이스 샤딩이나 캐시 레이어를 어떻게 확장할 것인가?&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;가시성과 자동화&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;단순 모니터링을 넘어 &lt;strong&gt;가시성&lt;/strong&gt;이 핵심 화두이다.&lt;br /&gt;
프로메테우스와 그라파나(Grafana)를 통한 메트릭 수집, Datadog 또는 ELK 스택을 통한 중앙집중형 로그 분석, 
그리고 CI/CD 파이프라인(GitHub Actions, ArgoCD 등)을 통한 자동화된 배포 파이프라인 구축이 필요하다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며…&lt;/h1&gt;

&lt;p&gt;대규모 시스템 설계는 정해진 단 하나의 정답을 찾는 퀴즈가 아니다.&lt;/p&gt;

&lt;p&gt;1) 집요한 질문을 통해 문제를 정의하고
2) 협업을 통해 개략적인 뼈대를 세우며,
3) 핵심 기술 과제에 깊이 파고든 후,
4) 예외 상황과 운영까지 대비하는 종합적인 문제 해결 과정이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001033116&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
        <pubDate>Thu, 18 Jun 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/06/18/architecture-system-design-interview-framework/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/06/18/architecture-system-design-interview-framework/</guid>
        
        <category>architecture</category>
        
        <category>system-design</category>
        
        <category>interview-prep</category>
        
        <category>scalability</category>
        
        <category>large-scale</category>
        
        <category>대규모시스템설계</category>
        
        <category>시스템아키텍처</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(1) - 개략적 규모 추정</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#대규모-시스템-설계-시-대략적인-숫자-감각이-필요한-이유&quot;&gt;대규모 시스템 설계 시 ‘대략적인 숫자 감각’이 필요한 이유&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#개략적-규모-추정back-of-the-envelope&quot;&gt;개략적 규모 추정(Back-of-the-envelope)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#1-2의-제곱수와-데이터-볼륨-단위&quot;&gt;1. 2의 제곱수와 데이터 볼륨 단위&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-응답-지연&quot;&gt;2. 응답 지연&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#20252026년-하드웨어-발전에-따른-지연-시간-변화&quot;&gt;💡2025~2026년 하드웨어 발전에 따른 지연 시간 변화&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-가용성과-관련한-수치들&quot;&gt;3. 가용성과 관련한 수치들&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-예시-sns-데이터-기반-qps와-저장소-요구량-추정&quot;&gt;4. 예시: SNS 데이터 기반 QPS와 저장소 요구량 추정&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#단계-1-qps-추정치-산출&quot;&gt;단계 1: QPS 추정치 산출&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#단계-2-미디어-저장을-위한-저장소-요구량-산출&quot;&gt;단계 2: 미디어 저장을 위한 저장소 요구량 산출&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-마무리&quot;&gt;5. 마무리&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;대규모-시스템-설계-시-대략적인-숫자-감각이-필요한-이유&quot;&gt;대규모 시스템 설계 시 ‘대략적인 숫자 감각’이 필요한 이유&lt;/h1&gt;

&lt;p&gt;수백만, 수천만 명의 트래픽을 감당하는 대규모 시스템을 설계할 때 우리는 종종 ‘서버는 몇 대나 필요할까?’, ‘DB 용량은 얼마나 잡아야 할까?’라는 질문에 직면한다.&lt;br /&gt;
이 때 복잡한 성능 테스트나 코딩에 앞서 요구사항을 빠르고 합리적으로 예측하는 과정이 필수적인데, 이를 &lt;strong&gt;개략적 규모 추정&lt;/strong&gt;이라고 부른다.&lt;/p&gt;

&lt;p&gt;여기서는 반드시 숙지해야 할 데이터 볼륨 단위, 응답 지연 시간, 고가용성 지표, 그리고 SNS 데이터를 바탕으로 한 QPS(초당 트래픽) 산정 방법에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;개략적-규모-추정back-of-the-envelope&quot;&gt;개략적 규모 추정(Back-of-the-envelope)&lt;/h1&gt;

&lt;p&gt;개략적 규모 추정이란, 시스템 설계 시 &lt;strong&gt;보편적으로 통용되는 성능 수치와 수학적 계산을 바탕으로 시스템의 용량이나 성능 요구사항을 대략적으로 예측하는 과정&lt;/strong&gt;을 말한다.&lt;/p&gt;

&lt;p&gt;어떤 설계가 요구사항에 부합하는지, 혹은 병목 현상을 유발할지 판단하는 나침반 역할을 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-2의-제곱수와-데이터-볼륨-단위&quot;&gt;1. 2의 제곱수와 데이터 볼륨 단위&lt;/h1&gt;

&lt;p&gt;컴퓨터의 최소 단위는 1byte이며, 이는 8bit로 구성된다.&lt;br /&gt;
우리가 흔히 아는 ASCII 문자 하나가 차지하는 메모리 크기가 바로 1byte이다.&lt;/p&gt;

&lt;p&gt;시스템 용량을 계산하려면 데이터 볼륨 단위를 직관적으로 파악하고 있어야 한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;데이터 볼륨 단위 표&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;2의 x 제곱&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;근사치&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;이름&lt;/th&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;축약형&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;10&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1천(thousand)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1킬로바이트(Kilobyte)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1KB&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;20&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1백만(million)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1메가바이트(Megabyte)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1MB&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;30&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;10억(billion)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1기가바이트(Gigabyte)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1GB&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;40&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1조(trillion)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1테라바이트(Terabyte)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1TB&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;50&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1000조(quadrillion)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1페타바이트(Petabyte)&lt;/td&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;1PB&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-응답-지연&quot;&gt;2. 응답 지연&lt;/h1&gt;

&lt;p&gt;컴퓨터 내부 및 네트워크에서 데이터가 이동할 때 걸리는 시간을 이해하는 것은 아키텍처 설계의 기본이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2020년 기준 프로그래머가 알아야 할 지연 시간 (Latency Numbers)&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;연산명&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;시간&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;L1 캐시 참조 (L1 cache reference)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1ns&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;분기 예측 오류 (branch mispredict)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3ns&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;L2 캐시 참조 (L2 cache reference)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;4ns&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;뮤텍스(mutex) 락/언락&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;17ns&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;일반 네트워크(commodity network)로 2,000 바이트 전송&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;44ns&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;주 메모리 참조 (Main memory reference)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;100ns&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Zippy로 1 KB 압축&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2,000ns = 2µs&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;메모리에서 1,000,000 바이트(1 MB) 순차적으로 read&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3,000ns = 3µs&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;SSD 임의(random) read&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;16,000ns = 16µs&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;SSD에서 1,000,000 바이트(1 MB) 순차적으로 read&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;49,000ns = 49µs&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;같은 데이터 센터 내에서의 왕복 지연시간 (Round trip)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;500,000ns = 500µs&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;디스크에서 1,000,000 바이트(1 MB) 순차적으로 read&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;825,000ns = 825µs&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;디스크 탐색(seek)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2,000,000ns = 2ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;한 패킷의 CA(캘리포니아)로부터 네덜란드까지의 왕복 지연시간&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;150,000,000ns = 150ms&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;시간 단위 참고:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;ns&lt;/strong&gt; = nanosecond (나노초), &lt;strong&gt;µs&lt;/strong&gt; = microsecond (마이크로초), &lt;strong&gt;ms&lt;/strong&gt; = millisecond (밀리초)&lt;/li&gt;
  &lt;li&gt;1나노초 = \(10^{-9}\)초&lt;/li&gt;
  &lt;li&gt;1마이크로초 = \(10^{-6}\)초 = 1,000나노초&lt;/li&gt;
  &lt;li&gt;1밀리초 = \(10^{-3}\)초 = 1,000µs = 1,000,000ns&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;위의 수치들을 보면 아래와 같은 결론이 나온다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;메모리는 빠르지만 디스크는 아직 느리다.&lt;/li&gt;
  &lt;li&gt;병목의 주범인 디스크 탐색(seek) 연산은 가능한 한 피해야 한다.&lt;/li&gt;
  &lt;li&gt;단순 압축 알고리즘 연산 속도는 매우 빠르다.&lt;/li&gt;
  &lt;li&gt;따라서 데이터를 네트워크로 전송하기 전에 &lt;strong&gt;가능하면 압축&lt;/strong&gt;하라&lt;/li&gt;
  &lt;li&gt;데이터 센터는 보통 여러 region에 분산되어 있으며, 글로벌 센터 간 데이터를 주고받는 데는 시간이 소요된다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;20252026년-하드웨어-발전에-따른-지연-시간-변화&quot;&gt;💡2025~2026년 하드웨어 발전에 따른 지연 시간 변화&lt;/h2&gt;

&lt;p&gt;제프 딘의 &lt;a href=&quot;https://colin-scott.github.io/personal_website/research/interactive_latency.html&quot;&gt;지연 시간 표&lt;/a&gt;가 발표된 이후 하드웨어는 더 발전했다.&lt;br /&gt;
2026년 현재 대규모 시스템 설계 시 고려해야 할 최신 트렌드는 아래와 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;네트워크가 디스크를 이겼다.&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;100GbE(초당 100GB 전송) 이상의 초고속 데이터센터 네트워크 환경에서는 네트워크를 통해 다른 서버의 메모리(RAM) 데이터를 읽어오는 것(약 1.5µs~2µs)이, 로컬 디스크(HDD, SSD)에서 데이터를 읽는 것(2ms, 15µs)보다 훨씬 빠르다.&lt;/li&gt;
      &lt;li&gt;이로 인해 저장소와 컴퓨팅을 분리하는 ‘컴퓨트-스토리지 분리 아키텍처’가 대세가 되었다.
        &lt;ul&gt;
          &lt;li&gt;기존엔 서버 한 대에 CPU/RAM/디스크가 세트로 묶여 있었기 때문에 데이터가 너무 많아서 용량이 부족해지면 디스크만 사서 꽂는 게 아니라 CPU, RAM이 포함된 비싼 서버 자체를 통째로 증설해야 하므로 비용 낭비가 있었다.&lt;/li&gt;
          &lt;li&gt;컴퓨트-스토리지 분리 아키텍처에서는
            &lt;ul&gt;
              &lt;li&gt;컴퓨트 클러스터: 계산과 로직 연산만 전담하는 서버들의 집합(CPU, RAM)&lt;/li&gt;
              &lt;li&gt;스토리지 클러스터: 데이터 저장만 전담하는 저장소 서버들의 집합(AWS S3 등)&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;로컬 HDD에서 데이터를 읽어오는 속도: &lt;strong&gt;약 825µs ~ 2,000µs&lt;/strong&gt;&lt;/li&gt;
          &lt;li&gt;초고속 네트워크를 통해 멀리 떨어진 스토리지 서버의 메모리/SSD에서 데이터를 읽어오는 속도: &lt;strong&gt;약 1.5µs ~ 2µs&lt;/strong&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;NVMe SSD의 혁명&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;기존 HDD의 디스크 탐색이 2~10ms가 걸렸다면, 최신 NVMe SSD의 Random Read는 &lt;strong&gt;약 15µs&lt;/strong&gt; 내외로 과거 대비 거의 1,000배 가량 빨라졌다.&lt;/li&gt;
      &lt;li&gt;메모리와 저장 장치 사이의 속도 격차가 급격히 줄었다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-가용성과-관련한-수치들&quot;&gt;3. 가용성과 관련한 수치들&lt;/h1&gt;

&lt;p&gt;가용성은 시스템이 중단 없이 정상적으로 운영되는 시간을 뜻하며, 관습적으로 숫자 ‘9’를 사용하여 표시한다.(예: 99.9%는 ‘Three nine’)&lt;br /&gt;
9의 개수가 많을수록 뛰어난 안정성을 의미하지만, 그만큼 인프라 비용이 발생한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9의 개수와 시스템 장애 시간(downtime) 사이의 관계&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;가용률(SLA)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;하루당 장애시간&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;주당 장애시간&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;개월당 장애시간&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;연간 장애시간&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;99%&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;14.40분&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1.68시간&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;7.31시간&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;3.65일&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;99.9%&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1.44분&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;10.08분&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;43.83분&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;8.77시간&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;99.99%&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;8.64초&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;1.01분&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;4.38분&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;52.60분&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;99.999%&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;864.00밀리초&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;6.05초&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;26.30초&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;5.26분&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;99.9999%&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;86.40밀리초&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;604.80밀리초&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2.63초&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;31.56초&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-예시-sns-데이터-기반-qps와-저장소-요구량-추정&quot;&gt;4. 예시: SNS 데이터 기반 QPS와 저장소 요구량 추정&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;[가정 사항]&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;월간 능동 사용자(MAU)는 3억명&lt;/li&gt;
  &lt;li&gt;사용자의 50%는 매일 SNS를 사용함&lt;/li&gt;
  &lt;li&gt;평균적으로 각 사용자는 매일 2건의 포스트를 작성함&lt;/li&gt;
  &lt;li&gt;포스트 중 미디어(사진/영상)를 포함하는 포스트는 10%&lt;/li&gt;
  &lt;li&gt;데이터는 5년간 보관함&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;단계-1-qps-추정치-산출&quot;&gt;단계 1: QPS 추정치 산출&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;일간 능동 사용자(DAU)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;3억명 * 50% = 1.5억명&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;평균 QPS&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;1.5억명 * 2포스트 / 24시간 / 60분 / 60초 = &lt;strong&gt;약 3,500 QPS&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;최대 QPS(Peak QPS)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;2 * 평균 QPS = &lt;strong&gt;약 7,000 QPS&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;보통 평균 QPS의 2배&lt;/strong&gt;를 안전한 시스템 마지노선으로 계산하는 것이 업계 룰&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;단계-2-미디어-저장을-위한-저장소-요구량-산출&quot;&gt;단계 2: 미디어 저장을 위한 저장소 요구량 산출&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;1건당 평균 포스트 크기 산정&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;post_id: 64 byte&lt;/li&gt;
      &lt;li&gt;텍스트: 140byte
        &lt;ul&gt;
          &lt;li&gt;초창기 SNS 포스트의 상징적 글자 수 제한인 140자에서 유래한 수치임&lt;/li&gt;
          &lt;li&gt;ASCII 기준 1글자 = 1byte이므로 140byte&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;미디어: 1MB
        &lt;ul&gt;
          &lt;li&gt;사진 및 짧은 영상 썸네일의 ‘압축된 평균 크기’를 계산하기 쉽도록 1MB로 일반화&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;하루 미디어 저장소 요구량&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;1.5억명 * 2포스트 * 10% * 1MB = &lt;strong&gt;30TB/일&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;5년간 미디어 보관 요구량&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;30TB * 365일 * 5년 = &lt;strong&gt;약 55PB&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-마무리&quot;&gt;5. 마무리&lt;/h1&gt;

&lt;p&gt;개략적 규모 추정은 완벽히 정확한 숫자를 맞추는 것이 목적이 아니다.&lt;br /&gt;
주어진 요구사항 속에서 &lt;strong&gt;QPS, 최대 QPS, DB 저장소 요구량, 캐시 요구량, 필요한 웹 서버 수&lt;/strong&gt; 등을 논리적으로 도출하고, 그 과정에서 타당한 근거를 제시하는 
능력을 기르는 과정이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001033116&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://highscalability.com/google-pro-tip-use-back-of-the-envelope-calculations-to-choo/&quot;&gt;Google Pro Tip: Use Back-of-the-envelope-calculations&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://colin-scott.github.io/personal_website/research/interactive_latency.html&quot;&gt;Latency Numbers Every Programmer Should Know (Colin Scott)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
        <pubDate>Wed, 17 Jun 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/06/17/architecture-back-of-the-envelope-estimation-guide/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/06/17/architecture-back-of-the-envelope-estimation-guide/</guid>
        
        <category>architecture</category>
        
        <category>system-design</category>
        
        <category>scalability</category>
        
        <category>latency</category>
        
        <category>capacity-planning</category>
        
        <category>back-of-the-envelope</category>
        
        <category>qps</category>
        
        <category>high-availability</category>
        
        <category>대규모시스템설계</category>
        
        <category>규모추정</category>
        
        <category>응답지연</category>
        
        <category>고가용성</category>
        
        <category>트래픽예측</category>
        
        <category>용량산정</category>
        
        <category>아키텍처</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(2) - 대규모 지표 모니터링 및 경보 시스템 설계 아키텍처</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-요구사항-정의&quot;&gt;1. 요구사항 정의&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#1분-단위-데이터로-변환한다의-의미는-무엇일까&quot;&gt;💡’1분 단위 데이터로 변환한다’의 의미는 무엇일까?&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#11-기능적-요구사항&quot;&gt;1.1. 기능적 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#12-비기능-요구사항&quot;&gt;1.2. 비기능 요구사항&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-시계열-데이터-모델-및-저장소-아키텍처&quot;&gt;2. 시계열 데이터 모델 및 저장소 아키텍처&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-모니터링-시스템의-5가지-컴포넌트&quot;&gt;2.1. 모니터링 시스템의 5가지 컴포넌트&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-시계열-데이터-모델과-라인-프로토콜line-protocol&quot;&gt;2.2. 시계열 데이터 모델과 라인 프로토콜(Line Protocol)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#데이터-구조-관점에서의-list와-array를-채택한-이유&quot;&gt;💡데이터 구조 관점에서의 List와 Array를 채택한 이유&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-데이터-접근-패턴&quot;&gt;2.3. 데이터 접근 패턴&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#24-범용-저장소의-한계와-tsdbtime-series-database-채택-근거&quot;&gt;2.4. 범용 저장소의 한계와 TSDB(Time-Series Database) 채택 근거&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#시계열-db-가용성을-결정짓는-인덱스-cardinality-관리&quot;&gt;💡시계열 DB 가용성을 결정짓는 인덱스 Cardinality 관리&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#25-개략적-설계안&quot;&gt;2.5. 개략적 설계안&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-상세-설계-1-지표-수집-및-전송-파이프라인-확장-전략&quot;&gt;3. 상세 설계 1: 지표 수집 및 전송 파이프라인 확장 전략&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-지표-수집-아키텍처-분석&quot;&gt;3.1. 지표 수집 아키텍처 분석&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#311-pull-모델&quot;&gt;3.1.1. Pull 모델&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#3111-서비스-탐색-연동을-통한-동적-대상-식별&quot;&gt;3.1.1.1. 서비스 탐색 연동을 통한 동적 대상 식별&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#3112-안정-해시-링consistent-hashing-ring을-통한-수집-부하-분산&quot;&gt;3.1.1.2. 안정 해시 링(Consistent Hashing Ring)을 통한 수집 부하 분산&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#312-push-모델&quot;&gt;3.1.2. Push 모델&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#3121-에이전트-측-버퍼링-및-로드밸런서-배치&quot;&gt;3.1.2.1. 에이전트 측 버퍼링 및 로드밸런서 배치&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#313-pull-vs-push-모델&quot;&gt;3.1.3. Pull vs Push 모델&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#tcp-vs-udp-전송-성능-특성&quot;&gt;💡TCP vs UDP 전송 성능 특성&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#서버리스-패러다임과-하이브리드-수집-모델의-부상-이유&quot;&gt;💡서버리스 패러다임과 하이브리드 수집 모델의 부상 이유&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-지표-전송-파이프라인의-규모-확장&quot;&gt;3.2. 지표 전송 파이프라인의 규모 확장&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#321-카프카를-통한-대역폭-확장-및-메트릭-분류&quot;&gt;3.2.1. 카프카를 통한 대역폭 확장 및 메트릭 분류&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#지표별-우선순위-지정을-위한-큐-설계-기법&quot;&gt;💡지표별 우선순위 지정을 위한 큐 설계 기법&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#322-카프카의-대안-인메모리-시계열-db-고릴라gorilla-아키텍처&quot;&gt;3.2.2. 카프카의 대안: 인메모리 시계열 DB ‘고릴라(Gorilla)’ 아키텍처&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-데이터-집계-지점별-장단점&quot;&gt;3.3. 데이터 집계 지점별 장단점&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-상세-설계-2-질의-서비스&quot;&gt;4. 상세 설계 2: 질의 서비스&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-질의-서비스-캡슐화-및-캐시-계층&quot;&gt;4.1. 질의 서비스 캡슐화 및 캐시 계층&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#42-시계열-데이터베이스-질의어&quot;&gt;4.2. 시계열 데이터베이스 질의어&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#프로메테우스prometheus와-influxdb의-솔루션-포지셔닝-비교&quot;&gt;💡프로메테우스(Prometheus)와 InfluxDB의 솔루션 포지셔닝 비교&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#43-저장-용량-최적화를-위한-인코딩-및-델타-압축&quot;&gt;4.3. 저장 용량 최적화를 위한 인코딩 및 델타 압축&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#44-다운샘플링&quot;&gt;4.4. 다운샘플링&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#45-cold-storage&quot;&gt;4.5. Cold Storage&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-경보-및-시각화-시스템&quot;&gt;5. 경보 및 시각화 시스템&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#51-경보-시스템-연동-구조&quot;&gt;5.1. 경보 시스템 연동 구조&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#52-경보-피로alert-fatigue-방지를-위한-이벤트-병합&quot;&gt;5.2. 경보 피로(Alert Fatigue) 방지를 위한 이벤트 병합&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#53-grafana-기반의-시각화-시스템&quot;&gt;5.3. Grafana 기반의 시각화 시스템&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-요약&quot;&gt;6. 요약&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;수많은 서버와 마이크로 서비스로 구성된 대규모 인프라 환경에서 모니터링 시스템은 매우 중요하다.&lt;br /&gt;
시스템에 문제가 발생했을 때 즉각적으로 경보를 울려 대형 장애를 막아주고, 서비스가 안정적으로 운영되고 있는지 시각화해 주기 때문이다.&lt;/p&gt;

&lt;p&gt;여기서는 대형 IT 업체에서 내부적으로 사용하는 것과 유사한 서비스를 설계해본다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Datadog&lt;/li&gt;
  &lt;li&gt;influxdb&lt;/li&gt;
  &lt;li&gt;NewRelic&lt;/li&gt;
  &lt;li&gt;Nagios&lt;/li&gt;
  &lt;li&gt;Prometheus&lt;/li&gt;
  &lt;li&gt;MUNIN&lt;/li&gt;
  &lt;li&gt;Grafana&lt;/li&gt;
  &lt;li&gt;graphite&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-요구사항-정의&quot;&gt;1. 요구사항 정의&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;시스템 타깃&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 시스템의 고객은 누구인가? 회사 내부에서 사용하는 시스템인가, 아니면 Datadog처럼 제 3자 SaaS 제품을 설계하는 것인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; &lt;strong&gt;회사 내부에서 엔지니어들이 사용할 사내 인프라 모니터링 시스템&lt;/strong&gt;이다.&lt;br /&gt;
따라서 범용적인 멀티테넌시(Multi-tenancy) 기능보다 우리 회사의 대규모 인프라를 안정적이고 비용 효율적으로 처리하는데 집중한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;모니터링할 지표의 종류&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 어떤 지표들을 수집하는가? 에러로그나 비즈니스 매출 지표로 이 시스템에서 처리해야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; &lt;strong&gt;운영 지표만 수집&lt;/strong&gt;한다.&lt;br /&gt;
CPU 부하, 메모리 사용률부터 RPS, 웹 서버 프로세스 개수, 메시지 큐의 대기량 같은 고차원 지표가 대상이다.&lt;br /&gt;
회사의 매출같은 사업 지표나 에러 로그 수집, 분산 시스템 추적은 설계 범위가 아니다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;수집 데이터 규모&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 이 시스템에서 모니터링해야 할 인프라의 규모는 어느 정도인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 일간 능동 사용자 수(DAU)는 &lt;strong&gt;1억 명&lt;/strong&gt; 기준이다. &lt;br /&gt;
인프라는 약 &lt;strong&gt;1,000개의 서버 풀&lt;/strong&gt;로 구성되어 있으며, 풀마다 &lt;strong&gt;100개의 서버 하드웨어&lt;/strong&gt;가 존재하므로 관리 대상 장비는 총 100,000대이다.&lt;br /&gt;
서버 한 대당 100개의 운영 지표를 수집한다고 가정하면 시스템이 추적하고 관리해야 하는 지표의 총 개수는 &lt;strong&gt;약 1,000만 개(100 지표 * 100 서버 * 1,000 서버 풀)&lt;/strong&gt;에 달한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 수명 주기 및 해상도 변환 정책&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 지표 데이터의 최종 보관 기간은 얼마이며, 저장 공간을 아끼기 위해 데이터의 해상도(Resolution)를 낮추어 보관해도 되는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 전체 데이터 보관 기간은 &lt;strong&gt;1년&lt;/strong&gt;이다. 데이터 보관 비용을 최적화하기 위해 시간이 지남에 따라 해상도를 점진적으로 낮추는 정책을 채택한다.
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;최근 7일:&lt;/strong&gt; 수집된 원본 데이터를 그대로 보관&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;7일 이후~30일:&lt;/strong&gt; 데이터를 &lt;strong&gt;1분 단위&lt;/strong&gt;로 요약(집계)하여 보관&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;30일 이후~1년:&lt;/strong&gt; 데이터를 다시 &lt;strong&gt;1시간 단위&lt;/strong&gt;로 다시 요약하여 보관&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;알림 발송 채널 범위&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 시스템 장애나 이상 징후 감지 시 어떤 채널로 경보를 보내야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 엔지니어가 즉시 인지할 수 있도록 &lt;strong&gt;이메일, SMS, &lt;a href=&quot;https://www.pagerduty.com/&quot;&gt;PagerDuty&lt;/a&gt;, 그리고 외부 시스템과 연동 가능한 HTTPS 서비스 엔드포인트(Webhook)&lt;/strong&gt;를 기본적으로 지원해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;멀티테넌시(Multi-tenancy)&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;하나의 시스템을 여러 고객(테넌시)이 함께 나누어 쓰는 아키텍처&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;1분-단위-데이터로-변환한다의-의미는-무엇일까&quot;&gt;💡’1분 단위 데이터로 변환한다’의 의미는 무엇일까?&lt;/h2&gt;

&lt;p&gt;시계열 데이터의 &lt;strong&gt;다운샘플링(Downsampling)&lt;/strong&gt;이라고 한다.&lt;/p&gt;

&lt;p&gt;예를 들어 수집 에이전트가 특정 서버의 CPU 사용량을 &lt;strong&gt;10초에 한 번씩&lt;/strong&gt; 측정한다고 하면 1분 동안 총 6번의 데이터가 쌓이게 된다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;수집된 원본 데이터(10초 주기)&lt;/strong&gt;: [50%, 55%, 65%, 70%, 60%, 60%] -&amp;gt; 총 6개 로우(Row) 저장&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;7일이 지나면 이 6개의 row를 모두 들고있을 필요성이 떨어진다. 대시보드로 3주 전 기록을 조회할 때는 10초 단위의 미세한 떨림보다는 전체적인 흐름만 보면 되기 때문이다.&lt;br /&gt;
따라서 1분이 지나는 시점에 이 6개의 값을 &lt;strong&gt;하나의 대푯값&lt;/strong&gt;으로 저장한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;1분 단위 데이터로 변환 후:&lt;/strong&gt; [평균값: 60%] 또는 [최댓값: 70%] -&amp;gt; 단 1개의 로우(Row)로 압축&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-기능적-요구사항&quot;&gt;1.1. 기능적 요구사항&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;다양한 레이어의 지표 수집&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;인프라 저수준 지표: CPU 사용률, 메모리 사용량, 디스크 사용량 등 OS 레벨 데이터 수집&lt;/li&gt;
      &lt;li&gt;애플리케이션 고수준 지표: RPS, 웹 서버 프로세스 개수, 메시지 큐 내 대기 메시지 수 등&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;1,000개 서버풀 * 풀 당 100대 서버 = 총 100,000대의 장비
    &lt;ul&gt;
      &lt;li&gt;장비당 100개의 운영 지표를 동시 수집하며 &lt;strong&gt;총 10,000,000개의 실시간 지표 트래픽&lt;/strong&gt;을 누락없이 소화&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;기간별 데이터 해상도 자동 변환(다운샘플링)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;실시간성 보장: 수집 직후 최근 7일 동안은 가공되지 않은 고해상도 원본 데이터 보관&lt;/li&gt;
      &lt;li&gt;1차 요약: 저장소 효율을 위해 7일이 지난 데이터는 &lt;strong&gt;1분 단위&lt;/strong&gt;로 압축하여 30일간 보관&lt;/li&gt;
      &lt;li&gt;장기 보관: 30일이 지난 데이터는 &lt;strong&gt;1시간 단위&lt;/strong&gt;로 최종 압축하여 총 &lt;strong&gt;1년 동안&lt;/strong&gt; 보관&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;멀티 채널 경보&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;지정된 임계치(예: 디스크 사용량 &amp;gt; 90%) 초과 시 이상 징후 즉시 감지&lt;/li&gt;
      &lt;li&gt;이메일, SMS, PagerDuty로 경보 발송&lt;/li&gt;
      &lt;li&gt;사내 타 시스템과의 유연한 연동을 위해 HTTPS 서비스 엔드포인트(Webhook) 기능 제공&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;12-비기능-요구사항&quot;&gt;1.2. 비기능 요구사항&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;규모 확장성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;인프라가 증설되면서 밀려드는 지표의 수와 실시간 경보 연산의 양에 맞춰 시스템이 Scale-out 될 수 있어야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;낮은 응답 지연&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;대시보드와 경보를 신속히 처리 가능할 수 있도록 질의에 대한 낮은 응답 지연을 보장&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;안전성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;높은 안정성을 제공하여 중요 경보를 놓치지 않도록 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;유연성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;기술 트렌드는 빠르게 변화한다. 향후 새로운 데이터 수집 도구나 데이터 파이프라인 소프트웨어를 손쉽게 교체할 수 있도록 컴포넌트 간의 결합도가 낮은 유연한 아키텍처 구조를 가져야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-시계열-데이터-모델-및-저장소-아키텍처&quot;&gt;2. 시계열 데이터 모델 및 저장소 아키텍처&lt;/h1&gt;

&lt;p&gt;대규모 인프라 환경에서 발생하는 수많은 메트릭을 유실 없이 처리하기 위해서는 데이터 모델과 저장소 계층의 기술적 타당성을 먼저 검토해야 한다.&lt;br /&gt;
모니터링 시스템은 일반적인 CRUD 중심의 애플리케이션과 상이한 데이터 접근 패턴을 가진다.&lt;br /&gt;
RDBMS의 한계를 분석하고, 시계열 데이터베이스(TSDB)를 채택해야 하는 아키텍처적 근거에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-모니터링-시스템의-5가지-컴포넌트&quot;&gt;2.1. 모니터링 시스템의 5가지 컴포넌트&lt;/h2&gt;

&lt;p&gt;지표 모니터링 파이프라인은 기능과 책임에 따라 아래 5가지 컴포넌트로 구성된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;데이터 수집&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;호스트, 컨테이너, DB, 메시지 큐 등 모니터링 대상 자원으로부터 메트릭 데이터를 생성하고 수집&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 전송&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수집된 지표 데이터를 지연없이 모니터링 스토리지로 유입시키는 데이터 파이프라인 레이어&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 저장소&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;고빈도로 유입되는 시계열 데이터를 효율적으로 압축하여 메모리 및 디스크에 적재하는 영속성 스토리지 계층&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;경보&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;인입되는 실시간 데이터를 지속적으로 연산/평가하여 임계치 초과 등 이상 징후 감지 시 이메일, SMS 등으로 이벤트를 라우팅&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;시각화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수집된 지표를 차트나 그래프 형태로 렌더링하여 전체 인프라 상태를 직관적으로 관측할 수 있도록 돕는 대시보드(예: Grafana) 컴포넌트&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-시계열-데이터-모델과-라인-프로토콜line-protocol&quot;&gt;2.2. 시계열 데이터 모델과 라인 프로토콜(Line Protocol)&lt;/h2&gt;

&lt;p&gt;모니터링 시스템이 처리하는 지표 데이터는 시간의 흐름에 따라 순차적으로 누적되는 &lt;strong&gt;시계열 데이터(Time-Series Data)&lt;/strong&gt; 모델을 따른다.&lt;br /&gt;
&lt;a href=&quot;https://prometheus.io/docs/introduction/overview/&quot;&gt;프로메테우스(Prometheus)&lt;/a&gt; 및 InfluxDB 등의 메인스트림 솔루션은 상호 운용성을 확보하기 위해 텍스트 기반의 표준 포맷인 라인 프로토콜(Line Protocol)을 준수한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Line Protocol 데이터 포맷 예시&lt;/span&gt;
CPU.load &lt;span class=&quot;nv&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;webserver01,region&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;us-west 1613707265 50
CPU.load &lt;span class=&quot;nv&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;webserver01,region&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;us-west 1613707275 62
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;행의 마지막에 기록된 CPU 부하 수치의 평균을 구하면 되는 것이다.&lt;/p&gt;

&lt;p&gt;시계열 레코드는 내부적으로 &lt;a href=&quot;https://prometheus.io/docs/concepts/data_model/&quot;&gt;아래 3가지 속성&lt;/a&gt;으로 가공되어 저장소에 적재된다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;이름&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;자료형&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;아키텍처적 역할 및 특징&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;지표 이름&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;문자열(String)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;측정 대상 자체를 식별하기 위한 고유 명칭&lt;br /&gt;예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CPU.load&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memory.usage&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;태그/레이블 집합&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;Key:Value&amp;gt;&lt;/code&gt; 쌍의 리스트(List)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;다차원 분석, 필터링 및 그룹화를 위한 메타데이터셋&lt;br /&gt;예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;host=webserver01&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;지표 값 및 그 타임스탬프의 배열&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;Value, Timestamp&amp;gt;&lt;/code&gt; 쌍의 배열(Array)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Epoch 시간대별 실제 측정 수치 데이터의 시퀀스 엔트리&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;데이터-구조-관점에서의-list와-array를-채택한-이유&quot;&gt;💡데이터 구조 관점에서의 List와 Array를 채택한 이유&lt;/h3&gt;

&lt;p&gt;지표 모델에서 메타데이터는 List로, 실제 수치 데이터는 Array 구조로 정의하는 것은 메모리 및 디스크 I/O 효율을 극대화하기 위함이다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;태그/레이블 집합 → List&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;배포 환경이나 인프라 토폴로지에 따라 태그 속성은 동적으로 추가/삭제될 수 있어야 하므로 크기가 가변적임&lt;/li&gt;
      &lt;li&gt;따라서 요소의 삽입과 제거가 자유롭고 크기 확장이 유연한 동적 리스트 구조가 적합&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;지표 값 및 타임스탬프 → Array&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;타임스탬프와 지표 값은 정형화된 고정 크기의 데이터가 시간순으로 인입됨&lt;/li&gt;
      &lt;li&gt;이를 메모리상에서 연속된 공간(Array)으로 고정 배치해야만 특정 시간 범위를 스캔하는 &lt;strong&gt;Range Query&lt;/strong&gt; 시 Disk Seek Time을 줄이고 순차 읽기 성능을 최적화할 수 있음&lt;/li&gt;
      &lt;li&gt;연속된 숫자형 데이터 배열은 압축 알고리즘을 적용하기에도 훨씬 유리함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-데이터-접근-패턴&quot;&gt;2.3. 데이터 접근 패턴&lt;/h2&gt;

&lt;p&gt;지표 모니터링 시스템이 처리해야 하는 워크로드는 극단적인 &lt;strong&gt;I/O 비대칭성&lt;/strong&gt;을 가진다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;압도적인 쓰기 중심 워크로드&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수만 대의 호스트와 컨테이너가 10초~1분 주기로 지표를 끊임없이 밀어넣기 때문에, 시스템의 인입 쓰기 트래픽은 상시 높은 수준의 고점을 유지한다.&lt;/li&gt;
      &lt;li&gt;시스템 리소스의 대부분은 이 대량의 쓰기 적재 처리에 할당된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;간헐적인 읽기 스파이크 워크로드&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;읽기 연산은 상시 균등하게 발생하지 않는다.&lt;/li&gt;
      &lt;li&gt;경보 시스템이 매 분마다 평가 규칙을 실행하거나, 대시보드를 일제히 새로고침하는 시점에만 일시적으로 읽기 요청이 급증(Bursty Spike)하는 패턴을 보인다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 상시 몰아치는 대용량 쓰기 성능을 최우선으로 받아내면서, 간헐적인 대량 읽기 요청이 스토리지 엔진의 쓰기 파이프라인을 블로킹하지 않도록 &lt;strong&gt;I/O 격리 및 버퍼링 구조&lt;/strong&gt;를 
반드시 확보해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;24-범용-저장소의-한계와-tsdbtime-series-database-채택-근거&quot;&gt;2.4. 범용 저장소의 한계와 TSDB(Time-Series Database) 채택 근거&lt;/h2&gt;

&lt;p&gt;데이터 접근 패턴 및 시계열 연산 특성으로 인해 RDBMS나 범용 NoSQL은 대규모 환경에서 아래와 같은 기술적 임계점에 직면한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;RDBMS의 한계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;‘시계열 데이터의 지수 이동 평균값’같은 연속성 기반의 윈도우 연산을 SQL로 처리할 경우 쿼리 복잡도가 급격히 상승하며 실행 계획 최적화가 어렵다.&lt;/li&gt;
      &lt;li&gt;또한 다차원 필터링을 위해 레이블마다 B-Tree 인덱스를 생성하면, 대량의 쓰기 작업 시 인덱스 페이지 분할 및 갱신 오버헤드로 인해 스토리지 쓰기 성능이 급격히 저하된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;범용 NoSQL(Cassandra, &lt;a href=&quot;https://docs.cloud.google.com/bigtable/docs/schema-design-time-series?hl=ko&quot;&gt;Bigtable&lt;/a&gt; 등)의 한계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Scale-out을 통해 대용량 데이터를 수용할 수 있지만, 효과적인 시계열 질의(Range Scan 등)를 지원하려면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;내부 컬럼 패밀리 레이아웃&lt;/code&gt;에 최적화된 고도의 스키마 설계가 강제된다.&lt;/li&gt;
      &lt;li&gt;운영 공수와 인프라 관리 비용이 대폭 증가하는 단점이 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;시계열 데이터베이스(TSDB)의 최적화 구조&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;InfluxDB나 프로메테우스 같은 전용 TSDB는 쓰기 최적화된 로그 구조 스토리지 엔진(LSM-Tree 변형 아키텍처)과 인메모리 캐시 계층을 결합하여 대량 인입 트래픽을 효율적으로 소화한다.&lt;/li&gt;
      &lt;li&gt;8 CPU 코어와 32GB RAM을 갖춘 단일 InfluxDB 노드 환경에서도 &lt;strong&gt;초당 250,000회의 쓰기 연산&lt;/strong&gt;을 안정적으로 처리하도록 벤치마킹되어 있다.&lt;/li&gt;
      &lt;li&gt;InfluxDB는 레이블 기반의 신속한 데이터 질의를 위해 레이블별로 인덱스를 구축한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;컬럼 패밀리(Column-Family) 레이아웃&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;구글의 Bigtable이나 아파치 Cassandra같은 와이드 컬럼 스토어(Wide-Column Store) 데이터베이스가 &lt;strong&gt;데이터를 디스크에 물리적으로 정렬하고 저장하는 구조&lt;/strong&gt;를 말한다.&lt;/p&gt;

  &lt;p&gt;행(Row) 중심으로 데이터를 모아 저장하는 RDBMS와 달리, 컬럼 패밀리 데이터베이스는 &lt;strong&gt;행 키(Row Key)를 기준으로 데이터를 정렬한 뒤, 그 내부에서 연관된 컬럼들을 그룹(Column Family) 단위로 묶어서 디스크에 연속적으로 저장&lt;/strong&gt;한다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;시계열 데이터에 최적화된 저장소 시스템들은 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;OpenTSDB&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;분산 시계열 데이터베이스지만 하둡과 HBase에 기반하고 있어서 하둡/HBase 클러스터를 구성하고 운영해야 하므로 복잡&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;MetricsDB:&lt;/strong&gt; &lt;a href=&quot;https://blog.x.com/engineering/en_us/topics/infrastructure/2019/metricsdb&quot;&gt;X에서 사용 중&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://aws.amazon.com/ko/timestream/&quot;&gt;Timestream&lt;/a&gt;:&lt;/strong&gt; 아마존에서 출시&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&quot;https://db-engines.com/en/ranking/time+series+dbms&quot;&gt;DB-engines&lt;/a&gt;에 따르면 시장에서 가장 인기 있는 시계열 데이터베이스 2개는 &lt;a href=&quot;https://www.influxdata.com/&quot;&gt;InfluxDB&lt;/a&gt; 와 프로메테우스이다.&lt;/p&gt;

&lt;p&gt;핵심은 각 레이블이 가질 수 있는 값의 Cardinality가 낮아야 한다는 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;시계열-db-가용성을-결정짓는-인덱스-cardinality-관리&quot;&gt;💡시계열 DB 가용성을 결정짓는 인덱스 Cardinality 관리&lt;/h3&gt;

&lt;p&gt;시계열 DB는 레이블 단위의 고속 필터링을 지원하기 위해 내부적으로 &lt;strong&gt;역인덱스(Inverted Index)&lt;/strong&gt;를 구성하며, 이 인덱스는 빠른 쿼리 응답 속도를 위해 주로 메모리에 상주한다.&lt;br /&gt;
따라서 메타데이터의 &lt;strong&gt;카디널리티(고유값의 수)&lt;/strong&gt; 제어가 전체 시스템 가용성의 핵심 지표가 된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;낮은 카디널리티:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;region&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zone&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;env&lt;/code&gt; 등 고유값의 범위가 명확히 한정된 메타데이터 유형으로, 인덱스 엔트리의 크기가 예측 가능한 범위 내에서 안전하게 유지된다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;높은 카디널리티:&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uuid&lt;/code&gt; 처럼 거의 무한대에 가까운 고유값을 가지는 유형이다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;만일 지표의 태그 정보로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id&lt;/code&gt;나 매 요청마다 새로 생성되는 임의의 식별 문자열을 포함할 경우, 시계열 DB 내부의 인덱스 엔트리가 기하급수적으로 증가하는 
&lt;strong&gt;시계열 폭발(Time-Series Explosion)&lt;/strong&gt; 현상이 발생한다.&lt;br /&gt;
이는 &lt;strong&gt;OOM 예외&lt;/strong&gt;를 유발하여 DB 전체 노드를 다운시키는 치명적인 아키텍처적 결함으로 이어진다.&lt;/p&gt;

&lt;p&gt;따라서 &lt;strong&gt;모니터링 시스템의 태그/레이블을 반드시 카디널리티가 낮은 인프라 식별자 위주로 설계&lt;/strong&gt;해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;25-개략적-설계안&quot;&gt;2.5. 개략적 설계안&lt;/h2&gt;

&lt;p&gt;앞서 정의한 컴포넌트 간의 책임 분리와 데이터 흐름을 반영한 시스템 개략 설계안은 아래와 같다.&lt;br /&gt;
지표의 수집부터 영속화, 소비 레이어가 명확히 격리된 구조를 가진다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/1.png&quot; alt=&quot;개략적 설계안&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;지표 출처&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;애플리케이션 서버, SQL DB, 메시지 큐 등 원시 메트릭 데이터를 생성하는 인프라 컴포넌트&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;지표 수집기&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;지표 출처로부터 원시 데이터를 폴링하거나 수신하여 시계열 데이터 포맷으로 정돈하는 게이트웨이 역할&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;시계열 데이터베이스(TSDB)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;파이프라인을 통해 들어오는 메트릭 데이터를 압축 및 인덱싱하여 디스크에 영속화하는 타깃 저장소&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;질의 시스템&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;TSDB 전면에 위치하여 외부 시스템의 조회 요청을 대리 처리하는 전담 서비스 레이어&lt;/li&gt;
      &lt;li&gt;DB 직접 접근을 차단하여 스토리지 엔진의 부하를 격리함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;경보 시스템 &amp;amp; 시각화 시스템&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;질의 시스템을 인터페이스로 활용하여 정제된 데이터를 컨슈밍하는 다운스트림 컴포넌트&lt;/li&gt;
      &lt;li&gt;수집된 메트릭을 기반으로 알림을 하거나 대시보드를 렌더링함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-상세-설계-1-지표-수집-및-전송-파이프라인-확장-전략&quot;&gt;3. 상세 설계 1: 지표 수집 및 전송 파이프라인 확장 전략&lt;/h1&gt;

&lt;p&gt;인프라 규모가 커질수록 지표 수집기 클러스터와 시계열 데이터베이스(TSDB)가 직면하는 대역폭 압박은 심화된다.&lt;br /&gt;
대량의 메트릭 데이터를 정체 없이 적재하고, 특정 컴포넌트의 장애가 전체 파이프라인의 유실로 이어지지 않도록 제어하는 확장 전략을 다룬다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-지표-수집-아키텍처-분석&quot;&gt;3.1. 지표 수집 아키텍처 분석&lt;/h2&gt;

&lt;p&gt;CPU 사용량이나 네트워크 트래픽 같은 운영 지표는 결제나 정산 데이터와 달리 일부 패킷이 소실되더라도 시스템 전체의 비즈니스 정밀도에 치명적인 타격을 주지 않는다.&lt;br /&gt;
따라서 수집 클라이언트는 전송 성공 여부에 과도한 리소스를 할당하지 않고 동기식 확인(ACK)을 생략하는 형태로 설계 가능하다.&lt;br /&gt;
지표가 수집되는 전체적인 파이프라인 흐름은 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/collect.png&quot; alt=&quot;지표 수집 흐름&quot; /&gt;&lt;/p&gt;

&lt;p&gt;지표 데이터를 모니터링 코어 시스템으로 인입시키는 방식은 Pull 모델, Push 모델로 나뉘며, 인프라 토폴로지에 따라 명확한 트레이드오프가 존재한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;311-pull-모델&quot;&gt;3.1.1. Pull 모델&lt;/h3&gt;

&lt;p&gt;Pull 모델은 실행 중인 애플리케이션 서버나 인프라 타깃에 지표 수집기가 직접 접근하여 주기적으로 데이터를 폴링(Polling)하는 방식이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/pull.png&quot; alt=&quot;Pull 모델&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;3111-서비스-탐색-연동을-통한-동적-대상-식별&quot;&gt;3.1.1.1. 서비스 탐색 연동을 통한 동적 대상 식별&lt;/h4&gt;

&lt;p&gt;서버 가상화 및 컨테이너 환경에서는 인스턴스가 수시로 생성되고 소멸한다.&lt;br /&gt;
지표 수집기 내부에 대상 서버의 IP 목록을 고정 파일 형태로 관리하는 방식은 대규모 환경에서 적용이 불가하다.&lt;br /&gt;
이를 해결하기 위해 &lt;strong&gt;etcd&lt;/strong&gt;나 &lt;strong&gt;아파치 주키퍼&lt;/strong&gt;같은 &lt;strong&gt;서비스 탐색(Service Discovery) 시스템&lt;/strong&gt;을 파이프라인 전면에 연동한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/sds.png&quot; alt=&quot;서비스 탐색 기술 기반 pull 모델&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;각 서비스 인프라는 기동 시 자신의 엔드포인트 및 가용성 메타데이터를 서비스 탐색 시스템에 등록&lt;/li&gt;
  &lt;li&gt;지표 수집기는 서비스 탐색 시스템으로부터 활성화된 타깃 목록(IP, 포트, 수집 주기 등)을 동적으로 확보&lt;/li&gt;
  &lt;li&gt;지표 수집기는 서비스 탐색의 변경 이벤트 알림 콜백을 수신하여 수집 대상을 실시간으로 최신화&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;3112-안정-해시-링consistent-hashing-ring을-통한-수집-부하-분산&quot;&gt;3.1.1.2. 안정 해시 링(Consistent Hashing Ring)을 통한 수집 부하 분산&lt;/h4&gt;

&lt;p&gt;엄청난 양의 지표를 단일 수집기 노드로 처리할 수 없으므로 수집기 서버를 대규모 클러스터로 확장해야 한다.&lt;br /&gt;
이 때 여러 수집기가 동일한 대상 서버에 접근하여 지표를 중복 수집하는 현상을 방지하기 위해 &lt;strong&gt;안정 해시 링&lt;/strong&gt; 분산 메커니즘을 도입한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/ring.png&quot; alt=&quot;안정 해시링&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;대상 서버의 식별자 해시값을 기반으로 해시 링 위에 위치를 할당하고, 수집기 노드가 특정 구간에 속한 서버들의 지표 수집 워크로드만 전담하도록 격리&lt;/li&gt;
  &lt;li&gt;위 그림에서 수집기 2는 해시 공간 구조에 따라 S1(서버 1)과 S5(서버 5) 구간의 지표 수집만을 담당하므로 클러스터 내의 중복 연산을 원천 차단함&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;312-push-모델&quot;&gt;3.1.2. Push 모델&lt;/h3&gt;

&lt;p&gt;Push 모델은 대상 호스트 내부에서 메트릭을 생성하는 서비스나 에이전트가 주체가 되어, 수집기 서버로 데이터를 직접 전송하는 방식이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/push.png&quot; alt=&quot;푸시 모델&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;3121-에이전트-측-버퍼링-및-로드밸런서-배치&quot;&gt;3.1.2.1. 에이전트 측 버퍼링 및 로드밸런서 배치&lt;/h4&gt;

&lt;p&gt;모니터링 대상 장비에 가벼운 &lt;strong&gt;수집 에이전트&lt;/strong&gt; 소프트웨어를 설치하여 커널 및 애플리케이션 지표를 수집한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;로컬 집계 및 버퍼링&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;에이전트는 원시 데이터를 즉시 전송하지 않고, 로컬 메모리 버퍼에서 1차적인 메트릭 요약 및 집계를 수행하여 아웃바운드 패킷 밀도를 낮춤&lt;/li&gt;
      &lt;li&gt;다운스트림 수집기 서버가 일시적인 부하로 에러를 반환하면, 에이전트는 로컬 디스크나 링 버퍼에 데이터를 임시 보관한 후 재전송하여 데이터 소실 방지&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;로드밸런싱 및 오토스케일링&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;대량의 에이전트가 동시다발적으로 메트릭을 전송할 수도 있으므로 수집기 서버는 레이어 전면에 고가용성 &lt;strong&gt;로드 밸런서&lt;/strong&gt; 배치가 강제됨&lt;/li&gt;
      &lt;li&gt;트래픽 인입 강도에 대응할 수 있도록 수집기 서버 클러스터 또한 오토스케일링 그룹으로 구성해야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;푸시 모델의 경우 모니터링 대상 서버에 통상 수집 에이전트라고 부르는 소프트웨어를 설치한다.
수집 에이전트는 해당 장비에서 실행되는 서비스가 생산하는 지표 데이터를 받아 모은 후 주기적으로 수집기에 전달한다.
간단한 지표의 경우 수집기에 보내기 전에 에이전트가 직접 데이터 집계 등의 작업을 처리할 수도 있다.
데이터 집계는 수집기에 보내는 데이터의 양을 줄이는 효과적인 방법이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/lb_push.png&quot; alt=&quot;로드밸런서 및 지표 수집기 클러스터의 auto scaling&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;313-pull-vs-push-모델&quot;&gt;3.1.3. Pull vs Push 모델&lt;/h3&gt;

&lt;p&gt;두 모델은 기술적 장단점이 뚜렷하므로 인프라 환경의 제약 조건에 따라 채택해야 한다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;비교 항목&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Pull(풀) 모델 (예: Prometheus)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Push(푸시) 모델 (예: &lt;a href=&quot;https://aws.amazon.com/ko/cloudwatch/&quot;&gt;CloudWatch&lt;/a&gt;, &lt;a href=&quot;https://graphiteapp.org/&quot;&gt;Graphite&lt;/a&gt;)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;디버깅 편의성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;각 서버가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/metrics&lt;/code&gt; HTTP 엔드포인트를 열어두므로, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; 명령어로 실제 노출되는 지표 데이터 포맷을 즉시 검증할 수 있어 &lt;strong&gt;Pull 모델이 유리&lt;/strong&gt;합니다.&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;에이전트 내부 링 버퍼 상태나 전송 아웃바운드 패킷을 추적해야 하므로 디버깅 상태 강도가 높습니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;상태 진단&lt;br /&gt;(Health Check)&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;수집기가 Pull 요청을 날렸을 때 대상 서버가 응답하지 않으면, 인프라 장애나 애플리케이션 크래시 상태임을 &lt;strong&gt;즉각적이고 명확하게 판별&lt;/strong&gt;할 수 있습니다.&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;지표 유입이 중단되었을 때, 에이전트 프로세스 자체의 다운인지 단순 네트워크 패킷 드롭인지 원인 추적이 모호합니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;단기 프로세스 처리&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;수집 주기(Poll Interval)보다 생명 주기가 짧은 일괄 작업(Batch Job)이나 서버리스 함수의 지표는 미처 수집하기 전에 증발하므로 단독 처리 방식으로는 &lt;strong&gt;불리&lt;/strong&gt;합니다.&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;프로세스 소멸 직전에 수집기로 데이터를 직접 푸시하고 종료할 수 있으므로 임시 프로세스 모니터링에 &lt;strong&gt;적합&lt;/strong&gt;합니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;네트워크 구성 및 방화벽&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;수집기가 대상 서버 내부망 포트로 진입해야 하므로, 멀티 데이터센터 망이나 DMZ망 구조 환경에서는 인바운드 방화벽 규칙 관리가 &lt;strong&gt;매우 복잡&lt;/strong&gt;해집니다.&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;수집기 전면의 로드밸런서(주로 아웃바운드 HTTPS 단일 포트) 방향으로만 데이터를 밀어 넣으면 되므로 네트워크 보안 구성이 &lt;strong&gt;간결&lt;/strong&gt;합니다.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;데이터 신뢰성&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;서비스 탐색을 통해 이미 신뢰 관계가 검증된 화이트리스트 대상 서버의 지표만 수집하므로 타겟 위조 위험이 낮습니다.&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;외부 노드가 수집기 엔드포인트로 위조 패킷을 주입할 위험이 존재하므로, 수집기 레이어에서 API 토큰 인증이나 IP ACL 검증 로직이 추가로 요구됩니다.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;tcp-vs-udp-전송-성능-특성&quot;&gt;💡TCP vs UDP 전송 성능 특성&lt;/h4&gt;

&lt;p&gt;지표 전송 파이프라인에서 Pull 모델은 HTTP 기반의 안정적인 &lt;strong&gt;TCP&lt;/strong&gt; 연결을 지향하며, 일부 Push 모델은 오버헤드 최소화를 위해 &lt;strong&gt;UDP&lt;/strong&gt; 를 활용한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;TCP&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;3-way handshake 및 슬라이딩 윈도우 기반의 흐름 제어로 패킷 전송을 보장하여 지표의 정밀도를 보장함&lt;/li&gt;
      &lt;li&gt;연결 비용은 HTTP/2 멀티플렉싱이나 지속 커넥션(Keep-Alive) 풀링으로 최적화함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;UDP&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;세션 연결 및 확인 응답 과정이 없어 패킷 전송 지연이 극도로 낮음&lt;/li&gt;
      &lt;li&gt;패킷 유실 가능성이 존재하지만, 메트릭 특성상 일부 유실이 치명타가 아니라는 전제하에 호스트 리소스를 극도로 아껴야 하는 초고빈도 전송 환경에서 제한적으로 채택됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;풀 모델과 푸시 모델 가운데 무엇이 나은지는 정답이 없다.
&lt;a href=&quot;https://aws.amazon.com/ko/lambda/serverless-architectures-learn-more/&quot;&gt;서버리스 기술&lt;/a&gt;이 각광받음에 따라 많은 조직이 두 모델을 모두 지원하고 있다.
지표 수집 에이전트를 설치할 서버가 마땅히 존재하지 않을 수도 있다는 점을 감안해야 한다는 뜻이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;서버리스-패러다임과-하이브리드-수집-모델의-부상-이유&quot;&gt;💡서버리스 패러다임과 하이브리드 수집 모델의 부상 이유&lt;/h4&gt;

&lt;p&gt;최근 대규모 아키텍처는 AWS Lambda 등의 서버리스 및 이피머럴(Ephemeral, 임시) 인프라의 도입 비중이 높다.&lt;br /&gt;
서버리스 환경은 개발자가 하부 VM 인프라에 접근할 수 없어 수집 에이전트를 상주시킬 수 없으며, 요청 처리가 완료되면 수 초 내에 컨테이너가 소멸한다.&lt;/p&gt;

&lt;p&gt;이러한 제약 조건으로 인해 최신 대규모 시스템은 단일 수집 아키텍처를 고집하지 않고 &lt;strong&gt;하이브리드&lt;/strong&gt; 방식을 취한다.&lt;br /&gt;
상시 가동되는 코어 인프라는 서비스 탐색 기반의 &lt;strong&gt;Pull 모델&lt;/strong&gt;로 고가용성 상태 감시를 수행하고,&lt;br /&gt;
서버리스 및 단기 배치 프로세스 레이어는 실행 종료 시점에 메트릭 게이트웨이로 데이터를 전송하는 &lt;strong&gt;Push 모델&lt;/strong&gt;을 혼용하여 상호 보완하도록 설계한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-지표-전송-파이프라인의-규모-확장&quot;&gt;3.2. 지표 전송 파이프라인의 규모 확장&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/transfer.png&quot; alt=&quot;지표 전송 파이프라인&quot; /&gt;&lt;/p&gt;

&lt;p&gt;수집기 클러스터가 TSDB에 직접 데이터를 동기식으로 쓰게 되면 대량의 쓰기 스파이크가 발생하거나 TSDB 레이어에 장애가 생겼을 때 파이프라인 전체가 마비되어 메트릭이 대거 
소실될 리스크가 존재한다.&lt;br /&gt;
지표 수집기와 시계열 데이터베이스 사이에 분산 메시지 큐인 아파치 카프카를 배치하여 안정적인 비동기 버퍼 계층을 구현한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/kafka.png&quot; alt=&quot;큐 추가&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림에서 지표 수집기는 지표 데이터를 카프카와 같은 큐 시스템에 전송한다. 그러면 아파치 스톰(Storm)이나 플링크(Flink), 스파크(Spark) 같은 소비자 즉 스트림 처리 서비스가
해당 데이터를 받아 시계열 데이터베이스에 저장한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장애 도메인 격리(Decoupling)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 수집 레이어와 저장 처리 레이어를 물리적으로 격리하여, 후방의 데이터베이스가 다운되더라도 전방의 수집 파이프라인은 정상 가동되도록 보장&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;배압(Backpressure) 제어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;카프카가 인입된 대량의 데이터를 로그 세그먼트 디스크에 안전하게 적재하므로, 다운스트림 스트림 프로세서(Flink, Storm, Consumer가 포함된 스트림 서비스 등)는 TSDB의 쓰기 
한계 용량에 맞춰 적절한 속도로 데이터를 컨슈밍(소비 속도 제어)할 수 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;321-카프카를-통한-대역폭-확장-및-메트릭-분류&quot;&gt;3.2.1. 카프카를 통한 대역폭 확장 및 메트릭 분류&lt;/h3&gt;

&lt;p&gt;카프카의 병렬 처리 단위인 파티션 메커니즘을 이용하면 천만 개 지표 트래픽을 선형적으로 Scale-out 할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/partition.png&quot; alt=&quot;카프카 파티션&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;지표 이름 기반 파티셔닝&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수집된 메트릭 데이터를 카프카 토픽으로 발행할 때 지표 이름을 파티션 키로 지정&lt;/li&gt;
      &lt;li&gt;이 구성을 통해 동일한 종류의 지표는 항상 동일한 파티션 번호로 정렬되어 유입되므로, 다운스트림 소비자가 데이터를 메모리에 올려 시간 윈도우별 집계 연산을 수행하기가 수월해짐&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;태그 조합을 통한 핫스팟 방지&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 핵심 지표의 트래픽이 비정상적으로 몰려 단일 파티션의 대역폭 임계치를 넘어설 경우, 지표 이름 뒤에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;region&lt;/code&gt;이나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zone&lt;/code&gt; 같은 카디널리티가 낮은 태그 정보를 조합하여 
파티션 키를 생성함으로써 부하를 여러 파티션으로 균등하게 분산&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;중요 지표는 먼저 처리될 수 있도록 우선순위 지정&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;지표별-우선순위-지정을-위한-큐-설계-기법&quot;&gt;💡지표별 우선순위 지정을 위한 큐 설계 기법&lt;/h4&gt;

&lt;p&gt;카프카의 파티션은 구조적으로 FIFO 방식으로 동작하므로, 단일 파티션 내부에서 특정 메시지의 우선순위를 인위적으로 변경할 수 없다.&lt;br /&gt;
따라서 실시간 가용성에 직접 영향을 주는 핵심 지표(예: 서비스 다운과 직결되는 5xx 에러율)를 일반 운영 지표(예: 디스크 남은 용량)보다 먼저 적재하기 위해서는 
&lt;strong&gt;우선순위별 멀티 토픽 아키텍처&lt;/strong&gt;를 설계해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;토픽의 등급별 격리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;카프카 내부에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metrics.priority.high&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metrics.priority.low&lt;/code&gt; 토픽을 물리적으로 분리하여 개설&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Producer 레이어의 라우팅 분류&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;지표 수집기에서 메트릭을 카프카로 발행하기 전 등급을 평가함&lt;/li&gt;
      &lt;li&gt;임계치 평가가 실시간으로 필요한 핵심 지표는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;high&lt;/code&gt; 토픽으로, 지연되어도 무방한 일반 지표는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;low&lt;/code&gt; 토픽으로 분기하여 전송&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Consumer 가중치 할당 제어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;다운스트림 프로세서는 두 토픽을 동시에 폴링하되, 컨슈머 루프 내부에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;high&lt;/code&gt; 토픽의 처리 &lt;strong&gt;스레드 풀 배정 비율과 폴링 빈도&lt;/strong&gt;를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;low&lt;/code&gt; 토픽보다 높게 가져가는 가중치 기반 라운드 로빈(Weighted Round-robin) 방식을 적용&lt;/li&gt;
      &lt;li&gt;이를 통해 트래픽 폭주 상황에서 크리티컬 경보 지표가 큐에 갇혀 지연되는 현상 방지&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;322-카프카의-대안-인메모리-시계열-db-고릴라gorilla-아키텍처&quot;&gt;3.2.2. 카프카의 대안: 인메모리 시계열 DB ‘고릴라(Gorilla)’ 아키텍처&lt;/h3&gt;

&lt;p&gt;아키텍처 구성 요소의 복잡성을 줄이기 위해 중간 미들웨어인 카프카를 생략하고 스토리지 레이어 자체의 쓰기 고가용성으로 대량 트래픽을 감당하는 대안도 있다.&lt;br /&gt;
페이스북이 엔지니어링 논문으로 공개한 인메모리 시계열 데이터베이스 시스템인 &lt;a href=&quot;https://www.vldb.org/pvldb/vol8/p1816-teller.pdf&quot;&gt;고릴라(Gorilla)&lt;/a&gt;가 대표적인 레퍼런스이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;고릴라는 디스크 I/O 병목을 우회하기 위해 모든 실시간 인입 데이터를 완전한 인메모리 구조로 관리하며, 네트워크 단절이나 일부 노드 장애가 발생하더라도 느슨한 동기화 및 복제 메커니즘을 통해 높은 수준의 쓰기 연산 가용성을 유지&lt;/li&gt;
  &lt;li&gt;이와 같이 초고성능 쓰기 전용 스토리지 아키텍처를 직접 구축하여 매핑할 경우, 메시지 큐 인프라의 관리 오버헤드와 트레이드오프할 수 있음&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-데이터-집계-지점별-장단점&quot;&gt;3.3. 데이터 집계 지점별 장단점&lt;/h2&gt;

&lt;p&gt;천만 개 수준의 대규모 메트릭 환경에서는 Raw 데이터를 어느 파이프라인 단계에서 요약/집계하여 적재할지가 전체 스토리지 용량 최적화의 핵심이 된다.&lt;br /&gt;
집계는 파이프라인 상의 3가지 지점에서 수행 가능하다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;수집 에이전트 단계(호스트 소스 로컬)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;방식:&lt;/strong&gt; 대상 호스트 내부 메모리에서 10초~1분간 유입된 지표를 미리 평균 및 합산하여 수집기 서버로 전송&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; 아웃바운드로 나가는 네트워크 대역폭과 패킷 발생 수가 급격히 감소하여 전송 비용 절감&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; 에이전트 프로세스가 호스트의 CPU 및 RAM 자원을 일부 점유하며, 여러 호스트의 데이터를 묶어 처리하는 복잡한 다차원 스트림 연산이 불가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 전송 파이프라인 단계(수집기 및 스트림 엔진 레이어)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;방식:&lt;/strong&gt; 카프카 소비단 전면에 Flink, Spark 같은 스트림 처리 엔진을 배치하여 TSDB에 쓰기 전 인메모리 타임 윈도우 연산으로 데이터를 요약 처리&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; TSDB에 최종 적재되는 row 수 자체가 압축되므로 스토리지 용량을 아낄 수 있음&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; 네트워크 지연으로 인해 늦게 도착하는 지표 데이터에 대한 정밀한 정리가 까다로우며, Raw 데이터가 유실되어 사후 장애 정밀 분석(Forensics)이 어려워짐&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;질의 실행 단계(On-Query Aggregation)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;방식:&lt;/strong&gt; 가공되지 않은 모든 원시 시계열 데이터를 TSDB에 100% 그대로 적재하고, 대시보드를 조회하거나 경보 시스템이 규칙을 평가하는 쿼리 시점에 실시간 연산&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; 원본 데이터의 손실이 전혀 없으므로 필요한 시간 구간에 맞춰 정밀도와 필터링을 무제한으로 유연하게 변경 가능&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; 질의를 처리하는 순간에 수억 건의 레코드 세트를 대상으로 매번 전체 집계 연산을 계산해야 하므로, 대시보드 로딩 속도가 느려지고 DB에 막대한 부하 유발&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-상세-설계-2-질의-서비스&quot;&gt;4. 상세 설계 2: 질의 서비스&lt;/h1&gt;

&lt;p&gt;대규모 시계열 데이터 백엔드는 밀려오는 쓰기 트래픽을 소화하는 것만큼, 적재된 대규모 데이터셋을 지연없이 조회하고 스토리지 비용을 효율적으로 통제하는 것이 중요하다.&lt;br /&gt;
여기서는 질의 계층의 격리 구조와 디스크 I/O 및 용량을 최적화하는 방법에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-질의-서비스-캡슐화-및-캐시-계층&quot;&gt;4.1. 질의 서비스 캡슐화 및 캐시 계층&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/cache.png&quot; alt=&quot;캐시 계층&quot; /&gt;&lt;/p&gt;

&lt;p&gt;시각화 대시보드(Grafana)나 경보 시스템이 시계열 데이터베이스(TSDB)에 직접 대량의 원시 쿼리를 수행하게 되면, 스토리지 엔진의 자원이 고갈되어 실시간 인입 쓰기 
파이프라인까지 마비되는 연쇄 장애가 발생할 수 있다.&lt;br /&gt;
이를 방지하기 위해 stateless 질의 서버 클러스터 풀로 구성된 &lt;strong&gt;질의 서비스 레이어&lt;/strong&gt;를 전면에 배치한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;저장소 추상화 및 캡슐화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;질의 서비스는 하부의 구체적인 TSDB 솔루션을 캡슐화하는 인터페이스 역할을 함&lt;/li&gt;
      &lt;li&gt;다운스트림 클라이언트(시각화, 경보)는 질의 서비스의 API 표준만 바라보므로, 후방의 TSDB를 다른 제품군으로 교체하더라도 상위 레이어의 코드 변경이 발생하지 않는 유연성 확보&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;캐시 계층 도입을 통한 I/O 격리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;엔지니어들이 상시 열어두는 대시보드나 주기적으로 반복 실행되는 경보 평가 규칙은 동일한 시간 범위의 데이터를 중복 요청하는 경향이 강함&lt;/li&gt;
      &lt;li&gt;질의 서비스 전면에 Redis 또는 인메모리 캐시 계층을 배치하여 동일 질의 결과를 캐싱함으로써 TSDB의 읽기 연산 부하를 낮춤&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;42-시계열-데이터베이스-질의어&quot;&gt;4.2. 시계열 데이터베이스 질의어&lt;/h2&gt;

&lt;p&gt;시계열 데이터 분석에 관계형 표준 SQL을 사용하는 것은 데이터 처리 아키텍처 관점에서 비효율적이다.&lt;br /&gt;
연속적인 시간 범위 내의 가중치 연산이나 누적 이동 평균을 계산할 때 SQL을 극도로 복잡한 윈도우 함수와 서브쿼리, Gaps-and-Islands 기법 처리가 강제되어 
파싱 오버헤드가 크고 실행 계획 최적화가 어렵다.&lt;/p&gt;

&lt;p&gt;반면 시계열 특화 도메인 언어(DSL)은 InfluxDB의 &lt;strong&gt;Flux&lt;/strong&gt;나 프로메테우스의 &lt;strong&gt;PromQL&lt;/strong&gt;은 시간 윈도우 기반의 스트림 파이프라인 연산자를 네이티브하게 지원하므로, 
연산 파이프라인이 간결하며 스토리지 엔진 내부에서 인덱스를 타고 고속 처리된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;프로메테우스prometheus와-influxdb의-솔루션-포지셔닝-비교&quot;&gt;💡프로메테우스(Prometheus)와 InfluxDB의 솔루션 포지셔닝 비교&lt;/h3&gt;

&lt;p&gt;두 솔루션 모두 ‘단순한 DB인가?’라는 의문에 대해 시스템 아키텍처 관점에서는 명확한 컴포넌트적 차이가 존재한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;InfluxDB&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;구조적으로 범용적이고 독립적인 &lt;strong&gt;고성능 시계열 데이터베이스 자체&lt;/strong&gt;에 집중&lt;/li&gt;
      &lt;li&gt;대량의 데이터 쓰기를 받아내기 위한 스토리지 엔진 최적화가 핵심&lt;/li&gt;
      &lt;li&gt;데이터 수집은 외부 에이전트에 위임하는 구조&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;프로메테우스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;단순 데이터베이스를 넘어 &lt;strong&gt;수집(Scraper), 저장(Built-in TSDB), 알림 연동(Alertmanager) 기능을 내장한 올인원 모니터링 에코 시스템&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;시계열 저장소 기능도 훌륭하지만, 서비스 탐색과 결합하여 대상 인프라를 능동적으로 폴링하는 ‘수집 파이프라인 런타임’으로서의 포지셔닝이 강함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;43-저장-용량-최적화를-위한-인코딩-및-델타-압축&quot;&gt;4.3. 저장 용량 최적화를 위한 인코딩 및 델타 압축&lt;/h2&gt;

&lt;p&gt;대규모 지표가 실시간 적재되는 환경에서는 압축 알고리즘의 효율성이 인프라 비용 통제의 성패를 가른다.&lt;br /&gt;
TSDB는 데이터 타입별 특성을 고려하여 타임스탬프 영역과 지표 수치(Value) 영역을 분리하여 압축한다.&lt;br /&gt;
대표적인 기법이 페이스북 고릴라(Gorilla) 아키텍처에서 정립된 이중-델타 인코딩이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/encoding.png&quot; alt=&quot;데이터 인코딩&quot; /&gt;&lt;/p&gt;

&lt;p&gt;일반적으로 Unix 타임스탬프 하나를 온전히 표현하려면 32bit 또는 64bit의 정밀도가 필요하다.&lt;br /&gt;
(위 그림에서 보듯 1610087371과 1610087381은 딱 10초만큼만 다른 값이며, 타임스탬프 하나를 온전히 표현하는데는 32비트가 필요하지만 10을 표현하는데는 4비트면 충분)
하지만 모니터링 에이전트가 10초 주기로 정확히 지표를 수집한다면, 타임스탬프 간의 차이(첫 번째 델타, \(\Delta\))는 항상 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10&lt;/code&gt;이라는 고정값을 가진다.&lt;br /&gt;
따라서 데이터를 완전한 형태로 저장하는 대신 기준값과의 차이를 1610087371, 10, 10, 9, 11과 같이 저장한다.&lt;/p&gt;

&lt;p&gt;여기서 한 단계 더 나아가 델타 값 간의 차이인 이중 델타(\(D\))를 계산하면 아래와 같은 흐름을 보인다.&lt;/p&gt;

\[D = \Delta_t - \Delta_{t-1}\]

&lt;p&gt;수집 주기가 일정하다면 이중 델타 값은 지속적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt;이 도출된다.&lt;/p&gt;

&lt;p&gt;위 그림의 구조를 수식으로 풀면 아래와 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;정기 수집 구간 (T1, T2):&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수집 오프셋이 정확히 10초이므로 이중 델타 값은 0이 되며, 이는 비트 패킹을 통해 단 1비트의 공간만으로 디스크에 저장 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;미세 지연 구간 (T3, T4):&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;네트워크 지연 등으로 수집 주기가 9초, 11초로 흔들리더라도 이중 델타 값은 -1, 2 등 영(0)에 극도로 수렴하는 작은 정수형 데이터가 도출되므로,&lt;br /&gt;
표준 가변 길이 비트 인코딩(Huffman Coding 변형)을 통해 수 비트 이내로 물리적 크기를 극대화하여 압축 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;44-다운샘플링&quot;&gt;4.4. 다운샘플링&lt;/h2&gt;

&lt;p&gt;인프라 요구사항에 따라 7일이 지난 고해상도 원본 데이터는 저장 공간 절감을 위해 30초 또는 1분 주기의 저해상도 데이터로 변환(다운샘플링) 가공 과정을 거친다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;지표&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;타임스탬프&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;호스트명&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;지표 값&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;cpu&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021-10-24T19:00:00Z&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;host-a&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;cpu&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021-10-24T19:00:10Z&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;host-a&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;16&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;cpu&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021-10-24T19:00:20Z&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;host-a&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;20&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;cpu&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021-10-24T19:00:30Z&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;host-a&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;30&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;cpu&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021-10-24T19:00:40Z&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;host-a&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;20&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;cpu&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021-10-24T19:00:50Z&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;host-a&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;30&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;위 10초 해상도 데이터를 30초 해상도 데이터로 집계한 결과는 아래와 같다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;지표&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;타임스탬프&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;호스트명&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;지표 값(avg)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;cpu&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021-10-24T19:00:00Z&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;host-a&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;19&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;cpu&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;2021-10-24T19:00:30Z&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;host-a&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;25&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;타임스탬프 2021-10-24T19:00:00Z 구간의 지표값 ‘19’ 도출 구조&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시스템에 설정된 다운샘플링 타임 윈도우가 시작 경계면을 포함하는 구조([19:00:00Z, 19:00:30Z])로 작동할 경우, 해당 30초 구간 내에 포함되는 4개의 원시 Raw 값을 타깃으로 지정&lt;/li&gt;
      &lt;li&gt;\(\frac{10 + 16 + 20 + 30}{4} = \frac{76}{4} = 19\) (평균치 통계 적용)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;타임스탬프 2021-10-24T19:00:30Z 구간의 지표값 ‘25’ 도출 구조&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;앞선 윈도우의 경계면 바로 다음 시점부터 그 다음 30초 구간을 커버하는 원시 로우셋&lt;/li&gt;
      &lt;li&gt;
\[\frac{20 + 30}{2} = \frac{50}{2} = 25\]
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;45-cold-storage&quot;&gt;4.5. Cold Storage&lt;/h2&gt;

&lt;p&gt;대규모 인프라 관측 가능성 연구 논문에 따르면, 실제 프로덕션 환경에서 발생하는 운영 데이터 질의의 &lt;strong&gt;약 85%는 최근 26시간 이내에 수집된 최신 지표 데이터&lt;/strong&gt;를 대상으로 집중된다.&lt;br /&gt;
이 워크로드 특성을 기반으로 스토리지 계층화 정책을 수립해야 인프라 비용 상승을 막을 수 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Hot Tier - 최적화 구조&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;최근 26~48시간 이내의 고해상도 메트릭 데이터는 고성능 SSD 또는 초고속 인메모리 스토리지 영역에 배치하여 경보 시스템 평가 및 실시간 트러블슈팅 질의 지연 최소화&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Warm/Cold Tier - 저비용 구조&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;다운샘플링이 완료된 30일 이상, 1년 미만의 장기 보관용 메트릭 데이터는 블록 스토리지가 아닌 GB당 단가가 저렴한 오브젝트 스토리지(AWS S3, Google Cloud Storage 등)로 이관 처리&lt;/li&gt;
      &lt;li&gt;오브젝트 스토리지는 탐색 지연이 존재하지만, 수개월 전의 장기 추세를 분석하는 리포팅 질의 특성 상 수 초 내외의 응답 지연은 충분히 허용 가능한 트레이드오프 범위 내에 있기 때문임&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-경보-및-시각화-시스템&quot;&gt;5. 경보 및 시각화 시스템&lt;/h1&gt;

&lt;p&gt;모니터링 시스템의 최종 목적지는 수집된 데이터를 기반으로 위험 상황을 실시간 감지하여 전송하는 경보와 인프라 상태를 직관적으로 파악할 수 있게 하는 시각화이다.&lt;br /&gt;
이 두 도메인은 시장에 오픈소스 및 기성 솔루션(Grafana, PagerDuty 등)이 매우 성숙해 있으므로, 직접 구현하기보다 안정적인 컴포넌트를 연동하는 
아키텍처를 취하는 것이 비용 대비 효과 측면에서 유리하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;51-경보-시스템-연동-구조&quot;&gt;5.1. 경보 시스템 연동 구조&lt;/h2&gt;

&lt;p&gt;경보 파이프라인은 실시간 쓰기 레이어와 결합도를 낮추고 분산 가용성을 확보하기 위해 아래와 같이 이벤트 기반 아키텍처(EDA)로 설계한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;경보 처리 워크로드 흐름&lt;/strong&gt;
&lt;strong&gt;1. 규칙 설정 파일 로드:&lt;/strong&gt; 미리 정의한 경보 규칙(예: CPU 사용량 90% 이상 시 경보) 설정 파일을 읽어 캐시 메모리에 로드
&lt;strong&gt;2. 경보 관리자의 규칙 참조:&lt;/strong&gt; 경보 관리자(Alert Manager)는 캐시에서 활성화된 경보 규칙들을 지속적으로 스캔
&lt;strong&gt;3. 질의 서비스를 통한 데이터 평가:&lt;/strong&gt; 경보 관리자는 설정된 주기마다 질의 서비스에 요청을 날려 디스크/메모리에 적재된 시계열 메트릭이 임계치를 초과했는지 평가
&lt;strong&gt;4. 상태 기록 및 영속화:&lt;/strong&gt; 경보의 상태 변환(정상 → 경고 → 심각) 및 발송 이력을 경보 저장소에 기록하여 영속화
&lt;strong&gt;5. 메시지 큐 발행:&lt;/strong&gt; 조건이 충족되어 경보가 발생하면, 경보 발생자는 이벤트를 카프카의 경보 전용 토픽으로 즉시 발행함, 후속 알림 레이어와의 비동기 격리를 위한 구조임
&lt;strong&gt;6. 경보 소비자 컨슈밍:&lt;/strong&gt; 경보 소비자 클러스터가 카프카로부터 알림 이벤트 컨슈밍
&lt;strong&gt;7. 멀티채널 라우팅:&lt;/strong&gt; 소비자는 이벤트를 파싱하여 이메일, SMS 등 지정된 타깃 다운스트림 채널로 알림 패킷을 최종 라우팅&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;52-경보-피로alert-fatigue-방지를-위한-이벤트-병합&quot;&gt;5.2. 경보 피로(Alert Fatigue) 방지를 위한 이벤트 병합&lt;/h2&gt;

&lt;p&gt;단일 호스트 내에 장애가 수많은 경보 이벤트를 동시다발적으로 유발할 수 있다.&lt;br /&gt;
예를 들어 특정 인스턴스의 디스크 사용량이 임계치를 넘으면 초 단위로 수십 개의 경보 이벤트가 유입된다.&lt;br /&gt;
이를 필터링 없이 그대로 발송하면 엔지니어가 중요한 알림을 놓치는 &lt;strong&gt;경보 피로(Alert Fatigue)&lt;/strong&gt; 현상이 발생한다.&lt;/p&gt;

&lt;p&gt;이를 제어하기 위해 경보 소비자 또는 관리자 레이어에서 &lt;strong&gt;이벤트 병합&lt;/strong&gt; 메커니즘을 수행한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;식별자 기반 그룹핑&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;동일한 호스트에서 동일한 규칙 원인으로 발생하는 다량의 이벤트를 시간 윈도우 블록 내에서 하나의 고유한 그룹 ID로 바인딩&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;중복 제거 및 요약&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수천 개의 장애 이벤트를 단 하나의 요약된 경보 알림(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;인스턴스 1에서 발생한 1개 경보&lt;/code&gt;)으로 병합 처리하여 알림 채널로 전달함으로써 관측 효율성 극대화&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;53-grafana-기반의-시각화-시스템&quot;&gt;5.3. Grafana 기반의 시각화 시스템&lt;/h2&gt;

&lt;p&gt;시각화 계층은 사내에서 웹 FE를 직접 구현하기보다, 업계 표준으로 자리 잡은 오픈소스 대시보드 솔루션인 Grafana를 도입하는 것이 적합하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-요약&quot;&gt;6. 요약&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;지표 출처 → 지표 수집기&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;안정 해시 링과 서비스 탐색 기술을 결합하여, 인프라의 동적 오토스케일링 환경에서도 수집기 노드 간 중복 없이 천만 개의 지표 트래픽 수집을 Scale-out함&lt;/li&gt;
      &lt;li&gt;환경적 요구 사항에 따라 Pull/Push 하이브리드 수집 모델 적용&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;지표 수집기 → 카프카 → 소비자&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 적재 전면에 분산 메시지 큐를 배치함으로써 대용량 쓰기 스파이크에 대한 Backpressure를 제어하고, 영속성 계층의 가용성 저하 시에도 데이터 유실을 차단하는 버퍼 계층 구현&lt;/li&gt;
      &lt;li&gt;메트릭의 식별자를 기반으로 파티셔닝 전략을 적용하여 처리 성능 최적화&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;TSDB → 캐시 및 질의 서비스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;쓰기 최적화 스토리지 엔진을 채택하여 초당 수십만 건의 적재를 처리&lt;/li&gt;
      &lt;li&gt;백그라운드 다운샘플링 및 이중-델타 인코딩을 가동하여 디스크 비용 절감&lt;/li&gt;
      &lt;li&gt;읽기 연산은 전단의 무상태 질의 서버와 캐시 계층을 거치도록 강제하여 스토리지 엔진의 I/O를 물리적으로 격리&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;다운스트림(경보 및 시각화)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;분리된 질의 인터페이스를 통해 데이터를 안전하게 컨슈밍하며, 비동기 큐 기반의 경보 시스템과 그라파나 대시보드를 연동하여 인프라 가시성과 높은 장애 감지 정밀도 제공&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;지표 데이터 수집 모델: 풀 모델 vs 푸시 모델&lt;/li&gt;
  &lt;li&gt;카프카를 활용한 규모 확장 방안&lt;/li&gt;
  &lt;li&gt;최적 시계열 데이터베이스의 선정&lt;/li&gt;
  &lt;li&gt;다운샘플링을 통한 데이터 크기 절감&lt;/li&gt;
  &lt;li&gt;경보/시각화 시스템: 구현할 것인가 구입할 것인가&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0616/final.png&quot; alt=&quot;최종 설계안&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬, 산 람 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000211656186&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links_vol2.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.splunk.com/&quot;&gt;splunk&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.pagerduty.com/&quot;&gt;PagerDuty&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.elastic.co/elastic-stack&quot;&gt;ELK&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.x.com/engineering/en_us/a/2012/distributed-systems-tracing-with-zipkin&quot;&gt;Distributed Systems Tracing with Zipkin&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://research.google/pubs/dapper-a-large-scale-distributed-systems-tracing-infrastructure/&quot;&gt;Dapper, a Large-Scale Distributed Systems Tracing Infrastructure&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://prometheus.io/docs/introduction/overview/&quot;&gt;Prometheus&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://opentsdb.net/&quot;&gt;OpenTSDB - A Distributed, Scalable Monitoring System&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://prometheus.io/docs/concepts/data_model/&quot;&gt;프로메테우스 데이터 모델&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.cloud.google.com/bigtable/docs/schema-design-time-series?hl=ko&quot;&gt;Bigtable 시계열 데이터의 스키마 설계&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.x.com/engineering/en_us/topics/infrastructure/2019/metricsdb&quot;&gt;MetricsDB: TimeSeries Database for storing metrics at Twitter&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ko/timestream/&quot;&gt;Amazon Timestream&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://db-engines.com/en/ranking/time+series+dbms&quot;&gt;DB-Engines Ranking of Time Series DBMS&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.influxdata.com/&quot;&gt;InfluxDB&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/cloudwatch/&quot;&gt;Amazon CloudWatch&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://graphiteapp.org/&quot;&gt;Graphite&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/prometheus/pushgateway&quot;&gt;프로메테우스의 pushgateway&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ko/lambda/serverless-architectures-learn-more/&quot;&gt;Serverless&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.vldb.org/pvldb/vol8/p1816-teller.pdf&quot;&gt;고릴라(Gorilla)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 16 Jun 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/06/16/architecture-large-scale-metrics-monitoring-alert-system/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/06/16/architecture-large-scale-metrics-monitoring-alert-system/</guid>
        
        <category>architecture</category>
        
        <category>system-design</category>
        
        <category>time-series-database</category>
        
        <category>tsdb</category>
        
        <category>prometheus</category>
        
        <category>influxdb</category>
        
        <category>kafka</category>
        
        <category>대규모시스템설계</category>
        
        <category>모니터링시스템</category>
        
        <category>시계열데이터베이스</category>
        
        <category>카프카</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(2) - 분산 메시지 큐</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#한번-늘어난-consumer는-자동으로-줄어들-수-있을까-auto-scaling-가능-여부&quot;&gt;💡한번 늘어난 Consumer는 자동으로 줄어들 수 있을까? (Auto-scaling 가능 여부)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#1-메시지-큐-vs-이벤트-스트리밍-플랫폼&quot;&gt;1. 메시지 큐 vs 이벤트 스트리밍 플랫폼&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-문제-이해-및-설계-범위-확정&quot;&gt;2. 문제 이해 및 설계 범위 확정&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#카프카에서-멀티미디어도-지원이-가능할까&quot;&gt;💡카프카에서 멀티미디어도 지원이 가능할까?&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#21-기능-요구사항&quot;&gt;2.1. 기능 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-비기능-요구사항&quot;&gt;2.2. 비기능 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-전통적-메시지-큐와-이번-설계의-다른-점&quot;&gt;2.3. 전통적 메시지 큐와 이번 설계의 다른 점&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-개략적-아키텍처&quot;&gt;3. 개략적 아키텍처&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-메시지-모델&quot;&gt;3.1. 메시지 모델&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#311-일대일point-to-point-모델&quot;&gt;3.1.1. 일대일(point-to-point) 모델&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#312-발행-구독publish-subscribe-모델&quot;&gt;3.1.2. 발행-구독(publish-subscribe) 모델&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-토픽-파티션-브로커&quot;&gt;3.2. 토픽, 파티션, 브로커&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-consumer-group&quot;&gt;3.3. Consumer Group&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#34-개략적-설계안&quot;&gt;3.4. 개략적 설계안&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-상세-설계&quot;&gt;4. 상세 설계&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-데이터-저장소&quot;&gt;4.1. 데이터 저장소&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#411-선택지-1-데이터베이스rdbmsnosql&quot;&gt;4.1.1. 선택지 1: 데이터베이스(RDBMS/NoSQL)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#412-선택지-2-쓰기-우선-로그wal-write-ahead-log채택&quot;&gt;4.1.2. 선택지 2: 쓰기 우선 로그(WAL, Write-Ahead Log)(채택)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#lsm-트리와-wal은-같은-개념일까&quot;&gt;💡LSM 트리와 WAL은 같은 개념일까?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#왜-더-큰-규모의-순차-쓰기가-더-높은-디스크-접근-대역폭을-달성할까&quot;&gt;💡왜 더 큰 규모의 순차 쓰기가 더 높은 디스크 접근 대역폭을 달성할까?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#4121-segment-단위-분할&quot;&gt;4.1.2.1. Segment 단위 분할&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#42-메시지-자료-구조와-일괄-처리&quot;&gt;4.2. 메시지 자료 구조와 일괄 처리&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#421-바이너리-계약과-제로-카피&quot;&gt;4.2.1. 바이너리 계약과 제로 카피&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#422-일괄-처리&quot;&gt;4.2.2. 일괄 처리&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#43-producer-측-작업-흐름&quot;&gt;4.3. Producer 측 작업 흐름&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#431-선택지-1-별도의-라우팅-계층-도입&quot;&gt;4.3.1. 선택지 1: 별도의 라우팅 계층 도입&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#432-선택지-2-생산자-내부-버퍼-및-스마트-라우팅채택&quot;&gt;4.3.2. 선택지 2: 생산자 내부 버퍼 및 스마트 라우팅(채택)&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#44-consumer-측-작업-흐름-및-데이터-전달-모델push-vs-pull&quot;&gt;4.4. Consumer 측 작업 흐름 및 데이터 전달 모델(Push vs Pull)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#441-소비자-측-기본-작업-흐름&quot;&gt;4.4.1. 소비자 측 기본 작업 흐름&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#442-push-모델&quot;&gt;4.4.2. Push 모델&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#443-pull-모델채택&quot;&gt;4.4.3. Pull 모델(채택)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#long-polling-이란&quot;&gt;💡&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long Polling&lt;/code&gt; 이란?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#데이터-처리와-오프셋-갱신-순서가-메시지-전송-시맨틱에-미치는-영향은&quot;&gt;💡데이터 처리와 오프셋 갱신 순서가 메시지 전송 시맨틱에 미치는 영향은?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#45-소비자-재조정consumer-rebalancing&quot;&gt;4.5. 소비자 재조정(Consumer rebalancing)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#소비자-목록에-변화가-생기면-코디네이터-브로커는-무조건-새-리더를-선출할까&quot;&gt;💡소비자 목록에 변화가 생기면 코디네이터 브로커는 무조건 ‘새 리더’를 선출할까?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#451-새로운-소비자가-그룹에-합류하는-경우&quot;&gt;4.5.1. 새로운 소비자가 그룹에 합류하는 경우&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#452-기존-소비자가-우아하게-그룹을-떠나는-경우graceful-shutdown&quot;&gt;4.5.2. 기존 소비자가 우아하게 그룹을 떠나는 경우(Graceful Shutdown)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#453-기존-소비자가-비정상적으로-다운된-경우장애-감지&quot;&gt;4.5.3. 기존 소비자가 비정상적으로 다운된 경우(장애 감지)&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#46-상태-저장소-메타데이터-저장소-주키퍼&quot;&gt;4.6. 상태 저장소, 메타데이터 저장소, 주키퍼&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#461-상태-저장소&quot;&gt;4.6.1. 상태 저장소&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#왜-데이터의-일관성-및-높은-읽기쓰기-속도에-요구될-때-주키퍼와-같은-키-값-저장소가-바람직할까&quot;&gt;💡왜 데이터의 일관성 및 높은 읽기/쓰기 속도에 요구될 때 주키퍼와 같은 키-값 저장소가 바람직할까?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#카프카는-왜-오프셋-저장소를-브로커로-이전했을까&quot;&gt;💡카프카는 왜 오프셋 저장소를 브로커로 이전했을까?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#최신-트렌드-kraft&quot;&gt;💡최신 트렌드: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KRaft&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#462-메타데이터-저장소&quot;&gt;4.6.2. 메타데이터 저장소&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#메타데이터-저장소로-주키퍼가-적절한-이유는&quot;&gt;💡메타데이터 저장소로 주키퍼가 적절한 이유는?&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#463-주키퍼&quot;&gt;4.6.3. 주키퍼&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#47-데이터-유실-제로-파티션-사본-동기화isr-전략&quot;&gt;4.7. 데이터 유실 제로: 파티션 사본, 동기화(ISR) 전략&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#471-파티션-사본&quot;&gt;4.7.1. 파티션 사본&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#메시지를-여러-파티션에-두면-같은-메시지가-여러-파티션에-중복되어-저장되는-걸까&quot;&gt;💡메시지를 여러 파티션에 두면 같은 메시지가 여러 파티션에 중복되어 저장되는 걸까?&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#472-사본-동기화isr&quot;&gt;4.7.2. 사본 동기화(ISR)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#deep-dive-isr-목록을-관리하는-high-watermark와-hands-free-복제&quot;&gt;🚀Deep Dive: ISR 목록을 관리하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;High Watermark&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hands-Free 복제&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#48-producer-ackall-0-1-옵션&quot;&gt;4.8. Producer ACK(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;) 옵션&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#481-ackall-또는-ack-1&quot;&gt;4.8.1. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACK=all&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACK=-1&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#482-ack1&quot;&gt;4.8.2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACK=1&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#483-ack0&quot;&gt;4.8.3. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACK=0&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#producer-측의-ack-와-consumer-측의-ack&quot;&gt;💡Producer 측의 ACK 와 Consumer 측의 ACK&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#49-규모-확장성&quot;&gt;4.9. 규모 확장성&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#491-브로커-결함-허용fault-tolerance과-장애-복구&quot;&gt;4.9.1. 브로커 결함 허용(Fault Tolerance)과 장애 복구&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#492-무중단-브로커-규모-확장scale-out&quot;&gt;4.9.2. 무중단 브로커 규모 확장(Scale-out)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#493-파티션-개수-변경과-저장소-계층의-변화&quot;&gt;4.9.3. 파티션 개수 변경과 저장소 계층의 변화&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#494-producer의-확장성&quot;&gt;4.9.4. Producer의 확장성&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#495-consumer의-확장성&quot;&gt;4.9.5. Consumer의 확장성&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#410-메시지-전달-시맨틱과-오프셋-갱신-타이밍&quot;&gt;4.10. 메시지 전달 시맨틱과 오프셋 갱신 타이밍&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#4101-최대-한-번at-most-once&quot;&gt;4.10.1. 최대 한 번(At-most-once)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#4102-최소-한-번at-least-once&quot;&gt;4.10.2. 최소 한 번(At-least-once)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#4103-정확히-한-번exactly-once&quot;&gt;4.10.3. 정확히 한 번(Exactly-once)&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-고급-기능&quot;&gt;5. 고급 기능&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#51-메시지-필터링&quot;&gt;5.1. 메시지 필터링&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#52-메시지의-지연-전송-및-예약-전송&quot;&gt;5.2. 메시지의 지연 전송 및 예약 전송&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-최종-요약&quot;&gt;6. 최종 요약&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;메시지 큐는 Producer와 Consumer 사이에서 메시지를 중재하는 비동기 통신 플랫폼이다.&lt;br /&gt;
이를 도입함으로써 얻을 수 있는 아키텍처적 이점은 크게 4가지로 요약된다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;결합도 완화(Decoupling)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;컴포넌트 간의 강한 의존성이 사라진다.&lt;/li&gt;
      &lt;li&gt;생산자와 소비자가 현재 살아있는지, 어떤 기술 스택을 쓰는지 알 필요 없이 오직 큐에 데이터를 넣기만 하면 된다.&lt;/li&gt;
      &lt;li&gt;따라서 각 시스템을 독립적으로 수정하고 배포할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;규모 확장성 개선&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;트래픽 부하에 맞춰 생산자와 소비자의 규모를 완전히 독립적으로 확장할 수 있다.&lt;/li&gt;
      &lt;li&gt;예) 이벤트 기간에 주문 트래픽이 몰리면 생산자 서버를 늘리고, 큐에 쌓인 메시지를 처리하는 소비자 서버만 유연하게 늘려 대응 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;가용성 개선&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Consumer 중 하나에 일시적인 장애가 발생하여 다운되어도 메시지는 안전하게 큐에 보관된다.&lt;/li&gt;
      &lt;li&gt;소비자 시스템이 복구되는 즉시 중단된 시점부터 다시 메시지를 읽어와 처리할 수 있으므로 전체 시스템의 가동률이 극대화된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;성능 개선&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메시지 큐는 비동기 통신을 보장한다.&lt;/li&gt;
      &lt;li&gt;Producer는 응답을 기다리며 Blocking 되지 않고, 메시지를 큐에 던진 후 즉시 다음 작업을 수행한다.&lt;/li&gt;
      &lt;li&gt;Consumer 역시 자신이 처리할 수 있는 속도에 맞춰 메시지를 가져가므로 시스템 전체의 처리량이 향상된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;한번-늘어난-consumer는-자동으로-줄어들-수-있을까-auto-scaling-가능-여부&quot;&gt;💡한번 늘어난 Consumer는 자동으로 줄어들 수 있을까? (Auto-scaling 가능 여부)&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;가능하다.&lt;/strong&gt;&lt;br /&gt;
Kubernetes에서는 &lt;strong&gt;KEDA(Kubernetes-based Event-driven Autoscaler)&lt;/strong&gt;와 같은 도구를 활용하여 메시지 큐의 &lt;strong&gt;Consumer Lag&lt;/strong&gt;(쌓여 있는 메시지의 양)을 모니터링 한다.&lt;br /&gt;
처리해야 할 잔여 메시지가 임계치 이하로 떨어지면 컨슈머 Pod를 자동으로 축소하며, 이 때 시스템은 자연스럽게 ‘소비자 재조정(Rebalancing)’을 일으켜 자원을 최적화한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-메시지-큐-vs-이벤트-스트리밍-플랫폼&quot;&gt;1. 메시지 큐 vs 이벤트 스트리밍 플랫폼&lt;/h1&gt;

&lt;p&gt;시중에는 아파치 카프카, 아파치 RabbitMQ, 아파치 Pulsar, 아파치 RocketMQ, 아파치 ActiveMQ 등 수많은 메시지 브로커가 존재한다.&lt;br /&gt;
흔히 혼용하여 부르지만 기술적으로는 &lt;strong&gt;전통적인 메시지 큐&lt;/strong&gt;와 &lt;strong&gt;이벤트 스트리밍 플랫폼&lt;/strong&gt;으로 명확히 구분된다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;전통적 메시지 큐(예: RabbitMQ, ActiveMQ)&lt;/th&gt;
      &lt;th&gt;이벤트 스트리밍 플랫폼(예: Kafka, Pulsar)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;메시지 보관성&lt;/td&gt;
      &lt;td&gt;소비자가 메시지를 수신하고 확인 응답(ACK)을 보내면 &lt;strong&gt;메시지를 즉시 파괴&lt;/strong&gt;함(소멸성)&lt;/td&gt;
      &lt;td&gt;메시지를 소비하더라도 보관 기간 동안 &lt;strong&gt;디스크에 영구히 유지&lt;/strong&gt;함(지속성)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;소비 패턴&lt;/td&gt;
      &lt;td&gt;단 한 명의 소비자만 해당 메시지를 가져갈 수 있는 구조가 기본&lt;/td&gt;
      &lt;td&gt;동일한 데이터를 여러 Consumer Group이 &lt;strong&gt;몇 번이고 반복해서&lt;/strong&gt; 읽을 수 있음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;데이터 순서&lt;/td&gt;
      &lt;td&gt;메시지의 소비 순서를 엄격하게 보장하기 어려움&lt;/td&gt;
      &lt;td&gt;동일한 파티션 내에서는 메시지가 들어온 &lt;strong&gt;순서를 완벽히 보장&lt;/strong&gt;함&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;최근에는 RabbitMQ에 스트리밍 기능이 추가되는 등 두 기술의 경계가 희미해지고 있지만, 대규모 로그 수집이나 실시간 스트림 처리를 위해서는 데이터 장기 보관 및 반복 소비가 
가능한 시스템이 필수적이다.&lt;br /&gt;
여기서는 이러한 &lt;strong&gt;이벤트 스트리밍 플랫폼의 강점을 지닌 분산 메시지 큐&lt;/strong&gt;를 다룬다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-문제-이해-및-설계-범위-확정&quot;&gt;2. 문제 이해 및 설계 범위 확정&lt;/h1&gt;

&lt;p&gt;대규모 트래픽과 지속성을 모두 만족하는 분산 메시지 큐를 설계하기 위한 구체적인 요구사항을 정의한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;메시지 형태 및 크기&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 메시지의 형태와 평균 크기는 어느 정도인가? 텍스트만 지원해야 하는가, 멀티미디어도 지원해야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 텍스트 형태의 메시지만 지원하며, 메시지 크기는 수 KB 수준이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;메시지 반복 소비 여부&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 메시지는 반복 소비가 가능해야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 그렇다. 하나의 메시지를 여러 Consumer Group이 반복해서 수신할 수 있어야 한다.
      한 소비자가 받아가면 대기열에서 즉시 지워버리는 전통적인 분산 메시지 큐와 달리, 본 시스템은 보관 기한 동안 메시지를 유지하여 멀티캐스팅을 지원한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;메시지 소비 순서 보장&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 메시지는 큐에 전달된 순서대로 소비되어야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 그렇다. 동일한 파티션 내에서는 메시지가 생성된 순서(FIFO)대로 소비자에게 전달되어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 지속성 및 보관 기간&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 데이터의 지속성이 보장되어야 하는가? 그렇다면 기간은 어느 정도이어야 하는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 그렇다. 하드웨어 장애에 대응하기 위해 디스크 기반의 지속성을 보장해야 하며, 메시지의 기본 보관 기간은 &lt;strong&gt;2주&lt;/strong&gt;로 설정한다.&lt;br /&gt;
보관 기간이 만료되거나 용량을 초과한 오래된 이력 데이터는 자동으로 삭제된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;서비스 이용 규모&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 지원해야 하는 Producer와 Consumer 수는 어느 정도인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 많으면 많을수록 좋다. 트래픽 급증 시 브로커 노드나 파티션을 추가하여 선형적으로 확장 가능한 분산 클러스터 구조이어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;메시지 전달 방식(Delivery Semantics)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 어떤 메시지 전달 방식을 지원해야 하는가? 최대 한 번(at-most-once), 최소 한 번(at-least-once), 정확히 한 번(exactly once) 중 무엇인가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 비즈니스 도메인의 특성에 맞춰 세 가지 방식 모두를 사용자가 설정을 통해 선택할 수 있어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;목표 성능(대역폭 및 지연 시간)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Q:&lt;/strong&gt; 목표로 해야 할 대역폭과 end-to-end 지연 시간은 어떻게 되는가?&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;A:&lt;/strong&gt; 대규모 로그 수집 시스템 등으로도 활용할 수 있어야 하므로 높은 수준의 대역폭을 제공해야 하며, 동시에 실시간 이벤트 처리를 위한 낮은 전송 지연도 필수적이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;카프카에서-멀티미디어도-지원이-가능할까&quot;&gt;💡카프카에서 멀티미디어도 지원이 가능할까?&lt;/h2&gt;

&lt;p&gt;일반적인 카프카도 멀티미디어(이미지, 동영상 등) 바이너리 데이터를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;byte[]&lt;/code&gt; 형태로 담아 전송하는 것 자체는 가능하다.&lt;br /&gt;
하지만 대용량 파일이 브로커로 직접 유입되면 메모리 캐시 효율이 급격히 떨어지고 디스크 I/O 병목이 발생한다.&lt;/p&gt;

&lt;p&gt;따라서 실무에서는 대용량 멀티미디어 파일을 S3와 같은 오브젝트 스토리지에 업로드하고, 메시지 큐에는 ‘파일의 저장 경로’만 메타데이터로 담아 
전달하는 &lt;strong&gt;클레임 체크(Claim-Check) 패턴&lt;/strong&gt;을 사용하는 것이 표준이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-기능-요구사항&quot;&gt;2.1. 기능 요구사항&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;생산자는 메시지 큐에 메시지를 발행할 수 있어야 한다.&lt;/li&gt;
  &lt;li&gt;소비자는 메시지 큐로부터 메시지를 수신(Pull)할 수 있어야 한다.&lt;/li&gt;
  &lt;li&gt;메시지는 설정에 따라 &lt;strong&gt;반복적으로 수신&lt;/strong&gt;할 수도 있고, 전통적인 방식처럼 단 한 번만 수신되도록 설정할 수도 있어야 한다.&lt;/li&gt;
  &lt;li&gt;보관기한이 만료되거나 용량을 초과한 오래된 이력 데이터는 자동으로 삭제된다.&lt;/li&gt;
  &lt;li&gt;메시지의 형태는 수 KB 수준의 일반적인 텍스트 데이터를 지원한다.&lt;/li&gt;
  &lt;li&gt;동일 파티션 내에서는 생산된 순서대로 소비자에게 전달되어야 한다.&lt;/li&gt;
  &lt;li&gt;사용자 설정에 따라 세 가지 메시지 전달 방식(최대 한 번, 최소 한 번, 정확히 한 번)을 모두 지원해야 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-비기능-요구사항&quot;&gt;2.2. 비기능 요구사항&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;높은 대역폭(Throughput) 혹은 낮은 전송 지연(Latency)&lt;/strong&gt; 중 비즈니스 특성에 맞게 시스템 옵션을 튜닝할 수 있어야 한다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;규모 확장성:&lt;/strong&gt; 트래픽이 급증하더라도 브로커 노드나 파티션을 추가하여 선형적으로 성능을 확장할 수 있는 분산 구조이어야 한다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;지속성 및 내구성(Persistence &amp;amp; Durability)&lt;/strong&gt;: 하드웨어 장애로 노드가 죽더라도 데이터가 유실되지 않도록 디스크 기반 저장소를 활용하고, 여러 브로커에 데이터를 복제해야 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-전통적-메시지-큐와-이번-설계의-다른-점&quot;&gt;2.3. 전통적 메시지 큐와 이번 설계의 다른 점&lt;/h2&gt;

&lt;p&gt;여기서 설계할 분산 메시지 큐의 차별점은 결국 &lt;strong&gt;데이터의 수명 관리와 순서 보장&lt;/strong&gt;에 있다.&lt;/p&gt;

&lt;p&gt;전통적 큐는 메모리 효율성을 위해 대기열의 메시지를 처리하는 즉시 지워버리며 &lt;a href=&quot;https://www.rabbitmq.com/docs/maxlength&quot;&gt;성능 임계치를 넘을 때만 디스크를 임시 버퍼&lt;/a&gt;로 사용한다.&lt;/p&gt;

&lt;p&gt;반면, 여기서 다룰 대규모 시스템용 분산 큐는 모든 이벤트를 디스크에 순차적 로그 형태로 기록하여 최대 2주간 안전하게 보관(Retention)한다.&lt;br /&gt;
이 지속성 때문에 소비자의 유연한 데이터 재생(Replay)이 가능해지며, 대규모 장애 상황에서도 데이터 유실 없는 완벽한 내결함성을 갖추게 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-개략적-아키텍처&quot;&gt;3. 개략적 아키텍처&lt;/h1&gt;

&lt;p&gt;요구사항을 명확히 했으니, 이제 분산 메시지 큐의 개략적인 구조에 대해 설계해보자.&lt;br /&gt;
대규모 시스템에서 메시지를 어떻게 분류하고, 저장하고, 분산 처리하는지 핵심 메커니즘에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-메시지-모델&quot;&gt;3.1. 메시지 모델&lt;/h2&gt;

&lt;p&gt;메시징 시스템은 데이터를 소비자에게 전달하는 방식에 따라 크게 두 가지 모델로 나뉜다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;311-일대일point-to-point-모델&quot;&gt;3.1.1. 일대일(point-to-point) 모델&lt;/h3&gt;

&lt;p&gt;전통적인 메시지 큐에서 흔히 발견되는 구조이다.&lt;br /&gt;
큐에 전송된 메시지는 오직 한 명의 소비자만 가져갈 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/point_to_point.png&quot; alt=&quot;일대일 모델&quot; /&gt;&lt;/p&gt;

&lt;p&gt;어떤 소비자가 메시지를 성공적으로 가져갔다고 큐에 확인 응답(ACK)을 보내면, 해당 메시지는 큐에서 즉시 삭제된다.&lt;br /&gt;
데이터 보관(Retention) 개념이 없기 때문에, 동일한 데이터를 다른 시스템에서 재소비할 수 없다.&lt;/p&gt;

&lt;p&gt;따라서 여기서 목표로 하는 ‘2주간 보관 및 반복 소비’ 요건에는 적합하지 않다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;312-발행-구독publish-subscribe-모델&quot;&gt;3.1.2. 발행-구독(publish-subscribe) 모델&lt;/h3&gt;

&lt;p&gt;본 설계의 기반이 되는 모델이다.&lt;br /&gt;
메시지를 주제별로 정리하는 토픽(Topic)이라는 개념을 사용하며, Producer는 특정 토픽에 메시지를 발행하고, Consumer는 해당 토픽을 구독하여 데이터를 가져온다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/pub_sub.png&quot; alt=&quot;발행-구독 모델&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이 모델에서는 토픽에 전달된 메시지가 해당 토픽을 구독하는 &lt;strong&gt;모든 Consumer Group에게 복사되어 전달&lt;/strong&gt;된다.&lt;br /&gt;
덕분에 하나의 이벤트를 결제 시스템, 알림 시스템 등 여러 곳에서 동시에 받아볼 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-토픽-파티션-브로커&quot;&gt;3.2. 토픽, 파티션, 브로커&lt;/h2&gt;

&lt;p&gt;발행-구독 모델은 훌륭하지만, 거대한 트래픽이 하나의 토픽으로 몰리면 서버 한 대의 디스크나 네트워크 대역폭으로는 감당할 수 없는 병목이 생긴다.&lt;br /&gt;
이 문제를 해결하는 분산 시스템의 핵심이 바로 &lt;strong&gt;파티션&lt;/strong&gt;, 즉 &lt;strong&gt;데이터 샤딩 기법&lt;/strong&gt;이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;파티션 분할&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하나의 토픽을 여러 개의 파티션으로 쪼개어 메시지를 분산 저장&lt;/li&gt;
      &lt;li&gt;각 파티션은 독립적인 FIFO 큐처럼 동작&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;브로커(Broker)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;파티션을 유지하고 관리하는 메시지 큐 클러스터 내의 개별 서버&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;파티션들을 여러 브로커에 고르게 분산 배치하는 것이 성능 선형 확장의 비결&lt;/strong&gt;임&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;오프셋(Offset)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;파티션 내에서 메시지의 위치를 나타내는 순차적인 고유 번호&lt;/li&gt;
      &lt;li&gt;파티션 내부에서는 이 오프셋 덕분에 메시지의 순서가 완벽히 보장됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/partition.png&quot; alt=&quot;파티션&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Producer가 메시지를 보낼 때는 메시지에 Key를 붙일 수 있다.&lt;br /&gt;
같은 Key를 가진 메시지는 항상 동일한 파티션으로 전송되어 완벽한 순서 보장을 받게 되며, Key가 없는 메시지는 라운드 로빈이나 무작위 방식으로 파티션에 균등하게 분산된다.&lt;/p&gt;

&lt;p&gt;아래는 브로커와 파티션을 갖춘 메시지 큐 클러스터이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/queue.png&quot; alt=&quot;메시지 큐 클러스터&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-consumer-group&quot;&gt;3.3. Consumer Group&lt;/h2&gt;

&lt;p&gt;토픽을 구독하는 Consumer 가 여럿일 때, 이들을 효율적으로 관리하기 위해 Consumer Group이라는 개념을 도입한다.&lt;br /&gt;
하나의 Consumer Group은 독립된 하나의 비즈니스 서비스(예: 배송 시스템 전용 그룹)를 대변한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/consumer_group.png&quot; alt=&quot;소비자 그룹&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림처럼 Consumer Group 내의 컴포넌트들은 토픽의 파티션들을 나누어 맡아 메시지를 병렬로 읽어온다.&lt;br /&gt;
대역폭 측면에서 대단히 유리하다. 하지만 여기서 아주 중요한 아키텍처적 딜레마가 발생한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️Q: 데이터를 병렬로 읽으면 처리량은 늘어나지만, 만일 소비자-1과 소비자-2가 같은 파티션-1의 메시지를 동시에 무작위로 읽어간다면 어떻게 될까?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A:&lt;/strong&gt; 파티션 내에서의 메시지 소비 순서가 완전히 깨지게 된다. 1번 주문 이벤트보다 2번 결제 완료 이벤트가 먼저 처리되는 대참사가 발생할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;해결책&lt;/strong&gt;: 시스템의 엄격한 순서 보장을 위해 제약 조건을 추가한다.&lt;br /&gt;
&lt;strong&gt;‘어떤 파티션의 메시지는 하나의 Consumer Group 내에서 오직 단 한 명의 Consumer만 읽을 수 있다’&lt;/strong&gt;&lt;br /&gt;
이 제약 조건 때문에 Consumer Group 내의 Consumer 수가 구독하는 토픽의 파티션 수보다 많아지면, 몇몇 Consumer는 할당받을 파티션이 없어 Idle 상태가 된다.&lt;br /&gt;
위 그림에서 소비자 그룹-2의 소비자-3이 토픽-B의 메시지를 수신하지 못하는 이유가 바로 이 때문이다.&lt;br /&gt;
이미 소비자-4가 해당 파티션을 점유하고 있기 때문이다.&lt;br /&gt;
따라서 병렬 처리량을 늘리려면 Consumer만 늘릴 게 아니라 &lt;strong&gt;토픽의 파티션 수도 함께 늘려주어야 한다.&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;34-개략적-설계안&quot;&gt;3.4. 개략적 설계안&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/2_final.png&quot; alt=&quot;개략적 설계안&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;클라이언트 레이어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Producer:&lt;/strong&gt; 메시지를 생성하여 특정 토픽의 파티션으로 발행&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Consumer:&lt;/strong&gt; Consumer Group을 형성하여 토픽을 구독하고 오프셋을 기반으로 메시지를 당겨와 처리&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;코어 브로커 및 저장소 레이어&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Broker:&lt;/strong&gt; 파티션을 나누어 저장하고 디스크 기반의 지속성 제공&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;데이터 저장소:&lt;/strong&gt; 파티션 실데이터(메시지)가 물리적으로 기록되는 공간&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;상태 저장소:&lt;/strong&gt; Consumer Group별로 각 파티션에서 어디까지 읽었는지 나타내는 &lt;strong&gt;오프셋 상태 정보&lt;/strong&gt;를 관리&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;메타데이터 저장소:&lt;/strong&gt; 토픽명, 파티션 수, Replica 배치 계획 등 클러스터의 전반적인 설정 정보 보관&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;조정 서비스(Coordination Service)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;서비스 탐색 및 Health check:&lt;/strong&gt; 어떤 브로커가 살아있고 죽었는지 모니터링&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;컨트롤러 선출:&lt;/strong&gt; 브로커 중 하나를 브로커 클러스터의 컨트롤러로 선출하며, 이 컨트롤러가 파티션 배치와 관리를 총괄
        &lt;ul&gt;
          &lt;li&gt;전통적으로 아파치 주키퍼나 etcd같은 도구가 이 역할을 담당함&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-상세-설계&quot;&gt;4. 상세 설계&lt;/h1&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;대규모 대역폭 달성을 위한 3가지 아키텍처 결정&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;OS 페이지 캐시와 순차 디스크 I/O 극대화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;회전 디스크(Rotational Disk)의 높은 순차 탐색 성능과 OS가 제공하는 적극적 디스크 캐시 전략(Aggressive Disk Caching Strategy)을 잘 이용하는 
디스크 기반 자료 구조(On-disk Data Structure)를 활용&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;수정 없는 전송을 위한 메시지 구조 설계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메시지가 생산자로부터 소비자에게 전달되는 순간까지 아무런 수정 없이도 전송이 가능하도록 메시지 자료 구조 설계&lt;/li&gt;
      &lt;li&gt;전송 데이터 양이 많은 경우 메모리 상에서 메시지를 복사하는데 드는 비용을 최소화하기 위함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;일괄 처리 우선 시스템 설계&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;소규모 I/O가 많아지면 시스템은 컨텍스트 스위칭과 네트워크 오버헤드로 인해 높은 대역폭을 지원하기 어려움&lt;/li&gt;
      &lt;li&gt;따라서 생산자는 메시지를 일괄 전송하고, 큐는 이를 더 큰 단위로 묶어 보관하며, 소비자도 일괄 수신하도록 시스템 전반에서 일괄 처리를 장려&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-데이터-저장소&quot;&gt;4.1. 데이터 저장소&lt;/h2&gt;

&lt;p&gt;첫 번째 결정인 ‘디스크 기반 자료 구조 활용’을 위해, 분산 메시지 큐의 트래픽 패턴을 먼저 분석한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;읽기/쓰기 연산:&lt;/strong&gt; 대규모로 빈번하게 발생&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;갱신/삭제 연산:&lt;/strong&gt; 기본적으로 전혀 발생하지 않음(보관 주기가 지난 데이터만 통째로 날림)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;접근 패턴:&lt;/strong&gt; 오프셋을 따라 순차적으로 읽고 맨 뒤에 추가하는 패턴이 대부분&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;411-선택지-1-데이터베이스rdbmsnosql&quot;&gt;4.1.1. 선택지 1: 데이터베이스(RDBMS/NoSQL)&lt;/h3&gt;

&lt;p&gt;토픽별로 테이블을 만들고 메시지가 올 때마다 레코드로 추가하는 방식이다.&lt;br /&gt;
읽기와 쓰기가 동시에 몰아치면 인덱스(B+ Tree)를 갱신하고 페이지를 분할하는 과정에서 엄청난 메모리 오버헤드와 무작위 디스크 I/O가 발생한다.&lt;br /&gt;
대규모 스트리밍 트래픽을 받기에는 인프라 비용과 오버헤드가 너무 크다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;412-선택지-2-쓰기-우선-로그wal-write-ahead-log채택&quot;&gt;4.1.2. 선택지 2: 쓰기 우선 로그(WAL, Write-Ahead Log)(채택)&lt;/h3&gt;

&lt;p&gt;WAL은 새로운 항목이 오직 맨 뒤에 추가되기만 하는(Append-only) 일반 파일이다.&lt;br /&gt;
수정이 없고 뒤에 붙이기만 하므로 접근 패턴이 100% 순차적이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;lsm-트리와-wal은-같은-개념일까&quot;&gt;💡LSM 트리와 WAL은 같은 개념일까?&lt;/h4&gt;

&lt;p&gt;종종 대규모 쓰기 성능을 위해 언급되는 &lt;a href=&quot;https://assu10.github.io/dev/2026/06/05/architecture-nearby/#242-%EC%9C%84%EC%B9%98-%EC%9D%B4%EB%8F%99-%EC%9D%B4%EB%A0%A5-dbcassandra&quot;&gt;&lt;strong&gt;LSM 트리(Log-Structured Merge-tree)&lt;/strong&gt;&lt;/a&gt;와 
&lt;strong&gt;WAL&lt;/strong&gt;을 같은 것으로 오해하곤 한다.&lt;br /&gt;
결론부터 말하면 &lt;strong&gt;WAL은 LSM 트리의 구성 요소 중 하나일 뿐, 완전히 다른 개념&lt;/strong&gt;이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;WAL&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 유실을 막기 위해 디스크에 순차적으로 기록하는 ‘가장 단순한 형태의 로그 파일’&lt;/li&gt;
      &lt;li&gt;별도의 인덱스가 없어 특정 키를 통한 무작위 조회가 불가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;LSM 트리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;WAL에 먼저 쓴 뒤 메모리 버퍼(MemTable)에 적재하고, 이를 디스크에 정렬된 파일(SSTable)로 내려보내어 주기적으로 병합(Compaction)하는 &lt;strong&gt;고급 키-값 데이터 구조&lt;/strong&gt;이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;카프카 같은 분산 큐는 특정 키의 무작위 조회가 필요 없고 오직 오프셋 번호로만 순차 리딩을 한다.&lt;br /&gt;
따라서 굳이 복잡한 LSM 트리를 쓸 필요 없이, &lt;strong&gt;가장 가볍고 가공하기 쉬운 WAL 방식(세그먼트 로그 파일)을 채택&lt;/strong&gt;한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;왜-더-큰-규모의-순차-쓰기가-더-높은-디스크-접근-대역폭을-달성할까&quot;&gt;💡왜 더 큰 규모의 순차 쓰기가 더 높은 디스크 접근 대역폭을 달성할까?&lt;/h4&gt;

&lt;p&gt;HDD든 SSD든 디스크 헤더가 물리적으로 이동하거나 메모리 블록을 지우고 쓰는 과정에서 무작위 접근(Random Access)은 엄청난 병목을 만든다.&lt;br /&gt;
반면, 대량의 메시지를 묶어서 순차 접근(Sequential Access)을 하면 디스크 탐색 시간이 사실상 0에 수렴한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://deliveryimages.acm.org/10.1145/1570000/1563874/jacobs3.jpg&quot; alt=&quot;Comparing Random and Sequential Access in Disk and Memory&quot; /&gt;&lt;/p&gt;

&lt;p&gt;OS는 디스크 데이터를 페이지 캐시에 아주 적극적으로 밀어 넣는데, 순차 쓰기 연산의 규모가 클수록 OS는 디스크 캐시에서 더 큰 규모의 연속된 공간을 한 번에 점유할 수 있게 된다.&lt;br /&gt;
즉, 자잘한 I/O 요청이 여러 번 발생하는 것보다 코어 브로커가 큰 덩어리의 순차 I/O를 일으키는 것이 디스크 접근 대역폭을 물리적 한계치까지 끌어올리는 비결이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;4121-segment-단위-분할&quot;&gt;4.1.2.1. Segment 단위 분할&lt;/h4&gt;

&lt;p&gt;하나의 파일이 무한정 커질 수는 없으므로, 시스템은 파티션을 &lt;strong&gt;세그먼트&lt;/strong&gt; 단위의 작은 파일로 쪼개어 관리한다.&lt;/p&gt;

&lt;p&gt;새로운 메시지는 오직 현재 활성화된 &lt;strong&gt;‘활성 상태(Active)의 세그먼트 파일’&lt;/strong&gt; 맨 뒤에만 추가되며, 파일 크기가 한계(예: 1GB)에 도달하면 새로운 활성 세그먼트가 개설된다.&lt;/p&gt;

&lt;p&gt;보관 기한(2주)이 지난 낡은 비활성 세그먼트는 파일 내부의 레코드를 일일이 지우는 대신, OS 레벨에서 파일 자체를 디스크에서 통째로 삭제하므로 쓰기 성능에 미치는 영향이 전무하다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/new_message.png&quot; alt=&quot;세그먼트에 새로운 메시지 추가&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/segment.png&quot; alt=&quot;토픽 파티션에 분산된 데이터 세그먼트 파일&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;42-메시지-자료-구조와-일괄-처리&quot;&gt;4.2. 메시지 자료 구조와 일괄 처리&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;제로 카피:&lt;/strong&gt; CPU의 &lt;strong&gt;데이터 복사 오버헤드&lt;/strong&gt;를 제거&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;일괄 처리:&lt;/strong&gt; CPU의 &lt;strong&gt;시스템 콜(컨텍스트 스위칭) 오버헤드&lt;/strong&gt;를 제거&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Producer가 뭉쳐놓은 대용량 메시지 번들을 브로커가 단 1번의 시스템 콜로 Consumer에게 통째로 보내기 때문에(제로 카피), 대용량 로그 수집도 가볍게 버티는 초고속 대역폭이 완성된다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;필드 이름&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;데이터 자료형&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;설명&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;crc&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;integer&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;데이터의 무결성을 검증하기 위한 순환 중복 검사 값&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;size&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;integer&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;메시지의 전체 크기&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;timestamp&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;long&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;메시지가 생성된 타임스탬프&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;attributes&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;byte&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;압축 코덱 정보 등 메시지 속성 플래그&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;key&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;byte[]&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;파티션을 결정하는 해시 키&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;value&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;byte[]&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;실제 비즈니스 데이터 페이로드 (Payload)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;offset&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;long&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;파티션 내 메시지 고유 위치 (브로커가 할당)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;421-바이너리-계약과-제로-카피&quot;&gt;4.2.1. 바이너리 계약과 제로 카피&lt;/h3&gt;

&lt;p&gt;두 번째 결정인 ‘메시지 복사 비용 최소화’ 를 구현하는 방법이다.&lt;/p&gt;

&lt;p&gt;Producer가 정의한 위 스키마 형식을 브로커가 아무런 수정 없이 그대로 파일에 쓰고 Consumer에게 전달하면 &lt;a href=&quot;https://assu10.github.io/dev/2024/07/13/kafka-mechanism-1/#42-%EC%9D%BD%EA%B8%B0-%EC%9A%94%EC%B2%AD&quot;&gt;제로 카피(Zero-Copy)&lt;/a&gt;를 
구현할 수 있다.&lt;/p&gt;

&lt;p&gt;일반적으로 디스크의 파일을 네트워크로 보낼 때는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;디스크 → 커널 버퍼 → 유저 버퍼 → 소켓 버퍼 → 네트워크 카드&lt;/code&gt;라는 많은 복사를 거친다.&lt;br /&gt;
하지만 데이터의 변경이 전혀 없다면, 리눅스의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sendfile&lt;/code&gt; 시스템 콜을 이용해 유저 공간을 거치지 않고 &lt;strong&gt;OS 페이지 캐시에서 네트워크 카드 버퍼로 데이터를 다이렉트로 전송&lt;/strong&gt;할 수 있다.&lt;br /&gt;
이 기법 덕분에 대용량 데이터 전송 시 브로커 CPU 오버헤드가 극적으로 줄어든다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;422-일괄-처리&quot;&gt;4.2.2. 일괄 처리&lt;/h3&gt;

&lt;p&gt;세 번째 결정인 ‘일괄 처리 우선 설계’를 구현하는 방법이다.&lt;/p&gt;

&lt;p&gt;제로 카피가 데이터 복사 비용을 줄여준다면, 일괄 처리는 시스템 콜과 네트워크 왕복 횟수를 줄여주는 기술이다.&lt;/p&gt;

&lt;p&gt;만일 메시지를 건당 하나씩 전송한다면, 제로 카피를 쓰더라도 메시지 1만 개를 보낼 때 1만 번의 콜(컨텍스트 스위칭)과 네트워크 Lag이 발생하여 CPU 부하가 크다.&lt;br /&gt;
또한 디스크에도 자잘한 쓰기 연산이 흩어져 발생하므로 I/O 효율이 극도로 떨어진다.&lt;/p&gt;

&lt;p&gt;이를 극복하기 위해 Producer 단계부터 메시지를 묶어 하나의 거대한 바이너리 배치를 만든다.&lt;br /&gt;
브로커는 이 덩어리를 디스크에 한 번에 순차 쓰기를 하므로 &lt;strong&gt;연속된 공간을 점유하여 디스크 접근 대역폭이 비약적으로 상승&lt;/strong&gt;한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;43-producer-측-작업-흐름&quot;&gt;4.3. Producer 측 작업 흐름&lt;/h2&gt;

&lt;p&gt;Producer가 특정 파티션에 메시지를 보내고자 할 때, 어느 브로커 노드가 해당 파티션의 리더인지 알고 찾아가야 한다.&lt;br /&gt;
이를 처리하는 두 가지 설계안이 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;431-선택지-1-별도의-라우팅-계층-도입&quot;&gt;4.3.1. 선택지 1: 별도의 라우팅 계층 도입&lt;/h3&gt;

&lt;p&gt;가장 직관적인 방법은 생산자와 브로커 클러스터 사이에 라우팅 프록시 서버를 두는 것이다.&lt;/p&gt;

&lt;p&gt;생산자는 아무 생각 없이 라우팅 계층에 메시지를 던지면, 라우팅 계층이 메타데이터 저장소에서 사본 분산 계획을 읽어와 적절한 브로커(파티션 리더)에게 메시지를 전달한다.&lt;/p&gt;

&lt;p&gt;아래 그림에서 생산자는 토픽 A 의 파티션-1로 메시지를 보내고자 한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/routing.png&quot; alt=&quot;라우팅 계층&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;메시지 발송&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;생산자는 메시지를 특정 브로커가 아닌 &lt;strong&gt;라우팅 계층&lt;/strong&gt;으로 보냄&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;메타데이터 캐싱&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;라우팅 계층은 메타데이터 저장소에서 &lt;strong&gt;사본 분산 계획(어떤 파티션의 사본들이 어느 브로커에 분산 배치되어 있는지에 대한 정보)&lt;/strong&gt;를 읽어와 자기 캐시에 보관&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;리더 사본으로 라우팅&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메시지가 도착하면 라우팅 계층은 캐시된 메타데이터를 기반으로 해당 파티션의 리더 사본(Leader Replica)이 있는 브로커로 메시지를 전송&lt;/li&gt;
      &lt;li&gt;예) 토픽-A 파티션-1의 리더가 브로커-1에 있다면 브로커-1로 포워딩&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;사본 동기화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;리더 사본이 메시지를 받으면, 해당 리더를 따르는 단순 사본(Follower)들이 리더 브로커로부터 새 메시지를 지속적으로 읽어가며 데이터를 동기화&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 커밋 및 ACK&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;‘충분한’ 수의 사본이 동기화되면 리더는 데이터를 디스크에 최종 기록(Commit)&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;데이터가 소비자에게 노출되어 소비 가능한 시점이 되는 바로 이 합의(Commit) 시점&lt;/strong&gt;임&lt;/li&gt;
      &lt;li&gt;기록이 끝나면 리더는 생산자에게 수신 확인 응답(ACK)를 회신&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;하지만 이 구조는 &lt;strong&gt;추가적인 네트워크 홉이 발생&lt;/strong&gt;하므로 전송 지연 시간이 늘어나고, 대규모 일괄 처리를 구현하기 어렵다는 한계가 존재한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;장애 감내(Fault Tolerance)를 위한 사본 메커니즘&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;여기서 메시지를 받는 브로커는 &lt;strong&gt;리더 사본&lt;/strong&gt;이다.&lt;br /&gt;
특정 브로커가 죽어도 데이터 유실을 막기 위해 이 복제본 간의 동기화 원리는 &lt;a href=&quot;#472-사본-동기화isr&quot;&gt;4.7. 사본 동기화(ISR 전략)&lt;/a&gt;에서 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;432-선택지-2-생산자-내부-버퍼-및-스마트-라우팅채택&quot;&gt;4.3.2. 선택지 2: 생산자 내부 버퍼 및 스마트 라우팅(채택)&lt;/h3&gt;

&lt;p&gt;네트워크 전송 지연을 줄이고 대역폭을 극대화하기 위해, 선택지 1에서 가상으로 두었던 라우터 계층의 역할을 &lt;strong&gt;Producer 클라이언트 라이브러리 내부로 완전히 편입&lt;/strong&gt;시키고 
&lt;strong&gt;메모리 버퍼&lt;/strong&gt;를 도입한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/buffer.png&quot; alt=&quot;생산자 측 버퍼 및 라우팅 계층&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;스마트 클라이언트&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;생산자 자체가 브로커 클러스터와 통신하여 메타데이터(사본 분산 계획)를 직접 읽어와 로컬에 캐싱&lt;/li&gt;
      &lt;li&gt;즉, 생산자가 스스로 어떤 브로커가 리더인지 알고 직접 다이렉트로 접속하므로 라우팅 프록시 서버를 거칠 필요가 없어 전송 지연이 줄어듦&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;클라이언트 측 메모리 버퍼링&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;생산자는 메시지를 발행할 때 브로커로 즉시 네트워크 전송을 보내지 않고, 내부 메모리 버퍼에 파티션별로 메시지를 차곡차곡 모아둠&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;일괄 전송&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;버퍼에 설정된 임계치만큼 데이터가 쌓이면, 대량의 메시지를 하나의 큰 바이너리 배치 단위로 묶어 리더 브로커로 한 번에 전송&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;버퍼를 도입하면 메시지 묶음이 한 번에 전송되므로 브로커 디스크에 더 큰 규모의 순차 쓰기가 발생하고 디스크 대역폭을 극한까지 끌어올릴 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/batch.png&quot; alt=&quot;일괄 처리 메시지 양 선택&quot; /&gt;&lt;/p&gt;

&lt;p&gt;결국 일괄 처리할 메시지의 양을 얼마나 잡을지는 아키텍처적 Trade-off의 문제이다.&lt;br /&gt;
양을 늘리면 대역폭은 치솟지만 버퍼가 찰 때까지 기다려야 하므로 Latency가 늘어난다.&lt;br /&gt;
따라서 시스템의 용도를 감안하여 이 일괄 처리 크기를 미세조정해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;44-consumer-측-작업-흐름-및-데이터-전달-모델push-vs-pull&quot;&gt;4.4. Consumer 측 작업 흐름 및 데이터 전달 모델(Push vs Pull)&lt;/h2&gt;

&lt;p&gt;생산자 측 작업 흐름과 마찬가지로, 소비자가 브로커로부터 데이터를 안전하고 효율적으로 읽어 가기 위한 핵심 워크 플로우와 클라이언트-브로커 간의 데이터 전달 패러다임을 
정립해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;441-소비자-측-기본-작업-흐름&quot;&gt;4.4.1. 소비자 측 기본 작업 흐름&lt;/h3&gt;

&lt;p&gt;소비자 아키텍처의 가장 기본적인 작동 원리는 단순하다.&lt;br /&gt;
&lt;strong&gt;소비자는 브로커에게 특정 파티션의 오프셋 번호를 건네고, 해당 위치에서부터 이벤트를 묶어(Batch) 가져온다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/consumer.png&quot; alt=&quot;소비자 측 메시지 흐름&quot; /&gt;&lt;/p&gt;

&lt;p&gt;소비자-1은 오프셋 6번을 브로커에 요청하여 그 이후의 이벤트들을 묶음으로 당겨오며, 처리가 더 빠른 소비자-2는 오프셋 13번을 주고 그 이후의 이벤트를 가져온다.&lt;br /&gt;
이처럼 각 소비자는 본인이 읽어야 할 위치를 독립적으로 제어할 수 있다.&lt;/p&gt;

&lt;p&gt;그렇다면 이 일괄 조회 과정을 브로커가 메시지를 받는 즉시 소비자에게 밀어주어야 할까(Push), 아니면 소비자가 준비되었을 때 당겨와야 할까?(Pull)&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;442-push-모델&quot;&gt;4.4.2. Push 모델&lt;/h3&gt;

&lt;p&gt;브로커가 메시지를 밀어 넣는 푸시 모델은 Latency를 최소화하는데 유리하다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메시지가 브로커에 도착하자마자 소비자에게 전송되므로 실시간성에 가까움&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;생산자가 메시지를 생성하는 속도가 소비자가 처리하는 속도보다 빠르면, 소비자는 트래픽을 감당하지 못하고 결국 Crash됨&lt;/li&gt;
      &lt;li&gt;결국 소비자는 항상 최고 트래픽에 맞춘 오버 스펙의 컴퓨팅 자원을 상시 대기시켜야 하므로 자원 효율성이 떨어짐&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;443-pull-모델채택&quot;&gt;4.4.3. Pull 모델(채택)&lt;/h3&gt;

&lt;p&gt;소비자가 본인의 처리 역량과 타이밍에 맞춰 브로커로부터 데이터를 직접 당겨오는 방식이다.&lt;br /&gt;
대규모 대역폭을 지향하는 시스템(예: 카프카)의 표준 아키텍처이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;그룹 합류 및 할당&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;소비자는 Consumer Group 내에서 본인이 전담할 코디네이터 브로커를 찾아 그룹에 합류하고 파티션을 할당받음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;오프셋 기반 Pull&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;소비자는 상태 저장소에 기록된 본인의 마지막 오프셋 이후부터 메시지를 자신의 처리 능력만큼 당겨(Pull)옴&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 처리 및 갱신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;가져온 메시지 번들을 처리한 후, 성공적으로 완료되면 새로운 오프셋 정보를 브로커에 보냄&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;소비 속도를 소비자가 제어하므로 시스템 안정성이 극대화됨&lt;/li&gt;
      &lt;li&gt;실시간 처리가 필요한 서비스는 끊임없이 당겨가고, 무거운 처리를 하는 배치 분석 시스템은 새벽에 한 번에 당겨가는 등 유연한 구성이 가능&lt;/li&gt;
      &lt;li&gt;무엇보다 &lt;strong&gt;한 번에 가져올 최대 메시지 크기를 소비자가 지정할 수 있어 ‘공격적인 일괄 처리’에 완벽하게 부합&lt;/strong&gt;함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;브로커에 새로 들어온 메시지가 없더라도 소비자가 계속 데이터를 달라고 요청(Polling)하므로, 무의미한 네트워크 트래픽과 소비자 측 CPU 자원이 낭비될 수 있음&lt;/li&gt;
      &lt;li&gt;이 단점은 아래 &lt;strong&gt;Long Polling&lt;/strong&gt; 기술로 해결 가능함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;long-polling-이란&quot;&gt;💡&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Long Polling&lt;/code&gt; 이란?&lt;/h4&gt;

&lt;p&gt;&lt;a href=&quot;https://kafka.apache.org/43/getting-started/introduction/&quot;&gt;롱 폴링(long polling)&lt;/a&gt;은 데이터가 &lt;strong&gt;없을 때만&lt;/strong&gt; 대기하는 최적화 기법이다.&lt;br /&gt;
만일 브로커에 소비자가 가져갈 메시지가 이미 쌓여 있다면, 대기 시간은 &lt;strong&gt;0초이며, 즉시 데이터를 반환&lt;/strong&gt;한다.&lt;/p&gt;

&lt;p&gt;브로커가 완전히 비어 있을 때, 일반적인 ‘쇼트 폴링(Short Polling)’은 ‘데이터 없음’이라는 빈 응답을 즉시 주고 연결을 끊어버려 소비자가 무한 루프를 돌며 
브로커를 Polling 한다.&lt;br /&gt;
반면, 롱 폴링은 데이터가 없을 때 빈 응답을 주는 대신, 데이터가 들어오거나 설정한 타임아웃이 될 때까지 &lt;strong&gt;소켓 커넥션을 끊지 않고 브로커가 대기&lt;/strong&gt;한다.&lt;br /&gt;
대기 도중 메시지가 단 한 건이라도 들어오면 그 즉시 커넥션을 통해 데이터를 전달하며 응답하므로 자원 낭비를 막아준다.&lt;/p&gt;

&lt;p&gt;이러한 이유로 대부분의 메시지 큐는 푸시 모델 대신 풀 모델을 지원한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/pull.png&quot; alt=&quot;풀 모델&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;① 그룹 합류 및 코디네이터 접속&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;소비자 그룹-1에 합류하고 토픽-A를 구독하길 원하는 새로운 소비자가 추가된다.&lt;/li&gt;
      &lt;li&gt;이 소비자는 그룹 이름을 해싱하여 자신이 접속할 브로커 노드를 찾는다.&lt;/li&gt;
      &lt;li&gt;동일한 해시 함수와 그룹 이름을 사용하기 때문에, &lt;strong&gt;같은 그룹의 모든 소비자는 결국 클러스터 내의 동일한 브로커 노드에 접속&lt;/strong&gt;하게 된다.&lt;/li&gt;
      &lt;li&gt;⚠️아래 두 개념을 헷갈리지 말자&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;소비자 그룹 코디네이터(Consumer Group Coordinator)&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;위의 해싱 과정을 통해 지정된 특정 브로커 노드를 뜻하며, 오직 &lt;strong&gt;해당 소비자 그룹의 조정 작업(멤버 관리, 오프셋 관리 등)&lt;/strong&gt;만 전담한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#34-개략적-설계안&quot;&gt;&lt;strong&gt;조정 서비스(Coordination Service)&lt;/strong&gt;&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;주키퍼나 etcd같은 외부 컴포넌트를 뜻하며, 소비자 개별 그룹이 아니라 &lt;strong&gt;전체 브로커 클러스터의 조정 작업(컨트롤러 선출, 브로커 헬스 체크 등)&lt;/strong&gt;을 담당한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;② 파티션 할당&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;그룹 전담 코디네이터 브로커는 해당 소비자를 그룹에 참여시키고, 토픽-A의 파티션-2를 해당 소비자에게 할당한다.&lt;/li&gt;
      &lt;li&gt;이때 파티션을 분배하는 &lt;a href=&quot;https://kafka.apache.org/20/configuration/consumer-configs/&quot;&gt;파티션 배치 정책&lt;/a&gt;에는 라운드 로빈, Range 기반, Sticky 등 비즈니스 요구사항에 따라 설정할 수 있는 여러 알고리즘이 존재한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;③ 상태 저장소 기반 메시지 수신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;소비자는 자신이 마지막으로 소비했던 오프셋 이후부터 메시지를 브로커(파티션-2)로부터 가져온다.&lt;/li&gt;
      &lt;li&gt;소비자가 이전에 어디까지 읽었는지에 대한 과거 오프셋 정보는 상태 저장소에 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;④ 데이터 처리 및 오프셋 갱신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;소비자는 가져온 메시지를 성공적으로 처리한 후, 자신이 어디까지 읽었는지 나타내는 새로운 오프셋 정보를 브로커(코디네이터)에게 보낸다.
        &lt;ul&gt;
          &lt;li&gt;위 그림에서는 브로커-2로 되어있지만, 실제로는 브로커-1로 오프셋 정보를 보낸다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;이 &lt;strong&gt;‘실제 데이터 비즈니스 로직 처리’와 ‘오프셋 갱신’을 처리하는 순서는 시스템의 신뢰성을 결정하는 ‘메시지 전송 시맨틱(전송 보장 등급)’에 절대적인 영향&lt;/strong&gt;을 미친다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;데이터-처리와-오프셋-갱신-순서가-메시지-전송-시맨틱에-미치는-영향은&quot;&gt;💡데이터 처리와 오프셋 갱신 순서가 메시지 전송 시맨틱에 미치는 영향은?&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;데이터를 먼저 처리하느냐, 오프셋 번호를 먼저 표기하느냐에 따라 시스템의 신뢰성 등급이 완전히 달라진다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;예를 들어 데이터를 받자마자 오프셋부터 늘려놓고 비즈니스 로직을 수행하다가 서버가 다운되면 데이터가 유실(최대 한 번)될 수 있다.&lt;br /&gt;
반대로 로직을 다 끝내고 오프셋을 늘리려다 다운되면 데이터가 중복 처리(최소 한 번)될 수 있다.&lt;/p&gt;

&lt;p&gt;이 메커니즘은 &lt;a href=&quot;#410-메시지-전달-시맨틱과-오프셋-갱신-타이밍&quot;&gt;4.10. 메시지 전달 시맨틱과 오프셋 갱신 타이밍&lt;/a&gt;에서 다룬다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;45-소비자-재조정consumer-rebalancing&quot;&gt;4.5. 소비자 재조정(Consumer rebalancing)&lt;/h2&gt;

&lt;p&gt;Consumer Group 내에 새로운 소비자가 합류하거나, 기존 소비자가 장애로 이탈하면 파티션 소유권을 누구에게 어떻게 재분배할지 결정해야 한다.&lt;br /&gt;
이 동적인 오케스트레이션 과정을 소비자 재조정(Consumer rebalancing)이라고 한다.&lt;/p&gt;

&lt;p&gt;이 과정에서 앞서 해시 함수로 찾아낸 &lt;strong&gt;그룹 코디네이터 브로커&lt;/strong&gt;가 관제탑 역할을 한다.&lt;/p&gt;

&lt;p&gt;코디네이터 브로커는 소비자들이 주기적으로 보내는 &lt;strong&gt;Heartbeat 메시지&lt;/strong&gt;를 살피며 헬스 체크를 하다가, 멤버 목록에 변화가 생기면 즉시 그룹 전체를 재조정 모드로 전환한다.&lt;/p&gt;

&lt;p&gt;코디네이터와 소비자가 어떻게 상호작용하는지 보자.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/coordinator.png&quot; alt=&quot;소비자 그룹 코디네이터&quot; /&gt;&lt;/p&gt;

&lt;p&gt;코디네이터는 자신이 연결한 소비자 목록을 유지하며, 이 목록에 변화가 생기면 코디네이터는 해당 그룹의 새 리더를 선출한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;소비자-목록에-변화가-생기면-코디네이터-브로커는-무조건-새-리더를-선출할까&quot;&gt;💡소비자 목록에 변화가 생기면 코디네이터 브로커는 무조건 ‘새 리더’를 선출할까?&lt;/h3&gt;

&lt;p&gt;그렇다. &lt;strong&gt;재조정이 시작되면 코디네이터는 소비자 그룹 중에서 ‘리더 소비자’를 무조건 새로 선출&lt;/strong&gt;한다.&lt;/p&gt;

&lt;p&gt;주의할 점은 여기서 말하는 리더는 브로커 서버가 아니라 클라이언트 애플리케이션인 &lt;strong&gt;소비자 노드 중 하나&lt;/strong&gt;라는 것이다.&lt;/p&gt;

&lt;p&gt;코디네이터 브로커가 파티션 분배 계획까지 직접 짜면 부하가 너무 커지기 때문에, 코디네이터는 재조정 요청을 가장 먼저 보낸 활성 컨슈머를 &lt;strong&gt;그룹 리더 소비자&lt;/strong&gt;로 임명한다.&lt;br /&gt;
리더 소비자가 파티션 배치 계획을 짜서 코디네이터에게 넘겨주면, 코디네이터는 이를 그룹 내의 다른 일반 소비자(Follower)들에게 전파하는 방식으로 역할을 분담한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/rebalancing.png&quot; alt=&quot;소비자 그룹 재조정&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;451-새로운-소비자가-그룹에-합류하는-경우&quot;&gt;4.5.1. 새로운 소비자가 그룹에 합류하는 경우&lt;/h3&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
  title 📌 새로운 소비자의 합류 📌
  participant A as 소비자 A
  participant C as 코디네이터
  participant B as 소비자 B

  A-&amp;gt;&amp;gt;C: 1a. heartbeat&amp;lt;br/&amp;gt;(해당 그룹에 속한 소비자 A임)
  C-&amp;gt;&amp;gt;A: 1b. heartbeat 정상 수신을 회신

%% 2번 영역 옅은 빨간색 배경 강조
  rect rgb(255, 230, 230)
    B-&amp;gt;&amp;gt;C: 2. 그룹 합류 요청&amp;lt;br/&amp;gt;(소비자 B가 그룹에 합류를 원함)
  end

  A-&amp;gt;&amp;gt;C: 3a. heartbeat&amp;lt;br/&amp;gt;(해당 그룹에 속한 소비자 A임)

%% 3b번 영역 옅은 빨간색 배경 강조
  rect rgb(255, 230, 230)
    C-&amp;gt;&amp;gt;A: 3b. 응답 (소비자 재조정이 필요하므로&amp;lt;br/&amp;gt;다시 그룹에 합류할 것)
  end

  A-&amp;gt;&amp;gt;C: 4a. 그룹 합류 요청&amp;lt;br/&amp;gt;(소비자 A가 그룹에 합류를 원함)
  C-&amp;gt;&amp;gt;A: 4b. 응답 (비-리더로 그룹에 정상 합류함)
  C-&amp;gt;&amp;gt;B: 4b. 응답 (리더로 그룹에 정상 합류하였으며&amp;lt;br/&amp;gt;그룹 멤버는 A, B임)
  A-&amp;gt;&amp;gt;C: 5. 그룹 동기화 요청&amp;lt;br/&amp;gt;(리더가 파티션 배치 계획을 보내길 대기)
  B-&amp;gt;&amp;gt;C: 5. 그룹 동기화 요청&amp;lt;br/&amp;gt;(파티션 배치 계획 전송)
  C-&amp;gt;&amp;gt;A: 6. 그룹 동기화 (A에 파티션 1, 3 배치)
  C-&amp;gt;&amp;gt;B: 6. 그룹 동기화 (B에 파티션 2, 4 배치)
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;452-기존-소비자가-우아하게-그룹을-떠나는-경우graceful-shutdown&quot;&gt;4.5.2. 기존 소비자가 우아하게 그룹을 떠나는 경우(Graceful Shutdown)&lt;/h3&gt;

&lt;p&gt;아래는 기존 소비자 A가 그룹을 떠나는 과정이다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
  title 📌 기존 소비자의 이탈 📌
  participant A as 소비자 A
  participant C as 코디네이터
  participant B as 소비자 B

  A-&amp;gt;&amp;gt;C: 1a. heartbeat (소비자 A)
  C-&amp;gt;&amp;gt;A: 1b. heartbeat 정상 수신 응답
  B-&amp;gt;&amp;gt;C: 1a. heartbeat (소비자 B)
  C-&amp;gt;&amp;gt;B: 1b. heartbeat 정상 수신 응답

%% 2a번 영역 옅은 빨간색 배경 강조
  rect rgb(255, 230, 230)
    A-&amp;gt;&amp;gt;C: 2a. 그룹 탈퇴 요청 (소비자 A)
  end
  C-&amp;gt;&amp;gt;A: 2b. 탈퇴 요청 승인

  B-&amp;gt;&amp;gt;C: 3a. heartbeat (소비자 B)

%% 3b번 영역 옅은 빨간색 배경 강조
  rect rgb(255, 230, 230)
    C-&amp;gt;&amp;gt;B: 3b. heartbeat (소비자 재조정이&amp;lt;br/&amp;gt;필요하므로 그룹 재합류 요청)
  end

  B-&amp;gt;&amp;gt;C: 4a. 그룹 합류 요청 (소비자 B)
  C-&amp;gt;&amp;gt;B: 4b. 응답 (리더로 그룹에&amp;lt;br/&amp;gt;정상 합류하였으며 멤버는 B가 있음)
  B-&amp;gt;&amp;gt;C: 5. 그룹 동기화 요청 (파티션 배치 계획 전송)
  C-&amp;gt;&amp;gt;B: 6. 그룹 동기화&amp;lt;br/&amp;gt;(B에 파티션 1, 2, 3, 4 배치)
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;453-기존-소비자가-비정상적으로-다운된-경우장애-감지&quot;&gt;4.5.3. 기존 소비자가 비정상적으로 다운된 경우(장애 감지)&lt;/h3&gt;

&lt;p&gt;아래는 소비자 A가 비정상적으로 가동을 중단한 경우에 대한 처리 흐름이다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    title 📌 기존 소비자에 장애 발생 📌
    participant A as 소비자 A
    participant C as 코디네이터
    participant B as 소비자 B

    A-&amp;gt;&amp;gt;C: 1a. heartbeat (소비자 A)
    C-&amp;gt;&amp;gt;A: 1b. heartbeat 정상 수신 응답
    B-&amp;gt;&amp;gt;C: 1a. heartbeat (소비자 B)
    C-&amp;gt;&amp;gt;B: 1b. heartbeat 정상 수신 응답

    %% 2번 영역 옅은 빨간색 배경 강조
    rect rgb(255, 230, 230)
        Note left of C: 2. 소비자 A로부터 더 이상의&amp;lt;br/&amp;gt;heartbeat 없음. 장애가 발생한 것으로&amp;lt;br/&amp;gt;보이므로 소비자 재조정이 필요.
    end

    B-&amp;gt;&amp;gt;C: 3a. heartbeat (소비자 B)

    %% 3b번 영역 옅은 빨간색 배경 강조
    rect rgb(255, 230, 230)
        C-&amp;gt;&amp;gt;B: 3b. heartbeat (소비자 재조정이&amp;lt;br/&amp;gt;필요하므로 그룹 재합류 요청)
    end

    B-&amp;gt;&amp;gt;C: 4a. 그룹 합류 요청 (소비자 B)
    C-&amp;gt;&amp;gt;B: 4b. 응답 (리더로 그룹에&amp;lt;br/&amp;gt;정상 합류하였으며 멤버는 B가 있음)
    B-&amp;gt;&amp;gt;C: 5. 그룹 동기화 요청 (파티션 배치 계획 전송)
    C-&amp;gt;&amp;gt;B: 6. 그룹 동기화&amp;lt;br/&amp;gt;(B에 파티션 1, 2, 3, 4 배치)
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;46-상태-저장소-메타데이터-저장소-주키퍼&quot;&gt;4.6. 상태 저장소, 메타데이터 저장소, 주키퍼&lt;/h2&gt;

&lt;p&gt;분산 메시지 큐 클러스터가 대용량 트래픽 속에서도 질서를 유지하려면 브로커의 상태, 소비자들의 오프셋, 토픽의 설정 정보를 관리하는 ‘분산 제어 계층’이 단단해야 한다.&lt;br /&gt;
여기서는 복잡도를 낮추기 위해 데이터 계층과 제어 계층을 엄격히 분리하고, 그 중심에 주키퍼(ZooKeeper)를 배치하여 관리한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;461-상태-저장소&quot;&gt;4.6.1. 상태 저장소&lt;/h3&gt;

&lt;p&gt;상태 저장소에는 Consumer Group이 각 파티션에서 마지막으로 읽어간 메시지의 위치인 &lt;strong&gt;오프셋&lt;/strong&gt; 정보와 소비자에 대한 파티션 배치 관계가 저장된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/offset.png&quot; alt=&quot;소비자 그룹별 마지막 소비 오프셋&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림처럼 소비자 그룹-1의 한 소비자가 파티션의 메시지를 순서대로 읽은 뒤 마지막으로 읽어간 메시지의 오프셋을 6으로 갱신해 두면, 해당 소비자가 고장 나더라도 같은 그룹의 
새로운 소비자가 이어받아 7번부터 안전하게 읽어갈 수 있다.&lt;/p&gt;

&lt;p&gt;이 상태 저장소 데이터의 이용 패턴을 인프라 관점에서 보면 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;읽기/쓰기가 매초 수만 번 이상 극도로 빈번하게 발생하지만, 데이터의 전체 양은 아주 적다.&lt;/li&gt;
  &lt;li&gt;데이터의 번호가 계속해서 올라가는 &lt;strong&gt;갱신 연산이 대부분&lt;/strong&gt;이며, 삭제되는 일은 거의 없다.&lt;/li&gt;
  &lt;li&gt;소비자들이 각자 다른 속도로 다른 파티션을 읽으므로, 읽기/쓰기 연산이 매우 &lt;strong&gt;무작위적인 패턴&lt;/strong&gt;을 보인다.&lt;/li&gt;
  &lt;li&gt;전송 보장을 위해 데이터의 일관성이 무엇보다 중요하다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;데이터의 일관성 및 높은 읽기/쓰기 속도에 대한 요구사항을 고려하면 주키퍼와 같은 키-값 저장소를 사용하는 것이 바람직하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;왜-데이터의-일관성-및-높은-읽기쓰기-속도에-요구될-때-주키퍼와-같은-키-값-저장소가-바람직할까&quot;&gt;💡왜 데이터의 일관성 및 높은 읽기/쓰기 속도에 요구될 때 주키퍼와 같은 키-값 저장소가 바람직할까?&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;상태 데이터의 특성인 ‘잦은 무작위 갱신’을 메모리 기반으로 가볍게 소화하면서 분산 노드 간의 동기화를 완벽하게 보장&lt;/strong&gt;하기 때문이다.&lt;/p&gt;

&lt;p&gt;오프셋 데이터는 용량이 작지만 무작위로 끊임없이 업데이트된다.&lt;br /&gt;
이를 일반적인 디스크 기반 RDBMS에 쓰면 디스크 헤더가 물리적으로 요동치며 쓰기 병목이 발생한다.&lt;br /&gt;
반면 주키퍼 같은 키-값 저장소는 데이터를 &lt;strong&gt;메인 메모리에 트리 구조로 유지&lt;/strong&gt;하므로 무작위 업데이트 속도가 압도적으로 빠르다.&lt;/p&gt;

&lt;p&gt;또한 주키퍼는 자체적인 분산 합의 프로토콜(Zab)을 통해 클러스터 노드 간의 ‘강한 일관성’을 제공한다.&lt;br /&gt;
덕분에 하나의 컨슈머 서버가 다운되어 재조정이 일어나도, 새로 투입된 컨슈머가 단 1번의 오차도 없이 일관된 최신 오프셋 상태를 즉시 읽어와 작업을 이어갈 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;카프카는-왜-오프셋-저장소를-브로커로-이전했을까&quot;&gt;💡카프카는 왜 오프셋 저장소를 브로커로 이전했을까?&lt;/h4&gt;

&lt;p&gt;초기 아파치 카프카는 이 오프셋 상태 정보를 주키퍼에 직접 저장했다. 하지만 트래픽이 거대해지자 소비자가 메시지를 읽을 때마다 주키퍼에 쓰기 연산이 발생하면서, 
&lt;strong&gt;주키퍼 클러스터 전체가 오프셋 쓰기 트래픽을 견디지 못하고 마비&lt;/strong&gt;되는 병목 현상이 발생했다.&lt;/p&gt;

&lt;p&gt;이를 해결하기 위해 &lt;a href=&quot;https://towardsdatascience.com/kafka-no-longer-requires-zookeeper-ebfbf3862104/&quot;&gt;카프카는 오프셋 저장소를 주키퍼에서 카프카 브로커 내부로 이전하였다.&lt;/a&gt;&lt;br /&gt;
오프셋 정보를 주키퍼가 아닌, 카프카 브로커가 관리하는 내부 특수 토픽인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;__consumer_offsets&lt;/code&gt;에 일괄 처리 순차 쓰기 방식으로 기록하게 한 것이다.&lt;br /&gt;
이 결정 덕분에 주키퍼는 무거운 쓰기 부하에서 해방되었다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;최신-트렌드-kraft&quot;&gt;💡최신 트렌드: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KRaft&lt;/code&gt;&lt;/h4&gt;

&lt;p&gt;오프셋 저장소를 브로커 내부로 옮겼음에도 불구하고, 메타데이터 때문에 여전히 주키퍼에 의존해야 하는 구조는 대규모 환경(파티션 수십만 개 이상)에서 동기화 지연이라는 한계가 있었다.&lt;br /&gt;
이에 최신 대규모 아키텍처 트렌드는 메타데이터마저 카프카 자체 로그 토픽으로 관리하고, 브로커들이 직접 Raft 합의 알고리즘을 수행하는 &lt;strong&gt;KRaft 모드&lt;/strong&gt;를 채택하여 주키퍼를 
완전히 제거하는 방향으로 진화하였다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;462-메타데이터-저장소&quot;&gt;4.6.2. 메타데이터 저장소&lt;/h3&gt;

&lt;p&gt;메타데이터 저장소에는 토픽 설정 정보, 파티션 수, 메시지 보관 기간(2주), 사본 배치 계획(어느 브로커에 복제본을 둘 것인가) 등이 저장된다.&lt;br /&gt;
이 데이터의 트래픽 패턴은 상태 저장소와 정반대이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;토픽 설정을 바꿀 때만 데이터가 변경되므로 &lt;strong&gt;자주 변경되지 않으며, 전체 데이터양도 매우 적다.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;하지만 메타데이터가 브로커마다 다르게 인식되면 데이터 라우팅이 꼬여 유실이 발생하므로 &lt;strong&gt;절대적인 일관성을 요구&lt;/strong&gt;한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;메타데이터-저장소로-주키퍼가-적절한-이유는&quot;&gt;💡메타데이터 저장소로 주키퍼가 적절한 이유는?&lt;/h4&gt;

&lt;p&gt;주키퍼는 자주 바뀌지 않지만 절대 틀려서는 안 되는 ‘CP(일관성 중심) 데이터’를 지키는 분산 제어에 아주 탁월하다.&lt;/p&gt;

&lt;p&gt;주키퍼는 CAP 이론 중 가용성(Availability)을 일부 타협하더라도 일관성(Consistency)을 완벽하게 지켜낸다.&lt;br /&gt;
메타데이터처럼 빈도는 낮지만 클러스터 전체가 단 1바이트의 오차도 없이 동일한 상태를 동기화해야 하는 ‘제어용 데이터’를 보관하기에 주키퍼의 계층적 트리 구조와 
분산 Lock 기능은 완벽한 선택지이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;463-주키퍼&quot;&gt;4.6.3. 주키퍼&lt;/h3&gt;

&lt;p&gt;주키퍼는 &lt;strong&gt;계층적 키-값 저장소&lt;/strong&gt; 기능을 제공하는, 분산 시스템에 필수적인 서비스이다.&lt;br /&gt;
디렉터리 구조와 유사한 트리 형태로 데이터를 관리하며, 신뢰성이 극도로 높아 보통 분산 설정 서비스, 동기화 서비스, 레지스트리 등으로 널리 이용된다.&lt;/p&gt;

&lt;p&gt;여기서는 주키퍼를 활용하여 아래 아키텍처 다이어그램처럼 분산 큐 시스템 설계를 단순화한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/zookeeper.png&quot; alt=&quot;주키퍼&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;메타데이터와 상태 저장소의 위임&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클러스터 제어에 필요한 핵심 저장소인 메타데이터 저장소와 상태 저장소를 주키퍼를 이용하여 구현한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;브로커의 역할 최소화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;복잡한 제어용 데이터를 외부에 위임한 덕분에, 브로커는 이제 오직 본연의 임무인 [&lt;strong&gt;‘메시지 데이터 저장소(WAL)’](#412-선택지-2-쓰기-우선-로그wal-write-ahead-log채택)만 유지&lt;/strong&gt;하면 된다.&lt;/li&gt;
      &lt;li&gt;복잡도가 내려가니 브로커의 성능과 안정성이 극대화된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;클러스터 리더 선출 과정 지원&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;브로커 클러스터를 총괄할 브로커(컨트롤러)를 선출하는 과정에서 주키퍼가 분산 동기화 기술을 통해 리더 선출 과정을 돕는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;47-데이터-유실-제로-파티션-사본-동기화isr-전략&quot;&gt;4.7. 데이터 유실 제로: 파티션 사본, 동기화(ISR) 전략&lt;/h2&gt;

&lt;h3 id=&quot;471-파티션-사본&quot;&gt;4.7.1. 파티션 사본&lt;/h3&gt;

&lt;p&gt;하드웨어는 언제든 고장날 수 있다.&lt;br /&gt;
특정 브로커 노드가 갑자기 다운되거나 디스크 에러가 발생하더라도 데이터가 유실되지 않고 중단 없이 서비스를 이어가게 만드는 핵심 메커니즘이 바로 사본 복제(Replication)이다.&lt;/p&gt;

&lt;p&gt;아래는 각 파티션은 3개의 사본을 갖고, 이 사본들은 서로 다른 브로커 노드에 분산되어 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/replication.png&quot; alt=&quot;복제&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림처럼 토픽의 각 파티션은 지정된 복제 계수(Replication Factor, 여기서는 3)에 따라 여러 브로커 노드에 사본을 분산 배치한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;리더 사본(Leader Replica):&lt;/strong&gt; 짙은 색으로 강조된 파티션으로, Producer의 쓰기 요청과 Consumer의 읽기 요청을 최전선에서 담당&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단순 사본(Follower Replica):&lt;/strong&gt; 오직 고가용성 백업을 위해 리더 브로커로부터 데이터를 지속적으로 복사해두는 역할만 수행&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;생산자가 보낸 메시지를 완전히 동기화한 사본의 개수가 지정된 임계값을 넘으면, 리더 브로커는 생산자에게 메시지를 잘 받았다는 수신 확인 응답(ACK)를 보낸다.&lt;br /&gt;
이 ACK를 얼마나 까다롭게 정의할지에 대해서는 &lt;a href=&quot;#48-producer-ackall-0-1-옵션&quot;&gt;4.8. Producer ACK(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;) 옵션&lt;/a&gt;에서 알아본다.&lt;/p&gt;

&lt;p&gt;사본을 어떤 브로커 노드에 어떤 규칙으로 분산하여 배치할지 기술하는 것을 &lt;strong&gt;사본 분산 계획&lt;/strong&gt;이라고 한다.&lt;br /&gt;
예를 들어 위의 그림에서의 사본 분산 계획은 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;토픽-A의 파티션-1: 사본 3개 배치. 리더 사본은 &lt;strong&gt;브로커-1&lt;/strong&gt;에 두고, 단순 사본들은 &lt;strong&gt;브로커-2와 브로커-3&lt;/strong&gt;에 분산&lt;/li&gt;
  &lt;li&gt;토픽-A의 파티션-2: 사본 3개 배치. 리더 사본은 &lt;strong&gt;브로커-2&lt;/strong&gt;에 두고, 단순 사본들은 &lt;strong&gt;브로커-3과 브로커-4&lt;/strong&gt;에 분산&lt;/li&gt;
  &lt;li&gt;토픽-A의 파티션-3: 사본 3개 배치. 리더 사본은 &lt;strong&gt;브로커-3&lt;/strong&gt;에 두고, 단순 사본들은 &lt;strong&gt;브로커-1과 브로커-4&lt;/strong&gt;에 분산&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 사본 분산 계획은 누가 만들까?&lt;/p&gt;

&lt;p&gt;주키퍼 같은 조정 서비스의 도움으로 브로커 노드 가운데 하나가 클러스터의 대장인 컨트롤러로 선출되면, 해당 활성 컨트롤러 브로커 노드가 전체 클러스터의 자원 상태를 보고 
이 사본 분산 계획을 직접 수립한다.&lt;br /&gt;
계획 작성이 완료되면 이를 &lt;strong&gt;메타데이터 저장소&lt;/strong&gt;에 보관하여 모든 브로커와 클라이언트가 라우팅 가이드로 삼도록 제어한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;메시지를-여러-파티션에-두면-같은-메시지가-여러-파티션에-중복되어-저장되는-걸까&quot;&gt;💡메시지를 여러 파티션에 두면 같은 메시지가 여러 파티션에 중복되어 저장되는 걸까?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;절대 아니다. 분산 시스템의 ‘샤딩’과 ‘복제’를 명확히 구분해야 한다.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;파티션 분할(샤딩)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하나의 토픽 데이터를 &lt;strong&gt;서로 다른 내용으로 쪼개는 것&lt;/strong&gt;이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;사본 복제(Replication)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;쪼개진 파티션-1 이라는 파일 자체를 &lt;strong&gt;브로커-1, 브로커-2, 브로커-3에 똑같이 복사해서 복제본을 만드는 것&lt;/strong&gt;이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;즉, 데이터의 중복 저장은 파티션 간에 일어나는 것이 아니라, &lt;strong&gt;동일한 하나의 파티션을 안전하게 지키기 위해 리더와 팔로워 사본 사이에서 의도적으로 발생하는 것&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;472-사본-동기화isr&quot;&gt;4.7.2. 사본 동기화(ISR)&lt;/h3&gt;

&lt;p&gt;ISR(In-Sync Replicas)은 동기화된 사본을 말한다.&lt;br /&gt;
리더는 항상 ISR 그룹에 포함된다.&lt;/p&gt;

&lt;p&gt;리더 브로커 혼자만 디스크에 쓰고 바로 응답을 주면 리더가 장애로 죽었을 때 데이터가 소실된다.&lt;br /&gt;
반대로 클러스터 안의 모든 팔로워 사본이 복제를 끝낼 때까지 생산자를 무한정 대기시키면 시스템의 응답 속도가 지연된다.&lt;br /&gt;
어느 사본 하나라도 네트워크 지연이 생기면 파티션 전체가 마비되기 때문이다.&lt;/p&gt;

&lt;p&gt;이 극단적인 성능과 영속성의 타협점이 바로 ISR이다.&lt;/p&gt;

&lt;p&gt;ISR은 파티션 리더의 최신 데이터를 잘 복사하여 실시간으로 따라잡고 있는 &lt;strong&gt;팔로워 사본들의 멤버 집합&lt;/strong&gt;이다.&lt;br /&gt;
만일 어떤 팔로워 사본이 성능 저하로 인해 리더의 최신 오프셋을 제시간에 따라잡지 못하면, 리더는 해당 사본을 ISR 목록에서 즉시 추방한다.&lt;br /&gt;
추후 리더 브로커가 장애로 다운되었을 때, 컨트롤러 브로커는 &lt;strong&gt;오직 이 ISR 그룹 내에서 검증된 단순 사본 중에서만 새로운 리더를 선출&lt;/strong&gt;하여 데이터 유실을 원천 방어한다.&lt;/p&gt;

&lt;p&gt;어떤 사본이 ISR 집합에 계속 남아있을 수 있는지 결정하는 과거 기준 중 하나로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replica.lag.max.message&lt;/code&gt; 설정이 있다.&lt;br /&gt;
리더 사본과 팔로워 사본 간의 메시지 개수 차이 한계선을 뜻한다.&lt;br /&gt;
예를 들어 이 값이 5로 설정되어 있을 때, 단순 사본에 보관된 메시지 개수와 리더 사본과의 차이가 3이면 해당 사본은 격차가 한계선인 5보다 작으므로 ISR 집합의 일원이다.&lt;/p&gt;

&lt;p&gt;아래 다이어그램을 보면서 ISR 내부에서 합의와 동기화가 어떻게 연산되는지 보자.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/isr.png&quot; alt=&quot;ISR 동작 원리&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;합의 오프셋(Committed Offset)의 흐름&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;현재 리더 사본의 합의 오프셋 값은 &lt;strong&gt;13&lt;/strong&gt;이다.&lt;/li&gt;
      &lt;li&gt;합의 오프셋은 &lt;strong&gt;이 오프셋 이전에 기록된 모든 메시지가 이미 ISR 집합 내의 모든 사본에 동기화가 완전히 끝났음&lt;/strong&gt;을 보장하는 선이다.&lt;/li&gt;
      &lt;li&gt;이후 이 리더의 14번, 15번이라는 2개의 새로운 메시지가 Producer로부터 기록되지만, 아직 사본 간의 전체 합의가 이루어진 것은 아니므로 컨슈머는 이 데이터를 읽을 수 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;사본-2, 사본-3(ISR 멤버)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;두 사본은 현재 리더의 상태를 동기화하여 ISR 상태를 유지하고 있다.&lt;/li&gt;
      &lt;li&gt;따라서 리더에 새로 들어온 14번, 15번 메시지를 즉시 읽어 가기 위한 가져오기(Fetch) 연산을 수행할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;사본-4(ISR 추방 상태)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;현재 11번 오프셋까지만 복제한 상태로, 리더의 최신 상태(15번)를 충분히 따라잡지 못했다.&lt;/li&gt;
      &lt;li&gt;격차가 임계치를 넘어섰기 때문에 아직 ISR 그룹이 아니다.&lt;/li&gt;
      &lt;li&gt;사본-4가 고장 상태에서 복구되어 리더의 오프셋을 충분히 따라잡고 나면, 컨트롤러에 의해 다시 ISR 집합으로 승격될 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;deep-dive-isr-목록을-관리하는-high-watermark와-hands-free-복제&quot;&gt;🚀Deep Dive: ISR 목록을 관리하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;High Watermark&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hands-Free 복제&lt;/code&gt;&lt;/h4&gt;

&lt;p&gt;둘 다 &lt;strong&gt;Consumer가 읽어도 안전한 ‘최종 합의선’을 긋고, 뒤쳐지는 사본을 시스템이 알아서 걸러내는 지능형 관리 기법&lt;/strong&gt;이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://rongxinblog.wordpress.com/2016/07/29/kafka-high-watermark/&quot;&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;High Watermark&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;위 그림의 &lt;strong&gt;합의 오프셋(Committed Offset=13)&lt;/strong&gt;과 정확히 일치하는 개념이다.&lt;/li&gt;
      &lt;li&gt;ISR 내의 모든 사본이 복제를 완료한 안전 한계선으로, Consumer는 오직 이 HW 이하의 메시지만 읽을 수 있다.&lt;/li&gt;
      &lt;li&gt;리더가 응답을 주기 직전에 다운되더라도 Consumer가 유령 데이터를 읽게 되는 현상을 막아주는 방어선이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.confluent.io/blog/hands-free-kafka-replication-a-lesson-in-operational-simplicity/&quot;&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hands-Free 복제&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;과거 리더와 팔로워 간의 메시지 개수 차이 설정인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replica.lag.max.message&lt;/code&gt; 방식은 트래픽 피크 타임에 멀쩡한 팔로워가 순간적 격차 때문에 ISR에서 억울하게 방출되었다가 다시 들어오는 스래싱(Thrashing) 현상이 발생했다.&lt;/li&gt;
      &lt;li&gt;이를 개선하여 최신 아키텍처는 오직 &lt;strong&gt;시간 기반의 지연 한계선(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replica.lag.time.max.ms&lt;/code&gt;, 예: 10초)&lt;/strong&gt;만 측정한다.&lt;/li&gt;
      &lt;li&gt;팔로워가 10초 이상 리더에게 데이터를 달라는 요청(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FetchRequest&lt;/code&gt;)을 보내지 않을 때만 진짜 고장난 것으로 판단하여 ISR에서 제외하므로 급격한 트래픽 변동 속에서도 인프라 복제 계층이 안정적으로 유지된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;48-producer-ackall-0-1-옵션&quot;&gt;4.8. Producer ACK(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1&lt;/code&gt;) 옵션&lt;/h2&gt;

&lt;p&gt;파티션 복제와 ISR 집합이 준비되었다면, 이제 Producer가 메시지를 보낼 때 ‘어느 수준까지 복제되는 것을 확인하고 수신 확인 응답(ACK)를 받을 것인가?’를 결정해야 한다.&lt;br /&gt;
이 설정은 시스템의 영속성과 지연시간 사이의 타협점을 정의하는 핵심이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;481-ackall-또는-ack-1&quot;&gt;4.8.1. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACK=all&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACK=-1&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Producer는 파티션 리더 뿐만 아니라, 현재 ISR 집합 내에 속한 &lt;strong&gt;모든 동기화된 사본(팔로워)이 메시지를 완전히 수신한 것까지 확인&lt;/strong&gt;한 뒤에야 브로커로부터 최종 ACK 응답을 받는다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/all.png&quot; alt=&quot;ACK=all&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;가장 느린 팔로워 브로커의 네트워크 응답까지 기다려야 하므로 전송 지연 시간이 다소 길어진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메시지의 영속성 측면에서는 가장 완벽하다.&lt;/li&gt;
      &lt;li&gt;리더 브로커가 ACK를 보낸 직후 다운되어도, ISR 내의 다른 사본들이 데이터를 100% 갖고 있으므로 데이터 유실이 절대 발생하지 않는다.&lt;/li&gt;
      &lt;li&gt;금융 결제 등 데이터 유실이 곧 대참사로 이어지는 도메인에 필수적이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;482-ack1&quot;&gt;4.8.2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACK=1&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Producer는 파티션의 &lt;strong&gt;리더 사본이 자신의 디스크(WAL)에 메시지를 무사히 기록하고 나면&lt;/strong&gt; 팔로워들이 복제를 마쳤는지 확인하지 않고 브로커로부터 즉시 ACK 응답을 받는다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/ack1.png&quot; alt=&quot;ACK=1&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;팔로워들의 복제를 기다리지 않으므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACK=all&lt;/code&gt;에 비해 응답 지연 시간이 크게 개선된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;위험성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;리더 브로커가 메시지를 디스크에 쓰고 Producer에게 ACK를 보낸 바로 그 직후, 단순 사본들이 데이터를 읽어가기 전에 리더 브로커가 다운되면 해당 메시지는 유실된다.&lt;/li&gt;
      &lt;li&gt;약간의 데이터 손실은 감수할 수 있지만 적당히 빠른 응답 속도가 필요한 일반적인 서비스 환경에 적합하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;483-ack0&quot;&gt;4.8.3. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACK=0&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Producer는 메시지를 브로커로 발송하는 즉시 &lt;strong&gt;수신 확인 응답을 전혀 기다리지 않고&lt;/strong&gt; 곧바로 다음 메시지를 전송한다.&lt;br /&gt;
브로커가 메시지를 잘 받았는지 실패했는지 관심이 없으므로 어떠한 retry도 하지 않는다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/ack0.png&quot; alt=&quot;ACK=0&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;특징&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;네트워크 왕복 비용(RTT)이 제로에 수렴하므로 지연 시간이 극도로 낮아지고 처리량은 물리적 한계치까지 치솟는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;위험성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;네트워크 장애로 브로커에 닿지 못했거나 브로커 디스크가 꽉 차서 저장이 실패해도 Producer는 이를 알 방법이 없어 데이터 유실율이 가장 높다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;용도&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;실시간 웹 트래픽 지표 수집, 데이터 로깅 등 유실이 조금 발생해도 비즈니스 전체 흐름에 큰 타격이 없는 서비스에 좋다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;producer-측의-ack-와-consumer-측의-ack&quot;&gt;💡Producer 측의 ACK 와 Consumer 측의 ACK&lt;/h3&gt;

&lt;p&gt;분산 메시지 큐 시스템에는 &lt;strong&gt;‘생산자 측 ACK’와 ‘소비자 측 ACK’가 완전히 다른 목적으로 둘 다 존재&lt;/strong&gt;한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Producer 측의 ACK 설정(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;acks=all, 1, 0&lt;/code&gt;)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;핵심 질문&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;생산자가 보낸 데이터가 브로커 디스크와 사본(ISR)에 &lt;strong&gt;안전하게 저장되었는가?&lt;/strong&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;동작 원리&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;생산자가 브로커에게 메시지를 전달했을 때, 브로커가 ‘디스크에 기록 완료했음’이라고 생산자에게 응답을 보내주는 기준을 정의&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;설정 위치&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;생산자 클라이언트 애플리케이션 소스코드&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Consumer 측의 ACK 설정 (오프셋 커밋)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;핵심 질문&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;브로커에서 가져간 데이터가 소비자 비즈니스 로직에서 &lt;strong&gt;안전하게 처리되었는가?&lt;/strong&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;동작 원리&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;소비자가 Pull 방식으로 메시지를 당겨와 처리한 뒤, 코디네이터 브로커에게 ‘메시지를 성공적으로 처리 완료했으니 다음 메시지 오프셋으로 기록해’라고 응답을 보내주는 행위&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;설정 위치&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;소비자 클라이언트 애플리케이션(예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;auto.commit&lt;/code&gt;)&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;즉,&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;생산자 ACK(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;acks=all, 1, 0&lt;/code&gt;)&lt;/strong&gt;: Write 의 안정성을 제어하는 생산자 옵션&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;소비자 ACK(오프셋 커밋)&lt;/strong&gt;: Read 의 완료 지점을 제어하는 소비자 옵션&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;49-규모-확장성&quot;&gt;4.9. 규모 확장성&lt;/h2&gt;

&lt;p&gt;대규모 아키텍처 환경에서는 트래픽 증가에 따라 서버를 증설하거나, 고장난 서버를 교체하는 작업이 &lt;strong&gt;서비스 중단없이(Zero-Downtime)&lt;/strong&gt; 진행되어야 한다.&lt;br /&gt;
브로커의 장애 복구와 무중단 확장 계층에 대해 알아보자.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;491-브로커-결함-허용fault-tolerance과-장애-복구&quot;&gt;4.9.1. 브로커 결함 허용(Fault Tolerance)과 장애 복구&lt;/h3&gt;

&lt;p&gt;4개의 브로커 노드를 운영 중이던 클러스터에서 브로커-3이 다운되었다고 가정해보자.&lt;br /&gt;
이 상황을 복구하는 흐름은 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/broker1.png&quot; alt=&quot;브로커 노드의 장애&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;최초 구성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;4개의 브로커가 있고, 파티션 분산 계획은 아래와 같음
        &lt;ul&gt;
          &lt;li&gt;토픽-A의 파티션-1: 사본은 각각 브로커-1(리더), 2, 3에 존재&lt;/li&gt;
          &lt;li&gt;토픽-A의 파티션-2: 사본은 각각 브로커-2(리더), 3, 4에 존재&lt;/li&gt;
          &lt;li&gt;토픽-B의 파티션-1: 사본은 각각 브로커-3(리더), 1, 4에 존재&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장애 감지 및 컨트롤러의 개입&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;조정 서비스(주키퍼 등)가 브로커-3의 하트비트가 끊겼음을 감지함&lt;/li&gt;
      &lt;li&gt;대장 브로커인 &lt;strong&gt;컨트롤러&lt;/strong&gt;가 브로커-3 내부에 있던 리더 파티션들(예: 토픽-B 파티션-1)을 정상적인 다른 사본 노드 중 하나로 긴급 승격시킴
        &lt;ul&gt;
          &lt;li&gt;토픽-A의 파티션-1: 사본은 각각 브로커-1(리더), 2에 존재&lt;/li&gt;
          &lt;li&gt;토픽-A의 파티션-2: 사본은 각각 브로커-2(리더), 4에 존재&lt;/li&gt;
          &lt;li&gt;토픽-B의 파티션-1: 사본은 각각 1, 4에 존재&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;분산 계획 재수립&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;브로커 컨트롤러는 남은 브로커들을 대상으로 아래와 같이 파티션 분산 계획을 수립함
        &lt;ul&gt;
          &lt;li&gt;토픽-A의 파티션-1: 사본은 각각 브로커-1(리더), 2, 4(신규)에 존재&lt;/li&gt;
          &lt;li&gt;토픽-A의 파티션-2: 사본은 각각 브로커-2(리더), 4, 1(신규)에 존재&lt;/li&gt;
          &lt;li&gt;토픽-B의 파티션-1: 사본은 각각 브로커-4(리더), 1, 2(신규)에 존재&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;백그라운드 동기화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;새로 지정된 브로커의 단순 사본들은 리더 사본의 데이터를 백그라운드에서 따라잡기 시작함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;글로벌 인프라 설계 시 추가 고려사항&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;파티션의 모든 복제본이 같은 Rack 이나 같은 가용 영역(AZ)에 몰려있으면 데이터 센터 전력 마비 시 파티션이 전멸한다.
    &lt;ul&gt;
      &lt;li&gt;따라서 사본 배치 정책 수립 시 반드시 &lt;strong&gt;랙 인식(Rack-Awareness) 및 멀티 가용 영역 분산&lt;/strong&gt; 전략을 적용해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;서로 다른 물리적 데이터 센터 간의 실시간 복제는 지연 시간이 너무 크므로, 평소에는 센터 내 복제만 수행하고 센터 간에는 &lt;a href=&quot;https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27846330&quot;&gt;&lt;strong&gt;데이터 미러링(MirrorMaker 등)&lt;/strong&gt;&lt;/a&gt; 도구를 사용하여 비동기로 데이터를 복제하는 것이 정석이다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;492-무중단-브로커-규모-확장scale-out&quot;&gt;4.9.2. 무중단 브로커 규모 확장(Scale-out)&lt;/h3&gt;

&lt;p&gt;클러스터의 용량이 부족하여 새로운 브로커-4 서버를 네트워크에 추가하였다.&lt;br /&gt;
이 때 데이터의 쏠림 없이 자원을 균등하게 분배하기 위해 시스템은 일시적인 &lt;strong&gt;오버 복제(Over-Replication)&lt;/strong&gt; 전략을 취한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/broker2.png&quot; alt=&quot;새 브로커 노드의 추가&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;최초 구성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;3개의 브로커, 2개의 파티션, 파티션 당 3개의 사본&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;임시 계획 수립&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;컨트롤러 브로커는 토픽-A 파티션-2를 새로운 서버로 나누기 위해 사본 배치 계획을 임시로 변경한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;복제 가동&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;신규 노드인 브로커-4에 배치된 사본이 파티션 리더인 브로커-2로부터 데이터를 당겨와 동기화를 개시한다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;이 동기화가 진행되는 동안 해당 파티션의 사본 수는 일시적으로 목표치(3개)보다 많아진다.&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;구 사본 제거&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;브로커-4가 리더의 오프셋을 완벽히 따라잡아 동기화가 완료되면, 원래 계획에 있었던 브로커-1 내부의 불필요해진 사본을 안전하게 삭제하고 저장 공간을 반환한다.&lt;/li&gt;
      &lt;li&gt;서비스 중단은 단 1초도 발생하지 않는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;493-파티션-개수-변경과-저장소-계층의-변화&quot;&gt;4.9.3. 파티션 개수 변경과 저장소 계층의 변화&lt;/h3&gt;

&lt;p&gt;토픽의 처리 대역폭을 더 늘리기 위해 파티션 개수를 늘리거나(Scale-up) 반대로 유지비 절감을 위해 안 쓰는 파티션을 제거(Decommission)해야 하는 경우가 있다.&lt;br /&gt;
이 때 저장소 파일 계층은 아래와 같이 움직인다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;파티션 추가 시&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;아래는 토픽에 새로운 파티션이 추가된 경우다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/partition1.png&quot; alt=&quot;파티션 추가&quot; /&gt;&lt;/p&gt;

&lt;p&gt;기존에 보관되어 디스크 점유 중이던 2주치 이력 데이터 파일들은 기존 파티션(파티션-1, 2)내에 그대로 유지되며, 다른 곳으로 이동하지 않는다.&lt;br /&gt;
새로운 파티션-3 파일 디렉터리가 브로커 디스크 상에 개설되는 즉시, 그 시점 이후부터 들어오는 새로운 유입 메시지들이 해시 알고리즘에 의해 3개의 파티션 전체로 
균등하게 분산 보관되기 시작한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;파티션 제거 시&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/partition2.png&quot; alt=&quot;파티션 제거&quot; /&gt;&lt;/p&gt;

&lt;p&gt;파티션-3을 제거하기로 결정(Decommission)되면 브로커는 즉시 해당 파티션을 ‘쓰기 금지(Read-Only)’ 상태로 만든다.&lt;br /&gt;
이후 Producer가 보내는 새 메시지들은 남은 파티션-1,2 로그 파일에만 기록된다.&lt;/p&gt;

&lt;p&gt;하지만 디스크에서 파티션-3 파일을 즉시 지우지는 않는다.&lt;br /&gt;
아직 해당 파티션의 낡은 메시지를 읽고 있는 느린 소비자가 존재할 수 있기 때문이다.&lt;br /&gt;
설정된 유예 기간 동안 소비자들이 남은 잔여 메시지를 다 읽어갈 때까지 파일을 유지(Retention)하다가, 기간이 만료되는 시점에 파일 전체를 날려 저장 공간을 반환한다.&lt;/p&gt;

&lt;p&gt;이처럼 &lt;strong&gt;생산자는 브로커와 통신할 때 메타데이터를 통해 파티션 변경 사실을 통지받으며, 소비자는 브로커의 통제 하에 안전하게 Rebalancing을 시행하므로, 파티션 수의 
인프라적 조정은 생산자와 소비자의 안전성과 중단 없는 서비스 레이어에 아무런 영향을 미치지 않는다.&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;494-producer의-확장성&quot;&gt;4.9.4. Producer의 확장성&lt;/h3&gt;

&lt;p&gt;브로커와 파티션 같은 저장소 인프라가 무중단으로 확장될 수 있는 것처럼, 데이터를 밀어 넣고 당겨가는 클라이언트(생산자/소비자) 계층 역시 아무런 병목 없이 독립적으로 
규모를 확장(Scale-out)하거나 축소할 수 있다.&lt;/p&gt;

&lt;p&gt;생산자들은 서로의 존재를 알 필요가 없으며, 그룹 단위의 조정이 전혀 필요하지 않다.&lt;/p&gt;

&lt;p&gt;트래픽이 몰려서 메시지를 더 빨리 발행해야 한다면, 그저 새로운 생산자 서버 인스턴스를 추가하기만 하면 된다.&lt;br /&gt;
반대로 트래픽이 줄면 서버를 그냥 삭제하면 된다.&lt;/p&gt;

&lt;p&gt;생산자는 메타데이터 저장소에서 사본 분산 계획만 읽어와 각자 브로커로 독립적으로 전송하기 때문에 수천 대의 생산자로 늘어나도 클러스터 전체 연산에 아무런 부담을 주지 않는다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;495-consumer의-확장성&quot;&gt;4.9.5. Consumer의 확장성&lt;/h3&gt;

&lt;p&gt;소비자 계층은 Consumer Group 이라는 단위 덕분에 구조적으로 우아하게 확장을 달성한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Consumer Group 간의 독립성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;서로 다른 서비스(예: 결제 팀 그룹, 푸시 팀 그룹)는 완전히 독립적이다.&lt;/li&gt;
      &lt;li&gt;토픽에 영향을 주지 않고 새로운 Consumer Group을 언제든지 자유롭게 추가하거나 삭제할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;그룹 내부의 동적 확장&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;동일한 Consumer Group 내에서 새로운 소비자 인스턴스를 추가하거나 삭제하는 경우, 혹은 특정 소비자 서버가 장애로 갑자기 제거되는 상황이 발생할 수 있다.&lt;/li&gt;
      &lt;li&gt;이 모든 변동 사항은 앞서 다룬 &lt;a href=&quot;#45-소비자-재조정consumer-rebalancing&quot;&gt;4.5. 소비자 재조정(Consumer rebalancing)&lt;/a&gt;이 백그라운드에서 완전히 자동으로 전담하여 
파티션 할당 선을 재배치하므로 개발자의 수동 개입 없이도 완벽한 Auto-Scaling 이 가능하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;410-메시지-전달-시맨틱과-오프셋-갱신-타이밍&quot;&gt;4.10. 메시지 전달 시맨틱과 오프셋 갱신 타이밍&lt;/h2&gt;

&lt;p&gt;분산 메시지 큐 시스템에서 가장 까다로운 문제 중 하나는 ‘장애가 발생했을 때 데이터의 유실이나 중복을 어떻게 막을 것인가?’ 이다.&lt;br /&gt;
&lt;a href=&quot;#데이터-처리와-오프셋-갱신-순서가-메시지-전송-시맨틱에-미치는-영향은&quot;&gt;💡데이터 처리와 오프셋 갱신 순서가 메시지 전송 시맨틱에 미치는 영향은?&lt;/a&gt;에서 살짝 보았듯이, 
소비자가 메시지를 읽어간 뒤 &lt;strong&gt;‘오프셋 확정 도장(Commit)’을 찍는 타이밍&lt;/strong&gt;에 따라 비즈니스의 신뢰성을 결정짓는 3가지 메시지 전달 보장 등급이 나뉜다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;4101-최대-한-번at-most-once&quot;&gt;4.10.1. 최대 한 번(At-most-once)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;유실은 감수하지만, 중복은 절대 허용하지 않는다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;메시지가 소비자에게 &lt;strong&gt;최대 한 번만 전달됨을 보장&lt;/strong&gt;하는 방식이다.&lt;br /&gt;
데이터가 유실될 수는 있어도 똑같은 데이터가 두 번 처리되는 일은 결코 없다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/at_most.png&quot; alt=&quot;최대 한 번&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[동작 순서]
1. 브로커로부터 메시지 수신 (오프셋: 10)
2. 오프셋을 11로 즉시 먼저 커밋 (수신 확인 완료)
3. 데이터베이스 저장 등 실제 비즈니스 로직(처리) 수행
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장애 시나리오&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Consumer가 오프셋을 11로 갱신(2단계)한 후, 실제 비즈니스 로직(3단계)를 수행하던 중 서버가 Crash 되었다.&lt;/li&gt;
      &lt;li&gt;서버가 복구된 후 상태 저장소에서 오프셋을 읽어오면 이미 11번으로 적혀있기 때문에, Consumer는 10번 메시지를 건너뛰고 11번부터 읽기 시작한다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;결과적으로 10번 메시지는 유실&lt;/strong&gt;된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;성능 오버헤드가 적고 중복 처리가 없어 가볍다.&lt;br /&gt;
하지만 데이터 유실 위험이 크기 때문에 대량의 로그 수집이나 실시간 메트릭 모니터링처럼 ‘중간에 한두 건 누락되어도 전체 비즈니스에 영향이 없는’ 도메인에서만 제한적으로 사용된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;4102-최소-한-번at-least-once&quot;&gt;4.10.2. 최소 한 번(At-least-once)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;중복은 감수하지만, 유실은 절대 허용하지 않는다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;메시지가 소비자에게 &lt;strong&gt;최소 한 번 이상은 반드시 전달됨을 보장&lt;/strong&gt;하는 방식이다.&lt;br /&gt;
데이터 유실은 절대 용납하지 않지만, 대신 똑같은 메시지가 중복 처리될 가능성은 있다.&lt;br /&gt;
&lt;strong&gt;대부분의 상용 분산 메시지 큐 시스템이 기본 모드로 채택하는 방식&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/at_least.png&quot; alt=&quot;최소 한 번&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[동작 순서]
1. 브로커로부터 메시지 수신 (오프셋: 10)
2. 데이터베이스 저장 등 실제 비즈니스 로직(처리) 수행 완료
3. 오프셋을 11로 최종 커밋 (나 다 끝냈어!)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장애 시나리오&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;소비자가 10번 메시지를 가져와 성공적으로 처리(2단계)했다.&lt;/li&gt;
      &lt;li&gt;오프셋을 11로 늘리려는 찰나(3단계) 서버가 Crash 되었다.&lt;/li&gt;
      &lt;li&gt;브로커는 오프셋 11 커밋 요청을 받지 못했으므로 상태 저장소에는 여전히 10번으로 기록되어 있다.&lt;/li&gt;
      &lt;li&gt;잠시 후 대체 투입된 새로운 소비자는 상태 저장소의 값대로 10번 메시지를 브로커에서 다시 Pull 해온다.&lt;/li&gt;
      &lt;li&gt;그리고 똑같은 데이터를 &lt;strong&gt;또 한번 처리&lt;/strong&gt;하게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;데이터가 절대 사라지지 않는다는 안정성을 준다.&lt;br /&gt;
대신 중복 데이터 처리에 대한 방어 로직이 필요하다.&lt;br /&gt;
쇼핑몰 주문 처리 등 데이터 누락이 치명적인 거의 모든 핵심 비즈니스 로직의 표준이다.&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;실무 해결책: 멱등성(Idempotency)&lt;/strong&gt;&amp;gt;&lt;br /&gt;
‘최소 한 번’ 모드에서 중복 결제 같은 사고를 막기 위해, Consumer 애플리케이션 레이어에 &lt;strong&gt;멱등성&lt;/strong&gt;을 반드시 확보해야 한다.&lt;br /&gt;
메시지 내부에 고유한 UUID를 심어두고, Consumer가 DB에 넣기 전에 이미 처리한 아이디인지 검증 등을 통해 걸러내는 작업이 병행되어야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;4103-정확히-한-번exactly-once&quot;&gt;4.10.3. 정확히 한 번(Exactly-once)&lt;/h3&gt;

&lt;p&gt;데이터의 &lt;strong&gt;유실도 없고, 중복도 없이 단 한 번만 정확하게 처리됨을 보장&lt;/strong&gt;하는 이상적인 등급이다.&lt;br /&gt;
모든 분산 시스템 엔지니어가 꿈꾸는 완벽한 신뢰성이지만, 구현 난이도가 매우 높고 시스템 자원 소모가 가장 크다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/once.png&quot; alt=&quot;정확히 한 번&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;구현 원리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;단순히 오프셋 갱신 타이밍만 조율해서는 달성이 불가능하다.&lt;/li&gt;
      &lt;li&gt;생산자-브로커-소비자로 이어지는 전체 파이프라인이 하나의 거대한 분산 트랜잭션으로 묶여야 한다.
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;Producer 멱등성&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;생산자가 메시지를 보낼 때 고유한 시퀀스 번호를 함께 보내어, 브로커가 중복 유입된 메시지를 디스크 기록 단계에서 원천 차단함&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;2단계 커밋(2-Phase Commit)&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;소비자가 메시지를 읽어와 외부 저장소(DB 등)에 데이터를 쓰고 오프셋을 커밋하는 과정을 ‘원자적(Atomic) 트랜잭션’으로 묶어버린다.&lt;/li&gt;
              &lt;li&gt;데이터 저장은 성공했는데 오프셋 커밋이 실패하면 전체 과정을 롤백하는 정교한 트랜잭션 코디네이터 프로토콜이 가동된다.&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;인프라 오버헤드가 크고 초당 처리 대역폭이 다소 저하되지만, 금융권의 계좌 이체 등 단 1원의 오차나 중복도 허용하지 않는 극도의 신뢰도가 요구되는 아키텍처의 종착지이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-고급-기능&quot;&gt;5. 고급 기능&lt;/h1&gt;

&lt;p&gt;기본적인 대역폭 확장과 고가용성 복제 아키텍처를 넘어, 엔터프라이즈 환경의 실제 워크로드에 분산 메시지 큐를 투입하려면 정교한 최적화 기술과 실무적인 요구사항(지연 전송, 필터링, 이력 보관)을 
수용할 수 있어야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;51-메시지-필터링&quot;&gt;5.1. 메시지 필터링&lt;/h2&gt;

&lt;p&gt;하나의 토픽에 수많은 메시지가 쌓일 때, 특정 소비자 그룹은 그 중 일부 카테고리의 메시지만 골라서 처리하고 싶어할 수 있다.&lt;br /&gt;
예) 주문 시스템은 토픽에 주문에 관련된 모든 이벤트를 전송하지만, 지불 시스템은 그 중 결제 이벤트만 관심이 있음&lt;/p&gt;

&lt;p&gt;지불 전용 토픽을 주문 토픽과 분리할 수도 있지만 그럼 아래와 같은 우려가 발생한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;나중에 비슷한 니즈가 생길 경우, 그 때마다 전용 토픽을 생성해야 함&lt;/li&gt;
  &lt;li&gt;같은 메시지를 여러 토픽에 저장하는 것은 자원 낭비&lt;/li&gt;
  &lt;li&gt;생산자-소비자 간의 결합도가 높아졌기 때문에 새로운 소비자 측 요구사항이 등장할 때마다 생산자 구현을 변경해야 함&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이를 해결하는 방식은 크게 3가지로 나뉜다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1)소비자 측 필터링(Client-side Filtering)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;소비자가 토픽의 모든 데이터를 무조건 당겨온 뒤, 애플리케이션 코드 단에서 걸러내는 방식이다.&lt;/p&gt;

&lt;p&gt;자신에게 필요없는 데이터까지 전부 네트워크로 전송받으므로 대량의 &lt;strong&gt;네트워크 대역폭 낭비&lt;/strong&gt;가 발생한다는 문제점이 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2)브로커 측 본문 파싱 필터링(Broker-side Full Parsing)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;브로커가 디스크에서 메시지를 읽은 뒤, 메시지 본문(Payload)을 일일이 압축 해제하고 역직렬화하여 조건에 맞는지 검사하는 방법이다.&lt;/p&gt;

&lt;p&gt;네트워크 대역폭은 아낄 수 있지만, 초당 수백만 건의 메시지를 열어보는 과정에서 &lt;strong&gt;브로커의 CPU 부하 때문에 병목&lt;/strong&gt;이 생긴다.&lt;br /&gt;
제로 카피 메커니즘도 완전히 파괴된다.&lt;/p&gt;

&lt;p&gt;또한 메시지에 민감한 데이터가 있다면 메시지 큐에서는 해당 메시지를 읽어서는 안 되므로, 브로커에서는 메시지의 payload를 추출하면 안 된다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;좀 더 자세한 내용은 &lt;a href=&quot;https://partners-intl.aliyun.com/help/en/doc-detail/29543.htm&quot;&gt;Filtering methods&lt;/a&gt;을 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/tag.png&quot; alt=&quot;태그 기반 메시지 필터링&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;3)메타데이터 태그(Tag) 기반 필터링(채택)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;메시지를 발행할 때 헤더 영역에 아주 작은 크기의 태그 문자열이나 바이트 플래그를 심어둔다.&lt;/p&gt;

&lt;p&gt;브로커는 암호화되거나 압축된 거대한 메시지 본문은 손대지 않고, &lt;strong&gt;고정된 스키마 크기를 가진 헤더의 태그 필드만 찰나의 속도로 스캔&lt;/strong&gt;한다.&lt;br /&gt;
조건에 맞지 않는 메시지는 버린다.&lt;/p&gt;

&lt;p&gt;네트워크 대역폭을 극적으로 아끼면서도 브로커 CPU 복호화 오버헤드를 0으로 유지하는, ‘제로 카피’와 ‘필터링’을 동시에 구현할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;52-메시지의-지연-전송-및-예약-전송&quot;&gt;5.2. 메시지의 지연 전송 및 예약 전송&lt;/h2&gt;

&lt;p&gt;‘결제 시도 후 30분 동안 응답이 없으면 취소 이벤트 발생’, ‘내일 아침 9시에 푸시 알림 메시지 발송’과 같은 요구사항은 매우 흔하게 발생한다.&lt;br /&gt;
하지만 순차적인 Append-only 로그(WAL) 파일 구조를 가진 메시지 큐에서 특정 메시지만 중간에 홀딩했다가 나중에 보내는 것은 구조적으로 불가능하다.&lt;br /&gt;
앞에 대기 중인 실시간 메시지들이 전부 막혀버리기 때문이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1)특별 임시 저장소 토픽의 활용&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;결제 완료 확인을 지시하는 메시지를 주문 시점에 바로 전송하되 큐 소비자에게는 30분 뒤에 전달되도록 해두면, 소비자는 메시지를 받았을 때 결제 여부만 확인하면 된다.&lt;/p&gt;

&lt;p&gt;이 문제를 해결하기 위해 시스템 내부에 비즈니스에 노출되지 않는 &lt;strong&gt;‘특수 목적용 지연 임시 토픽(예: SCHEDULE_TOPIC_XXX)’&lt;/strong&gt;을 개설한다.&lt;/p&gt;

&lt;p&gt;생산자가 ‘10분 뒤 전송’ 옵션을 걸어 메시지를 전달하면, 브로커는 이를 실제 목적지 토픽이 아닌 임시 저장소 토픽에 순차적으로 격리 보관한다.&lt;br /&gt;
클러스터 내부의 백그라운드 타이머 스레드가 이 임시 토픽을 상시 감시하다가, 지정된 지연 시간이 만료되는 타이밍에 메시지를 꺼내어 원래 가야 했던 실제 목적지 토픽으로 
배달해주는 아키텍처이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/latency.png&quot; alt=&quot;메시지 지연 전송&quot; /&gt;&lt;/p&gt;

&lt;p&gt;메시지 예약 전송은 지정된 시간에 소비자에게 메시지를 보낼 수 있게 하는 기능으로, 시스템 설계 철학은 메시지 지연 전송 시스템과 유사하다.&lt;br /&gt;
예약 전송은 ‘특정 미래 시각에 보내줘’, 지연 전송은 ‘지금으로부터 10분 뒤에 보내줘’ 이고 시스템은 오직 &lt;strong&gt;현재 시각이 목표 시각 이상이 되었는가?&lt;/strong&gt;라는 단일 조건문만 
바라보고 동작한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-최종-요약&quot;&gt;6. 최종 요약&lt;/h1&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0612/final.png&quot; alt=&quot;mind map&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬, 산 람 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000211656186&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links_vol2.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Apache_ZooKeeper&quot;&gt;Apache ZooKeeper 공식 가이드 (위키피디아)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Advanced_Message_Queuing_Protocol&quot;&gt;AMQP(Advanced Message Queuing Protocol) 표준 명세&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://kafka.apache.org/43/getting-started/introduction/&quot;&gt;Apache Kafka 공식 가이드: 시작하기 및 소개&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://kafka.apache.org/43/design/protocol/&quot;&gt;Apache Kafka 공식 가이드: 디자인 및 프로토콜 명세&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://kafka.apache.org/20/configuration/consumer-configs/&quot;&gt;Apache Kafka 공식 설정: 컨슈머(Consumer) 클라이언트 옵션 가이드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://towardsdatascience.com/kafka-no-longer-requires-zookeeper-ebfbf3862104/&quot;&gt;Towards Data Science: 카프카가 주키퍼를 버리고 브로커 자체 저장(KRaft)으로 전환한 이유&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.confluent.io/blog/hands-free-kafka-replication-a-lesson-in-operational-simplicity/&quot;&gt;Confluent Tech Blog: 운영 편의성을 극대화하는 핸즈프리 카프카 복제 기술&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://rongxinblog.wordpress.com/2016/07/29/kafka-high-watermark/&quot;&gt;Rongxin’s Tech Blog: 카프카 High Watermark(HW) 구조와 일관성 제어 원리&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27846330&quot;&gt;Apache Wiki: 데이터 센터 간 비동기 복제를 위한 MirrorMaker 설계&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://partners-intl.aliyun.com/help/en/doc-detail/29543.htm&quot;&gt;Alibaba Cloud Docs: RocketMQ 브로커 부하를 줄이는 메시지 필터링 기법과 태그 활용&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/06/05/architecture-nearby/#242-%EC%9C%84%EC%B9%98-%EC%9D%B4%EB%8F%99-%EC%9D%B4%EB%A0%A5-dbcassandra&quot;&gt;Assu’s DevLog: LSM 트리 자료 구조와 대규모 쓰기 성능 최적화의 비밀&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assu10.github.io/dev/2024/07/13/kafka-mechanism-1/#42-%EC%9D%BD%EA%B8%B0-%EC%9A%94%EC%B2%AD&quot;&gt;Assu’s DevLog: 리눅스 sendfile 시스템 콜을 활용한 카프카 제로 카피(Zero-Copy) 메커니즘&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.rabbitmq.com/docs/maxlength&quot;&gt;RabbitMQ 가이드: 대기열 효율을 제어하는 큐 길이 제한(Max Length Limits) 정책&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Fri, 12 Jun 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/06/12/architecture-distributed-message-queue-architecture/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/06/12/architecture-distributed-message-queue-architecture/</guid>
        
        <category>architecture</category>
        
        <category>message-queue</category>
        
        <category>kafka</category>
        
        <category>event-streaming</category>
        
        <category>system-design</category>
        
        <category>scalability</category>
        
        <category>isr</category>
        
        <category>rebalancing</category>
        
        <category>분산시스템</category>
        
        <category>대규모아키텍처</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(1) - 대규모 아키텍처 설계 핵심 12가지 체크리스트</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-단일-서버-아키텍처&quot;&gt;1. 단일 서버 아키텍처&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-db-rdbms-vs-nosql&quot;&gt;2. DB: RDBMS vs NoSQL&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-scale-out-vs-scale-up&quot;&gt;3. Scale-out vs Scale-up&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-웹-계층의-부하-분산-load-balancer와-private-ip&quot;&gt;4. 웹 계층의 부하 분산: Load Balancer와 Private IP&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-데이터-계층의-안정성-확보-데이터베이스-다중화&quot;&gt;5. 데이터 계층의 안정성 확보: 데이터베이스 다중화&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-latency-줄이기-cache&quot;&gt;6. Latency 줄이기: Cache&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#deep-dive-캐시-전략-5가지-비교&quot;&gt;🚀Deep Dive: 캐시 전략 5가지 비교&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-cdncontent-delivery-network&quot;&gt;7. CDN(Content Delivery Network)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#deep-dive-동적-콘텐츠-캐싱&quot;&gt;🚀Deep Dive: 동적 콘텐츠 캐싱&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#8-stateless-웹-계층&quot;&gt;8. Stateless 웹 계층&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#deep-dive-aws-elb-sticky-session-의-한계&quot;&gt;🚀Deep Dive: AWS ELB Sticky Session 의 한계&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#9-다중-데이터-센터와-지리적-라우팅geodns&quot;&gt;9. 다중 데이터 센터와 지리적 라우팅(GeoDNS)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#deep-dive-netflix-로-보는-active-active-다중-리전-복제-전략과-데이터-동기화&quot;&gt;🚀Deep Dive: Netflix 로 보는 Active-Active 다중 리전 복제 전략과 데이터 동기화&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#10-시스템-간-결합도-낮추기-메시지-큐&quot;&gt;10. 시스템 간 결합도 낮추기: 메시지 큐&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#11-로그-메트릭-모니터링-그리고-자동화&quot;&gt;11. 로그, 메트릭 모니터링 그리고 자동화&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#12-db-샤딩-도입&quot;&gt;12. DB 샤딩 도입&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#121-데이터의-재샤딩resharding&quot;&gt;12.1. 데이터의 재샤딩(Resharding)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#122-유명인사-문제celebrity&quot;&gt;12.2. 유명인사 문제(Celebrity)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#123-조인과-비정규화&quot;&gt;12.3. 조인과 비정규화&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#요약&quot;&gt;요약&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-단일-서버-아키텍처&quot;&gt;1. 단일 서버 아키텍처&lt;/h1&gt;

&lt;p&gt;단일 서버 아키텍처는 웹 애플리케이션, DB, 캐시 등 서비스 운영에 필요한 모든 컴포넌트를 단 한 대의 서버에서 모두 실행하는 가장 단순한 형태의 시스템 구조이다.&lt;br /&gt;
트래픽이 적은 초기 스타트업이나 토이 프로젝트에서 주로 사용한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/1.png&quot; alt=&quot;단일 서버 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① DNS 질의(Domain Name Resolution):&lt;/strong&gt; 사용자는 도메인 이름(api.mysite.com)을 브라우저 주소창에 입력한다. 클라이언트는 이 도메인을 가진 서버의 실제 주소를 찾기 위해 DNS(Domain Name Service) 서버에 질의한다.&lt;br /&gt;
&lt;strong&gt;② IP 주소 반환:&lt;/strong&gt; DNS 서버는 도메인 이름을 확인한 후, 그에 매핑된 웹 서버의 실제 IP 주소를 클라이언트로 반환한다.&lt;br /&gt;
&lt;strong&gt;③ HTTP 요청 송신:&lt;/strong&gt; IP 주소를 확보한 클라이언트는 해당 웹 서버를 향해 직접 HTTP 요청을 보낸다.&lt;br /&gt;
&lt;strong&gt;④ 응답 반환:&lt;/strong&gt; 요청을 수신한 웹 서버는 처리 후 HTTP 페이지나 데이터를 담아 JSON 형태로 응답한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️단일 서버의 한계&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;SPOF(Single Point of Failure, 단일 장애 지점):&lt;/strong&gt; 서버 한 대에 모든 것이 의존하므로, 서버가 다운되면 전체 서비스가 마비된다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;자원의 한계:&lt;/strong&gt; 동시 접속자 수가 늘어나면 CPU, 메모리, 디스크 I/O 병목이 발생하여 급격한 성능 저하가 발생한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-db-rdbms-vs-nosql&quot;&gt;2. DB: RDBMS vs NoSQL&lt;/h1&gt;

&lt;p&gt;대규모 아키텍처를 설계할 때 핵심은 비즈니스 데이터의 특성에 맞춰 RDBMS를 사용할지, 혹은 비관계형 데이터베이스(NoSQL)을 사용할지 결정하는 것이다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt; &lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;RDBMS&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;NoSQL&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;데이터 구조&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;엄격한 스키마, 테이블 기반(Row, Column)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;유연한 스키마, 다양한 데이터 모델&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;특징&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;SQL 활용, 고성능 JOIN 연산 지원, ACID 트랜잭션 보장&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;JOIN 없음, Scale-out이 매우 용이함&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;대표 기술&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;MySQL, Oracle, PostgreSQL, MariaDB&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Redis, MongoDB, Cassandra&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/2.png&quot; alt=&quot;DB 서버가 추가된 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;p&gt;NoSQL은 4가지 분류로 나눌 수 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Key-Value 저장소&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;고유한 키와 값의 쌍으로 데이터 저장&lt;/li&gt;
      &lt;li&gt;읽기/쓰기 속도가 극도로 빠름&lt;/li&gt;
      &lt;li&gt;예) Redis, DynamoDB&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Document 저장소&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;JSON이나 XML 같은 계층적인 문서 형태로 데이터 저장&lt;/li&gt;
      &lt;li&gt;데이터 구조가 유연하게 바뀔 때 유리함&lt;/li&gt;
      &lt;li&gt;예) MongoDB, CouchDB&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Column 저장소&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;행(Row)이 아닌 열(Column) 단위로 데이터를 묶어서 저장&lt;/li&gt;
      &lt;li&gt;대규모 데이터 분석 및 대량의 쓰기 연산에 특화&lt;/li&gt;
      &lt;li&gt;예) Cassandra, HBase&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Graph 저장소&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;노드(Node)와 간선(Edge)으로 데이터 간의 ‘관계’를 지도처럼 저장&lt;/li&gt;
      &lt;li&gt;SNS의 친구 관계나 추천 시스템에 필수적&lt;/li&gt;
      &lt;li&gt;예) Neo4j&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NoSQL은 일반적으로 join 연산은 지원하지 않는다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;❓NoSQL은 어떤 경우에 사용해야 할까?&lt;/strong&gt;&lt;br /&gt;
대규모 아키텍처 설계 관점에서 NoSQL은 일반적으로 아래와 같은 요구사항이 있을 때 RDBMS 대신, 혹은 RDBMS와 함께(Polyglot) 도입한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;초저지연(Low Latency)이 요구될 때:&lt;/strong&gt; 복잡한 관계 처리가 필요 없고, ms 단위의 빠른 응답 속도가 중요할 때 유리&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;비정형/반정형 데이터를 다룰 때:&lt;/strong&gt; 정해진 양식 없이 자주 스키마가 변경되거나 직렬화된 객체를 그대로 저장해야 할 때&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;방대한 대용량 데이터를 저장해야 할 때:&lt;/strong&gt; RDBMS가 감당하기 힘든 TB, PB 급의 데이터를 저비용으로 확장하며 저장해야 할 때&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-scale-out-vs-scale-up&quot;&gt;3. Scale-out vs Scale-up&lt;/h1&gt;

&lt;p&gt;서버로 유입되는 대규모 트래픽과 성능 병목을 해결하는 확장 전략은 크게 Scale-up과 Scale-out 으로 나뉜다.&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;Scale-up의 장단점&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;설계가 &lt;strong&gt;매우 단순&lt;/strong&gt;함. 애플리케이션 코드를 수정하거나 복잡한 분산 아키텍처를 도입할 필요 없이 하드웨어만 교체하면 성능이 즉시 향상됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;비용의 비선형적 증가:&lt;/strong&gt; 고성능 장비로 갈수록 가격이 기하급수적으로 비싸져 비용 효율성이 떨어짐&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;장애 복구(Failover) 불가:&lt;/strong&gt; 여전히 ‘단일 서버’ 구조이므로, 해당 장비에 장애가 발생하면 전체 서비스가 다운되는 위험(SPOF)을 해결하지 못함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;Scale-out의 장단점&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;무한한 확장성:&lt;/strong&gt; 이론적으로 서버를 수십, 수백 대 계속 추가할 수 있어 확장의 한계가 없음&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;고가용성 확보:&lt;/strong&gt; 특정 서버 한 대가 다운되더라도 다른 서버들이 트래픽을 나누어 받으므로 서비스가 중단되지 않고 자동 복구(failover) 설계가 가능해짐&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;복잡성 증가. 여러 서버로 트래픽을 고루 나눠주기 위한 &lt;strong&gt;로드밸런서&lt;/strong&gt; 도입이 필수적이며, 데이터의 일관성을 유지하기 위한 분산 시스템 설계가 요구됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-웹-계층의-부하-분산-load-balancer와-private-ip&quot;&gt;4. 웹 계층의 부하 분산: Load Balancer와 Private IP&lt;/h1&gt;

&lt;p&gt;로드밸런서는 서버로 유입되는 대규모 트래픽을 여러 대의 웹 서버로 골고루 분산해 주는 부하 분산 장치이다.&lt;br /&gt;
웹 계층을 Scale-out할 때 클라이언트의 요청을 최전선에서 맞이하는 핵심 컴포넌트이다.&lt;/p&gt;

&lt;p&gt;로드밸런서가 도입되면 보안성이 극대화되는데 그 중심에는 로드밸런서의 Public IP(예: 88.88.88.1)와 Private IP가 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/3.png&quot; alt=&quot;로드밸런서가 추가된 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Public IP(인터넷 공개 주소):&lt;/strong&gt; 클라이언트는 로드밸런서의 Public IP로만 접속한다. 웹 서버의 실제 주소는 외부 인터넷에 절대 노출되지 않는다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Private IP(내부 네트워크 주소):&lt;/strong&gt; 로드밸런서 뒷단에 위치한 웹 서버들은 동일한 로컬 네트워크 안에서만 통신할 수 있는 Private IP를 부여받는다. 인터넷을 통해서는 
이 주소로 직접 접근할 수 없다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-데이터-계층의-안정성-확보-데이터베이스-다중화&quot;&gt;5. 데이터 계층의 안정성 확보: 데이터베이스 다중화&lt;/h1&gt;

&lt;p&gt;데이터베이스 다중화란 데이터의 원본을 보유한 Primary와 복제된 사본을 보유한 Replica를 분리하여 데이터를 실시간 동기화하는 기술이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔄Read와 Write의 철저한 분업&lt;/strong&gt;&lt;br /&gt;
통상적인 웹 서비스는 Write 연산보다 Read 연산의 비중이 훨씬 높다.&lt;br /&gt;
따라서 Primary DB 서버 한 대에 여러 대의 Replica DB 서버를 붙이는 구조가 일반적이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/4.png&quot; alt=&quot;master DB의 수보다 slave DB의 수가 많음&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;DB 다중화의 장점&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;더 나은 성능:&lt;/strong&gt; 모든 쓰기는 Primary로, 읽기는 여러 Replica로 분산 처리되므로 병렬 질의 속도가 획기적으로 빨라짐&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;안정성:&lt;/strong&gt; 데이터센터 일부가 손상되더라도 여러 지역에 사본(Replica)이 보존되어 있으므로 데이터 유실 없음&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;고가용성:&lt;/strong&gt; Primary 서버가 죽으면 Replica 중 하나가 새로운 Primary DB로 승격(Failover)되어 중단 없는 서비스 제공 가능&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/5.png&quot; alt=&quot;로드밸런서와 데이터베이스 다중화 적용&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-latency-줄이기-cache&quot;&gt;6. Latency 줄이기: Cache&lt;/h1&gt;

&lt;p&gt;캐시는 처리 속도가 느린 DB를 매번 호출하는 대신, 상대적으로 연산 비용이 비싸거나 자주 참조되는 데이터를 &lt;strong&gt;메모리(RAM) 기반의 고속 저장소&lt;/strong&gt;에 임시로 두고 
요청을 빠르게 처리하는 컴포넌트이다.&lt;/p&gt;

&lt;p&gt;애플리케이션은 읽기 요청을 처리할 때 DB로 가기 전 먼저 캐시를 확인하는 캐시 우선 읽기 전략(Look-Aside / Cache-Aside)을 주로 사용한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Cache Hit:&lt;/strong&gt; 찾으려는 데이터가 캐시에 있는 경우, DB를 거치지 않고 즉시 데이터 반환&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cache Miss:&lt;/strong&gt; 캐시에 데이터가 없는 경우, DB에서 데이터를 직접 조회한 뒤 이를 캐시에 적재하고 클라이언트에 반환&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;🧹캐시 지우기 정책(Eviction)&lt;/strong&gt;&lt;br /&gt;
메모리 공간은 유한하기 때문에 캐시가 가득 차면 어떤 데이터를 버리고 새 데이터를 넣을지 결정해야 하는데 이를 &lt;strong&gt;캐시 지우기 정책&lt;/strong&gt;이라고 한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;LRU(Least Recently Used):&lt;/strong&gt; 마지막으로 사용된 지 &lt;strong&gt;가장 오래된 데이터&lt;/strong&gt;를 우선 삭제하는 정책(가장 널리 쓰임)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;LFU(Least Frequently Used):&lt;/strong&gt; 지금까지 사용된 &lt;strong&gt;빈도가 가장 낮은 데이터&lt;/strong&gt;를 우선 삭제하는 정책&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;FIFO(First In, First Out):&lt;/strong&gt; &lt;strong&gt;가장 먼저 들어온 순서대로&lt;/strong&gt; 데이터를 삭제하는 정책&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;deep-dive-캐시-전략-5가지-비교&quot;&gt;🚀Deep Dive: 캐시 전략 5가지 비교&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://codeahoy.com/2017/08/11/caching-strategies-and-how-to-choose-the-right-one/&quot;&gt;Caching Strategies and How to Choose the Right One&lt;/a&gt; 에 따르면 
시스템의 쓰기/읽기 패턴에 맞는 정확한 캐싱 전략을 선택해야만 정합성 문제와 Latency를 모두 잡을 수 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Cache Aside(Lazy Loading)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;작동 방식:&lt;/strong&gt; 앱이 캐시/DB 직접 제어, 애플리케이션이 캐시를 먼저 보고, 없으면(Miss) DB에서 가져와 직접 채워넣는 가장 일반적인 방식&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; 캐시 서버가 다운되어도 서비스가 중단되지 않고 DB로 직접 붙을 수 있어 복구 탄력성(Resilient)이 좋음&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; DB 데이터가 수정되었을 때 캐시 데이터와 불일치가 발생할 수 있으므로, TTL 설정 필수&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;데이터 변경은 적고 읽기 부하가 크고&lt;/strong&gt;, 캐시 장애가 서비스 중단으로 이어지면 안될 때 사용
        &lt;ul&gt;
          &lt;li&gt;예) 상품 카탈로그&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Read-Through&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;작동 방식:&lt;/strong&gt; 캐시가 DB 조회 대행, 애플리케이션은 오직 캐시 시스템하고만 통신함. 데이터가 없으면 캐시 라이브러리나 프로바이더가 내부적으로 직접 DB에서 데이터를 조회하여 캐시 갱신&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; 애플리케이션 코드가 단순해지고 캐시 채우기 책임이 분리됨&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; 캐시와 DB의 데이터 모델 구조가 항상 동일해야 하는 제약이 있음&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;동일한 데이터의 반복적인 읽기&lt;/strong&gt;가 주를 이루고, 코드를 깔끔하게 유지할 때 사용
        &lt;ul&gt;
          &lt;li&gt;예) 실시간 트렌드 피드&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Write-Through&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;작동 방식:&lt;/strong&gt; 캐시와 DB 동시 저장, 데이터를 저장할 때 &lt;strong&gt;캐시와 DB 두 곳에 동시&lt;/strong&gt;에 데이터를 기록&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; 캐시의 데이터가 항상 최신 상태를 유지하므로 &lt;strong&gt;데이터 정합성이 완벽하게 보장&lt;/strong&gt;됨&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; 매번 두 군데에 쓰기 연산을 수행하므로 쓰기 지연 시간이 늘어남. 읽기가 드문 데이터까지 캐싱되어 리소스가 낭비될 수 있음&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;데이터 유실이나 불일치가 절대 용인되지 않는&lt;/strong&gt; 중요 정보 처리 시 사용
        &lt;ul&gt;
          &lt;li&gt;예) 계좌 잔액, 실시간 상품 재고, 유저의 결제 상태 변경&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Write-Around&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;작동 방식:&lt;/strong&gt; DB에만 저장(캐시 우회), 모든 데이터는 캐시를 거치지 않고 &lt;strong&gt;DB에만 직접 기록&lt;/strong&gt;됨. 캐시는 오직 읽기 시 Cache Miss가 발생할 때만 채워짐&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; 한 번 저장된 후 자주 읽히지 않는 데이터를 처리할 때 캐시 공간을 무모하게 오염시키지 않음&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; 방금 쓴 데이터를 바로 읽으려고 할 때 캐시 미스가 발생하여 초기 지연이 생길 수 있음&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;한 번 쓰인 후 거의 읽히지 않는&lt;/strong&gt; 대량의 데이터 처리 시 사용
        &lt;ul&gt;
          &lt;li&gt;예) 시스템 로그, 과거 결제 내역, 보안 이벤트 기록&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Write-Back(Write-Behind)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;작동 방식:&lt;/strong&gt; 캐시에 선저장 후 DB 저장, 데이터를 &lt;strong&gt;캐시에만 먼저 초고속으로 저장&lt;/strong&gt;한 뒤, 일정 주기나 데이터가 모였을 때 &lt;strong&gt;DB에 비동기 배치로 일괄 반영&lt;/strong&gt;하는 방식&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;장점:&lt;/strong&gt; 쓰기 연산이 극단적으로 많은 서비스(대규모 이벤트 로그 수집, 게임 실시간 위치 정보 등)에서 &lt;strong&gt;최고의 쓰기 성능&lt;/strong&gt;을 냄. DB 부하를 획기적으로 줄여줌&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점:&lt;/strong&gt; 캐시 서버가 배치를 반영하기 전에 갑자기 다운되면 메모리에만 있던 &lt;strong&gt;데이터가 영구 유실될 위험&lt;/strong&gt; 존재&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;쓰기 트래픽이 폭발적&lt;/strong&gt;이어서 DB 병목을 반드시 해결해야 할 때 사용
        &lt;ul&gt;
          &lt;li&gt;예) 실시간 게임의 유저 좌표 데이터, SNS 게시글의 실시간 좋아요 수&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write-Around는 오직 데이터를 저장할 때 트래픽이 어디로 흐르는지만 정의한 ‘쓰기 전용 정책’이다.&lt;br /&gt;
협업에서는 Cache-Aside 패턴을 기본 아키텍처로 잡고, 쓰기 전략은 캐시를 우회하는 Write-Around 방식을 조합해서 사용한다.&lt;/p&gt;

&lt;p&gt;새로운 데이터를 저장할 때 Cache-Aside, Write-Around 모두 DB 에만 저장한다.&lt;br /&gt;
하지만 기존 데이터를 수정할 때 &lt;strong&gt;Cache-Aside는 DB 수정 후 캐시 데이터를 강제 삭제&lt;/strong&gt;하지만 &lt;strong&gt;Write-Around는 DB만 수정하고 캐시는 방치&lt;/strong&gt;한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;7-cdncontent-delivery-network&quot;&gt;7. CDN(Content Delivery Network)&lt;/h1&gt;

&lt;p&gt;CDN은 전 세계에 지리적으로 분산된 서버 네트워크를 활용하여, 이미지/비디오/CSS/JavaScript 등 변경이 잦지 않은 정적 콘텐츠를 사용자와 가까운 Edge 서버에서 
빠르게 제공하는 시스템이다. 이를 통해 원본 서버의 부하를 획기적으로 줄이고 대역폭 비용을 절감할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/6.png&quot; alt=&quot;CDN 동작&quot; /&gt;&lt;/p&gt;

&lt;p&gt;CDN 서버에 데이터가 있느냐 없느냐에 따라 트래픽은 다음과 같이 흐른다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;최초 요청 및 Cache Miss&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자 A가 CDN 도메인이 적용된 이미지 URL(image.png)에 접근&lt;/li&gt;
      &lt;li&gt;CDN 에지 서버에 해당 이미지가 없다면(Cache Miss), CDN 서버가 직접 원본 서버에 해당 파일을 요청&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;캐싱 및 응답&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;원본 서버가 파일을 CDN 서버로 반환할 때, HTTP 헤더에 해당 파일의 유효 기간을 명시하는 &lt;strong&gt;TTL&lt;/strong&gt; 값을 함께 보냄&lt;/li&gt;
      &lt;li&gt;CDN 서버는 이 파일을 메모리/디스크에 캐시하고 사용자 A에게 최종 반환함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cache Hit를 통한 초고속 반환&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이후 다른 사용자 B가 동일한 이미지에 접근하면, CDN 서버는 만료되지 않은 캐시 데이터를 확인하고 원본 서버를 거치지 않고 즉시 반환(Cache Hit)함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;CDN 도입 시 반드시 고려해야 할 사항들&lt;/strong&gt;&amp;gt;&lt;/p&gt;

&lt;p&gt;CDN은 강력한 도구이지만 실제 사용할 때는 비용과 효율성 관점에서 아래 5가지 요소를 반드시 따져봐야 한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;데이터 전송량 기반의 비용 산정&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;CDN은 보통 서드파티(Cloudflare, Akamai 등) 서비스로 운영되며, 에지 서버로 들어오고 나가는 데이터 전송량(Data Transfer Output)에 따라 요금이 부과된다.&lt;/li&gt;
      &lt;li&gt;트래픽이 폭발할 때 예상치 못한 비용 폭탄을 맞지 않도록 트래픽 패턴을 미리 예측해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;롱테일 콘텐츠 캐싱의 비효율성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;자주 쓰이지 않는 콘텐츠까지 캐싱하는 것은 비용 대비 이득이 크지 않다.&lt;/li&gt;
      &lt;li&gt;캐시 히트율(Cache Hit Ratio)이 낮다면 CDN에서 제외하고 원본 서버에서 직접 처리하는 것이 경제적이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;적절한 TTL 설정&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;콘텐츠의 성격에 맞춰 만료 시간을 정교하게 세팅해야 한다.&lt;/li&gt;
      &lt;li&gt;너무 길면 실시간 데이터 반영이 늦어지고, 너무 짧으면 캐시 미스가 자주 발생해 원본 서버가 부하를 고스란히 받게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;원본 서버 직접 복구 대처 방안&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;만일 전 세계 CDN 에지 서버가 일시적으로 먹통이 된다면 CDN 장애를 감지하는 즉시 클라이언트(웹/앱)가 원본 서버로부터 직접 콘텐츠를 다운로드하도록 우회 경로를 구성(fallback)하는 방안이 아키텍처에 반영되어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;캐시를 강제로 비우는 콘텐츠 무효화(Invalidation) 방법&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수정된 데이터를 즉시 전 세계에 반영해야 할 때, 아래 두 가지 무효화 전략 중 비즈니스 상황에 맞는 방식을 택한다.
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;CDN Provider API 활용:&lt;/strong&gt; 서비스 사업자가 제공하는 퍼지(Purge/Invalidation) API를 호출하여 에지 서버의 캐시를 강제로 삭제&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;오브젝트 버저닝(Object Versioning):&lt;/strong&gt; URL 뒤에 버전 query string을 붙이거나 파일명 자체를 바꾼다. 예) image.png?v=2&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/7.png&quot; alt=&quot;CDN과 캐시가 추가된 설계&quot; /&gt;&lt;/p&gt;

&lt;p&gt;정적 콘텐츠(js, css, 이미지 등)은 더 이상 웹 서버를 통해 서비스하지 않으며, CDN을 통해 제공하여 더 나은 성능을 보장한다.&lt;br /&gt;
그리고 캐시가 데이터베이스 부하를 줄여준다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;deep-dive-동적-콘텐츠-캐싱&quot;&gt;🚀Deep Dive: 동적 콘텐츠 캐싱&lt;/h2&gt;

&lt;p&gt;정적 파일만 캐싱하는 줄 알았던 CDN이 어떻게 매번 변하는 동적 콘텐츠(Dynamic Content)까지 최적화할 수 있을까?&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://aws.amazon.com/ko/cloudfront/dynamic-content/&quot;&gt;AWS CloudFront&lt;/a&gt;같은 최신 CDN 솔루션은 단순히 파일을 저장하는 것을 넘어, 캐싱할 수 없는 
API 요청이나 가변적 HTML 페이지 같은 &lt;strong&gt;동적 콘텐츠 전달 속도&lt;/strong&gt;도 획기적으로 개선한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;연결 최적화(Persistent Connection)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;네트워크 통신에서 가장 오랜 시간이 걸리는 작업 중 하나는 TCP Handshake와 TLS/SSL 암호화 연결을 맺는 과정이다.&lt;/li&gt;
      &lt;li&gt;CloudFront는 전 세계 사용자 근처의 에지 로케이션과 AWS 내부 원본 서버 간의 전용 네트워크 연결을 항상 ‘최신 유지(Keep Alive)’ 상태로 맺어준다.&lt;/li&gt;
      &lt;li&gt;사용자는 근처 에지까지만 빠르게 연결하면 되므로 전체 지연 시간이 급격히 줄어든다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;최적의 네트워크 경로 라우팅(Routing Optimization)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;일반 인터넷 망은 트래픽 혼잡도에 따라 데이터가 빙빙 돌아가기 일쑤이다.&lt;/li&gt;
      &lt;li&gt;CloudFront는 가공되지 않은 동적 요청을 수신하면, 공용 인터넷 망이 아닌 AWS가 보유한 글로벌 사설 전용 광섬유 네트워크 아키텍처를 통해 원본 서버까지 가장 빠르고 안정적인 최적의 경로로 뚫고 지나간다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;에지 단에서의 스마트 처리(Edge Computing)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;AWS CloudFront Functions나 Lambda@Edge를 활용하면 query string, cookie, request header의 조건에 따라 사용자 정의 HTTP 응답을 에지 단에서 직접 가공하여 반환할 수 있어 원본 서버가 아예 연산할 필요 조차 없게 만든다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;8-stateless-웹-계층&quot;&gt;8. Stateless 웹 계층&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Stateless 웹 계층&lt;/strong&gt;이란 웹 서버가 클라이언트의 상태 정보나 세션 데이터를 자신의 로컬 메모리나 디스크에 저장하지 않는 아키텍처를 말한다.&lt;br /&gt;
모든 요청이 완전히 독립적으로 처리되므로, 사용자는 로드밸런서 뒷단의 어떤 웹 서버로 접속하더라도 항상 중단 없이 서비스를 이용할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/8.png&quot; alt=&quot;Stateful 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚨Stateful 아키텍처의 한계&lt;/strong&gt;&lt;br /&gt;
Stateful 아키텍처에서는 특정 사용자의 세션 정보가 ‘서버 1’의 내부 메모리에 종속된다. 이 경우 사용자 A의 다음 요청은 무조건 서버 1로만 가야 인증이 유지된다.&lt;/p&gt;

&lt;p&gt;이를 해결하기 위해 로드밸런서가 특정 사용자의 요청을 지정된 서버로만 보내주는 &lt;strong&gt;Sticky Session&lt;/strong&gt; 기능을 제공하지만, 이는 대규모 시스템에서 치명적인 문제를 발생시킨다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;deep-dive-aws-elb-sticky-session-의-한계&quot;&gt;🚀Deep Dive: AWS ELB Sticky Session 의 한계&lt;/h2&gt;

&lt;p&gt;AWS의 대표적인 로드밸런서인 ELB(Elastic Load Balancing)에서 &lt;a href=&quot;https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-sticky-sessions.html&quot;&gt;Sticky Session&lt;/a&gt;을 
활성화하면 Cookie 기반으로 특정 인스턴스에 트래픽을 묶어둘 수 있어 편리해 보이지만, 규모 확장성 관점에서는 아래와 같은 한계에 부딪힌다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;부하 불균형&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 무거운 작업을 수행하는 헤비 유저들이 하나의 서버에 Sticky하게 몰릴 경우, 로드밸런서가 아무리 라운드 로빈으로 트래픽을 쪼개려 해도 특정 서버만 과부하가 걸리는 병목 현상이 일어남&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Auto-Scaling 유연성 상실&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;트래픽이 몰려 자동으로 Scale-out을 하거나, 한가해서 Scale-in을 할 때 인스턴스가 쉽게 제거되지 못함&lt;/li&gt;
      &lt;li&gt;서버를 끄는 순간 해당 서버에 Sticky하게 묶여있던 수많은 유저의 세션 데이터가 사라지기 때문임&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;이런 문제를 해결하기 위해서는 세션 정보를 웹 서버 내부가 아닌 물리적으로 분리된 외부 공유 저장소로 일원화해야 한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/9.png&quot; alt=&quot;Stateless 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 구조에서 웹 서버는 상태 정보가 필요한 경우 공유 저장소로부터 데이터를 가져오기 때문에 사용자로부터 HTTP 요청은 어떤 웹서버로도 전달될 수 있다.&lt;br /&gt;
이 공유 저장소는 RDBMS일 수도 있고 Memcached/Redis 와 같은 캐시 시스템일 수도 있다.&lt;/p&gt;

&lt;p&gt;아래는 Stateless 웹 계층을 갖도록 기존 설계를 변경한 것이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/10.png&quot; alt=&quot;Stateless 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;9-다중-데이터-센터와-지리적-라우팅geodns&quot;&gt;9. 다중 데이터 센터와 지리적 라우팅(GeoDNS)&lt;/h1&gt;

&lt;p&gt;다중 데이터 센터 아키텍처는 시스템의 가용성을 극대화하고 글로벌 사용자에게 최상의 속도를 제공하기 위해, 전 세계 여러 물리적 거점에 동일한 세트의 웹 서버와 데이터 계층 인프라를 
동시에 구축하여 운영하는 고도화된 전략이다.&lt;br /&gt;
만일 데이터 센터 중 한 곳에 지진이나 재앙이 발생하면, 모든 트래픽은 장애가 없는 정상 데이터 센터로 즉시 자동 우회된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/11.png&quot; alt=&quot;데이터센터&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;⚠️다중 데이터 센터 구축을 위한 2가지 기술적 난제&lt;/strong&gt;&lt;br /&gt;
다중 데이터 센터 아키텍처가 주는 고가용성을 완벽하게 누리려면 반드시 해결해야 하는 핵심 난제 2가지가 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;트래픽 우회(Traffic Rerouting)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자의 요청을 가장 효과적인 데이터 센터로 보내는 방법을 찾아야 한다.&lt;/li&gt;
      &lt;li&gt;이 난제를 해결하는 보편적인 기술이 지리적 라우팅(GeoDNS)이다.&lt;/li&gt;
      &lt;li&gt;GeoDNS는 사용자의 IP 주소를 기반으로 현재 위치를 실시간 계산하여, 물리적으로 가장 가까운 데이터 센터의 IP 주소로 트래픽을 안내한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 동기화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;만약 데이터 센터마다 서로 독립된 DB를 사용한다고 했을 때 동부 센터에 장애가 발생하여 서부 센터로 트래픽이 우회되었을 때, 서부 센터에는 사용자가 
동부에서 작성했던 데이터가 존재하지 않을 수 있다. 이 문제를 막기 위한 보편적인 전략은 데이터를 여러 데이터 센터에 걸쳐 실시간으로 다중화하는 것이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;deep-dive-netflix-로-보는-active-active-다중-리전-복제-전략과-데이터-동기화&quot;&gt;🚀Deep Dive: Netflix 로 보는 Active-Active 다중 리전 복제 전략과 데이터 동기화&lt;/h2&gt;

&lt;p&gt;글로벌 OTT 최강자인 넷플릭스는 전 세계 3개 리전을 동시에 가동하는 &lt;strong&gt;Active-Active 다중 리전 아키텍처&lt;/strong&gt;를 구축하여 데이터 동기화 난제를 해결하였다.&lt;br /&gt;
넷플릭스 기술 블로그인 &lt;a href=&quot;https://netflixtechblog.com/active-active-for-multi-regional-resiliency-c47719f6685b&quot;&gt;Active-Active for Multi-Regional Resiliency&lt;/a&gt;의 
핵심 기술 포인트는 아래와 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;왜 데이터가 누락되는 현상이 발생할까?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;일반적인 Primary-Replica 기반의 교차 데이터 센터 구조(Active-Passive)나 느슨한 비동기 복제를 사용하면 동부 센터에서 발생한 쓰기 연산이 물리적인 거리 한계 때문에 
실시간으로 서부 센터 DB에 동기화되지 못하고 몇 초간의 지연(Lag)이 생긴다.&lt;/li&gt;
      &lt;li&gt;이 찰나의 순간에 동부 데이터 센터가 예기치 않게 다운되어 유저들이 서부 데이터 센터로 강제 우회되면 ‘데이터가 존재하지 않는 현상’을 겪게 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Apache Cassandra를 활용한 글로벌 다중화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;넷플릭스는 중앙 집중형 Primary-Replica 구조 대신, 전 세계 데이터 센터가 모두 Primary 서버처럼 동작하는 글로벌 분산 NoSQL 데이터베이스인 카산드라를 도입했다.&lt;/li&gt;
      &lt;li&gt;유저가 전 세계 어느 리전 DB에 데이터를 쓰더라도, 카산드라 고유의 내부 분산 알고리즘을 통해 다른 글로벌 리전의 모든 노드로 데이터가 실시간 교차 전파(Multi-Region Replication)된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 충돌 해결: LWW(Last-Write-Win) 전략&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터 센터 간 동기화 도중, 아주 미세한 차이로 동일한 데이터에 대한 수정 요청이 동부와 서부에서 동시에 일어난다면 어떻게 일관성을 맞출까?&lt;/li&gt;
      &lt;li&gt;카산드라는 &lt;strong&gt;LWW(최종 쓰기 승리)&lt;/strong&gt; 기법을 사용한다.&lt;/li&gt;
      &lt;li&gt;각 데이터에 기록된 타임스탬프를 정밀하게 비교하여, 가장 나중에 들어온 최종 데이터를 전 세계 데이터 센터의 ‘정답’으로 인정하고 일치시킨다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;덕분에 넷플릭스는 데이터 센터 한 곳이 통째로 파괴되어 GeoDNS가 트래픽을 긴급 우회시켜도, 사용자가 데이터 유실이나 끊김을 전혀 느끼지 못하는 완벽한 글로벌 동기화를 달성하였다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;10-시스템-간-결합도-낮추기-메시지-큐&quot;&gt;10. 시스템 간 결합도 낮추기: 메시지 큐&lt;/h1&gt;

&lt;p&gt;메시지 큐는 메시지의 무손실을 보장하는 비동기 통신을 지원하는 분산 시스템 컴포넌트이다.&lt;br /&gt;
메시지 큐에 보관된 메시지를 Consumer가 꺼내서 처리할 때까지 큐 메모리나 디스크에 보존된다. 이를 통해 대규모 시스템에서 서비스 간의 결합도를 낮추고 안정성을 확보할 수 있다.&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;Producer와 Consumer의 결합도 완화&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;비동기 통신:&lt;/strong&gt; Producer는 시간이 오래 걸리는 작업을 큐에 던지기만 하고 즉시 유저에게 응답 반환&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장애 격리:&lt;/strong&gt; Consumer(작업 서버) 프로세스가 일시적으로 다운되거나 점검 중이라도 Producer는 아무런 제약 없이 메시지 발생, 반대로 Producer 서비스가 마비되어도 Consumer는 큐에 
쌓여있던 기존 메시지들을 처리함&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;11-로그-메트릭-모니터링-그리고-자동화&quot;&gt;11. 로그, 메트릭 모니터링 그리고 자동화&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;로그:&lt;/strong&gt; 대규모 환경에서는 서버마다 로그를 보러 들어갈 수 없으므로, 중앙 집중형 로그 수집 환경(예: ELK 스택, Grafana Loki)을 구축해야 함&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;메트릭:&lt;/strong&gt; 시스템의 현재 상태를 수치화된 데이터로 수집
    &lt;ul&gt;
      &lt;li&gt;호스트 메트릭: CPU 사용량, 메모리 잔여량, 디스크 I/O 등 물리적 자원 상태&lt;/li&gt;
      &lt;li&gt;비즈니스 메트릭: 일일 활성 사용자(DAU), 초당 요청 수(RPS), 응답 지연 시간(Latency)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;모니터링 알림:&lt;/strong&gt; 수집된 메트릭을 대시보드(Grafana 등)로 시각화하고, 시스템 임계치(예: CPU 80% 이상 지속)를 초과하면 슬랙으로 개발자에게 즉시 경고를 보냄&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;모니터링 시스템이 메트릭을 감시하다가 설정된 규칙에 따라 인프라를 자동으로 제어한다.&lt;br /&gt;
예를 들어 트래픽이 폭발하여 CPU 부하가 걸리면 자동으로 웹 서버 인스턴스를 늘리고, 새벽 시간대에 트래픽이 한산해지면 서버를 자동으로 줄여 인프라 비용을 아껴준다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/12.png&quot; alt=&quot;로그 메트릭과 메시지 큐를 적용한 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;12-db-샤딩-도입&quot;&gt;12. DB 샤딩 도입&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;DB 샤딩&lt;/strong&gt;은 대규모 데이터 계층을 확장하기 위해, 하나의 거대한 DB를 &lt;strong&gt;‘샤드(Shard)’라고 부르는 작은 단위의 독립된 DB 서버들로 분할하여 저장하는 기술&lt;/strong&gt;이다.&lt;br /&gt;
모든 샤드는 동일한 테이블 구조(스키마)를 공유하지만, 각 샤드에 보관되는 데이터 간에는 서로 중복되거나 겹치는 부분이 없다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/13.png&quot; alt=&quot;데이터베이스 샤드&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;샤딩 키의 중요성&lt;/strong&gt;&amp;gt;&lt;br /&gt;
샤딩을 구현할 때 가장 중요한 것은 샤딩 키(파티션 키)를 무엇으로 정하느냐이다.&lt;br /&gt;
샤딩 키는 데이터를 어떤 샤드 서버에 저장할지 결정하는 기준 컬럼(예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_id % 4&lt;/code&gt; 연산을 통해 0~3번 샤드로 분배)이다.&lt;br /&gt;
데이터가 모든 샤드에 수학적으로 균등하게 분산되도록 설계하는 것이 핵심이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;⚠️샤딩 도입 시 반드시 해결해야 할 3가지 난제&lt;/strong&gt;&lt;br /&gt;
샤딩은 데이터 계층의 무한 확장을 가능하게 하지만, 분산 시스템 고유의 난제를 치르게 한다.&lt;/p&gt;

&lt;h2 id=&quot;121-데이터의-재샤딩resharding&quot;&gt;12.1. 데이터의 재샤딩(Resharding)&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;원인:&lt;/strong&gt; 특정 샤드의 데이터 성장 속도가 너무 빨라 공간이 먼저 소진되거나, 샤드 간 데이터 분포가 균등하지 못해 특정 샤드만 빠르게 데이터가 차는 상황이 발생할 수 있다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;해결책:&lt;/strong&gt; 이 현상이 발생하면 샤드 배치 함수(해시 함수)를 수정하고 기존 데이터를 새로운 샤드 배열에 맞춰 다시 대규모 Data migration을 진행해야 한다. 
이 비용을 최소화하기 위해 향후 배울 &lt;strong&gt;안정 해시(Consistent Hashing)&lt;/strong&gt; 기법을 필수로 도입해야 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;안정 해시에 대해서는 추후 다룰 예정입니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;122-유명인사-문제celebrity&quot;&gt;12.2. 유명인사 문제(Celebrity)&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;원인&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특정 샤드 서버에 질의와 트래픽이 비정상적으로 집중되어 서버가 터지는 현상이다. 핫스팟 문제라고도 한다.&lt;/li&gt;
      &lt;li&gt;예를 들어 SNS 서비스에서 팔로워 수천만 명인 유명인의 데이터가 샤드 2번에 할당되어 있다면, 해당 인물이 글을 쓴 순간 수많은 팬의 읽기/쓰기 연산이 2번 샤드로만 집중되어 과부하가 걸린다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;해결책:&lt;/strong&gt; 해당 유명인사 유저들에게만 전용 샤드를 독립적으로 떼어주거나, 유명인사 관련 데이터를 더 잘게 쪼개는 특수한 파티셔닝 정밀 설계가 필요하다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;123-조인과-비정규화&quot;&gt;12.3. 조인과 비정규화&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;원인:&lt;/strong&gt; 하나의 물리적 DB 안에서는 자유롭게 테이블 간 JOIN 연산이 가능했지만, 데이터가 여러 샤드 서버로 물리적으로 찢어지고 나면 여러 샤드에 걸린 데이터를 단 한 번의 쿼리로 조인하는 것은 사실상 불가능해진다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;해결책:&lt;/strong&gt; 분산 네트워크 조인은 성능을 파괴하므로, 대규모 시스템에서는 DB를 의도적으로 비정규화하여 하나의 테이블 안에 필요한 정보를 중복으로 저장하여, 각 샤드 내부에서 단일 질의만으로 데이터를 조회할 수 있도록 데이터 구조를 전면 재설계해야 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;아래는 데이터베이스 샤딩이 적용된 아키텍처이다. 더불어 데이터베이스를 줄이기 위해 굳이 RDBMS가 요구되지 않는 기능들은 NoSQL로 이전하였다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0611/14.png&quot; alt=&quot;데이터베이스 샤딩이 적용된 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;요약&quot;&gt;요약&lt;/h1&gt;

&lt;p&gt;시스템 규모 확장을 위한 설계 체크 리스트&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;웹 계층은 Stateless로 유지&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;서버 로컬 메모리에 세션이나 유저 정보를 귀속시키지 말 것&lt;/li&gt;
      &lt;li&gt;부하 분산과 무한 Scale-out, 유연한 Auto-Scaling의 대전제임&lt;/li&gt;
      &lt;li&gt;상태 정보는 분산 Key-Value NoSQL인 Redis 같은 외부 공유 저장소에 위임&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;인프라의 모든 계층에 다중화(Replication) 도입&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;웹 서버는 로드밸런서 뒷단에 병렬 배정하고, DB는 읽기 전용 사본과 쓰기 전용 원본으로 분리하여 SPOF(단일 장애 지점)을 완전히 제거&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;글로벌 유저를 위한 여러 데이터 센터와 CDN 활용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;정적 파일은 전 세계 에지 서버에 뿌려주는 CDN을 통해 원본 서버 부하를 줄임&lt;/li&gt;
      &lt;li&gt;GeoDNS 기반의 지리적 라우팅 난제를 해결하여 사용자를 가장 가까운 다중 데이터 센터로 안내하고, 데이터 센터 간 동기화 체계를 굳건히 해야 함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 계층의 한계를 샤딩과 NoSQL로 돌파&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;단일 RDBMS의 용량과 성능 한계가 오면 데이터를 고르게 분산할 수 있는 샤딩 키를 선정하여 Scale-out 을 함&lt;/li&gt;
      &lt;li&gt;굳이 RDBMS 가 필요 없는 정형화되지 않은 대량의 데이터는 NoSQL로 이관하여 부하를 분산&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;메시지 큐로 서비스 간 결합도를 느슨하게 유지&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시간이 오래 걸리거나 무거운 작업들은 메시지 큐를 통해 비동기 처리&lt;/li&gt;
      &lt;li&gt;Producer와 Consumer의 결합도가 낮아져 한쪽 시스템이 다운되어도 전체 서비스가 마비되지 않는 탄력성을 얻게 됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;모니터링 자동화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;중앙 집중형 로그 수집과 메트릭 모니터링을 통해 시스템의 이상 징후를 실시간 감시&lt;/li&gt;
      &lt;li&gt;트래픽에 맞춰 Auto-Scaling 자동화는 필수&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000001033116&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://codeahoy.com/2017/08/11/caching-strategies-and-how-to-choose-the-right-one/&quot;&gt;캐시 전략 심층 분석: Codeahoy - Caching Strategies and How to Choose the Right One&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ko/cloudfront/dynamic-content/&quot;&gt;동적 콘텐츠 가속 원리: AWS 공식 문서 - CloudFront Dynamic Content Delivery&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-sticky-sessions.html&quot;&gt;로드밸런서 Sticky Session: AWS 공식 문서 - Elastic Load Balancing Sticky Sessions&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://netflixtechblog.com/active-active-for-multi-regional-resiliency-c47719f6685b&quot;&gt;글로벌 데이터 동기화 사례: Netflix Tech Blog - Active-Active for Multi-Regional Resiliency&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 11 Jun 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/06/11/architecture-system-design-scalability-guide/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/06/11/architecture-system-design-scalability-guide/</guid>
        
        <category>architecture</category>
        
        <category>scalability</category>
        
        <category>load-balancing</category>
        
        <category>database-replication</category>
        
        <category>caching-strategies</category>
        
        <category>cdn</category>
        
        <category>stateless</category>
        
        <category>message-queue</category>
        
        <category>sharding</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(2) - 주변 친구</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-요구사항-및-규모-추정&quot;&gt;1. 요구사항 및 규모 추정&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-시스템-제약-사항-및-가정&quot;&gt;1.1. 시스템 제약 사항 및 가정&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#12-기능-요구사항&quot;&gt;1.2. 기능 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#13-비기능-요구-사항&quot;&gt;1.3. 비기능 요구 사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#14-핵심-트래픽-규모-추정qps-계산&quot;&gt;1.4. 핵심 트래픽 규모 추정(QPS 계산)&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-개략적-아키텍처-설계&quot;&gt;2. 개략적 아키텍처 설계&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-p2p-모델의-한계와-공용-백엔드-도입-배경&quot;&gt;2.1. P2P 모델의 한계와 공용 백엔드 도입 배경&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-전체-컴포넌트-구조와-데이터-흐름&quot;&gt;2.2. 전체 컴포넌트 구조와 데이터 흐름&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#221-각-컴포넌트의-역할&quot;&gt;2.2.1. 각 컴포넌트의 역할&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#222-사용자-위치-변경-시-일어나는-일&quot;&gt;2.2.2. 사용자 위치 변경 시 일어나는 일&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#223-친구에게-위치-변경-내역이-전파되는-상세-라우팅-흐름&quot;&gt;2.2.3. 친구에게 위치 변경 내역이 전파되는 상세 라우팅 흐름&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-실시간-통신을-위한-웹소켓-api-설계&quot;&gt;2.3. 실시간 통신을 위한 웹소켓 API 설계&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#24-데이터-모델링-및-저장소-선택-배경&quot;&gt;2.4. 데이터 모델링 및 저장소 선택 배경&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#241-위치-정보-캐시redis&quot;&gt;2.4.1. 위치 정보 캐시(Redis)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#242-위치-이동-이력-dbcassandra&quot;&gt;2.4.2. 위치 이동 이력 DB(Cassandra)&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-시스템-확장성을-고려한-상세-설계&quot;&gt;3. 시스템 확장성을 고려한 상세 설계&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-stateless-api-서버-vs-stateful-웹소켓-서버-확장-전략graceful-draining&quot;&gt;3.1. Stateless API 서버 vs Stateful 웹소켓 서버 확장 전략(Graceful Draining)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#311-stateless-api-서버의-확장&quot;&gt;3.1.1. Stateless API 서버의 확장&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#312-stateful-웹소켓-서버의-확장&quot;&gt;3.1.2. Stateful 웹소켓 서버의 확장&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-웹소켓-클라이언트-초기화&quot;&gt;3.2. 웹소켓 클라이언트 초기화&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-위치-정보-캐시-샤딩과-장애-복구warmed-up-전략&quot;&gt;3.3. 위치 정보 캐시 샤딩과 장애 복구(Warmed Up) 전략&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#331-사용자-db의-scale-out&quot;&gt;3.3.1. 사용자 DB의 Scale-out&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#332-위치-정보-캐시redis-cache의-한계와-샤딩&quot;&gt;3.3.2. 위치 정보 캐시(Redis Cache)의 한계와 샤딩&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#34-레디스-pubsub-서버의-메모리-및-cpu-병목&quot;&gt;3.4. 레디스 Pub/Sub 서버의 메모리 및 CPU 병목&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#341-병목은-메모리가-아니라-cpu-사용량&quot;&gt;3.4.1. 병목은 메모리가 아니라 CPU 사용량&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#35-분산-레디스-펍섭-클러스터와-안정-해시consistent-hash-ring&quot;&gt;3.5. 분산 레디스 펍/섭 클러스터와 안정 해시(Consistent Hash Ring)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#36-레디스-펍섭-서버-클러스터-규모-확장-시-주의사항&quot;&gt;3.6. 레디스 펍/섭 서버 클러스터 규모 확장 시 주의사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#37-예외-케이스-처리-및-기능-확장&quot;&gt;3.7. 예외 케이스 처리 및 기능 확장&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#371-친구-추가삭제-및-변경-처리&quot;&gt;3.7.1. 친구 추가/삭제 및 변경 처리&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#372-친구가-많은-사용자&quot;&gt;3.7.2. 친구가 많은 사용자&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#373-주변의-임의-사용자-노출을-위한-지오해시-pubsub-풀-구조&quot;&gt;3.7.3. 주변의 임의 사용자 노출을 위한 지오해시 Pub/Sub 풀 구조&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-레디스-pubsub을-대체할-대안-얼랭erlang&quot;&gt;4. 레디스 Pub/Sub을 대체할 대안: 얼랭(Erlang)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-얼랭을-활용한-분산-메시지망-구조&quot;&gt;4.1. 얼랭을 활용한 분산 메시지망 구조&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-최종-아키텍처-다이어그램&quot;&gt;5. 최종 아키텍처 다이어그램&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#핵심-요약-및-takeaway&quot;&gt;핵심 요약 및 Takeaway&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;여기서는 주변 친구라는 모바일 앱 기능을 지원하는 규모 확장이 가능한 BE를 설계해본다.&lt;/p&gt;

&lt;p&gt;앱 사용자 중 본인 위치 정보 접근 권한을 허락한 사용자에 한해 인근의 친구 목록을 보여주는 시스템이다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/05/24/architecture-proximity/&quot;&gt;Architecture - 근접성 서비스(Geohash, Quadtree)&lt;/a&gt; 와 다른 점이 있다면 근접성 서비스의 경우 사업장 주소는 정적이지만, 
주변 친구 위치는 자주 바뀐다는 점이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-요구사항-및-규모-추정&quot;&gt;1. 요구사항 및 규모 추정&lt;/h1&gt;

&lt;p&gt;대규모 시스템을 설계할 때 가장 먼저 해야 할 일은 &lt;strong&gt;설계의 범위와 제약 조건을 명확히 하는 것&lt;/strong&gt;이다.&lt;br /&gt;
여기서는 모바일 사용자들을 위해 인근에 있는 활성 상태의 친구 목록을 실시간으로 보여주는 ‘주변 친구(Nearby Friends)’ 서비스의 요구 사항을 정의하고, 이를 감당하기 위한 
시스템 규모를 예측해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-시스템-제약-사항-및-가정&quot;&gt;1.1. 시스템 제약 사항 및 가정&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;주변 거리 정의&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;지리적으로 얼마나 가까워야 ‘주변에 있다’고 할 수 있나?
        &lt;ul&gt;
          &lt;li&gt;사용자의 지리적 반경 5마일(약 8km) 이내의 친구를 ‘주변에 있다’라고 정의하며, 이 수치는 유연하게 설정 가능해야 한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;거리 계산 방식&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;그 거리는 두 사용자 사이의 직선거리라고 가정해도 되나?
        &lt;ul&gt;
          &lt;li&gt;두 사용자 사이의 거리는 복잡한 도로망이 아닌 단순 직선거리라고 가정한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;서비스 이용 규모&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;얼마나 많은 사용자가 이 앱을 사용하나? 전체 가입자 10억 명 중 10% 정도가 이 기능을 쓴다고 가정해도 되나?
        &lt;ul&gt;
          &lt;li&gt;서비스의 전체 가입자는 10억 명이며, 이 중 10% 수준인 1억 명이 이 주변 친구 기능을 활용한다고 가정한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;이동 이력 보관&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자의 이동 이력도 시스템에 보관해야 하는가?
        &lt;ul&gt;
          &lt;li&gt;사용자가 이동한 과거 위치 정보 이력은 머신러닝 분석 등 다양한 비즈니스 용도로 활용할 수 있도록 시스템에 보관해야 한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;비활성 유저 처리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;친구 관계인 사용자가 10분 이상 비활성 상태(앱 종료 등)라면 주변 친구 목록에 마지막 위치를 보여줘야 하는가, 아니면 사라지게 해야 하는가?
        &lt;ul&gt;
          &lt;li&gt;친구 관계인 사용자가 10분 이상 비활성 상태(앱 종료, 미이동 등)가 되면 해당 사용자를 주변 친구 목록에서 즉시 제거해야 한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;규제 및 보호법 적용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;GDPR이나 CCPA 같은 글로벌 사생활 보호 및 데이터 보호법도 고려해야 하나?
        &lt;ul&gt;
          &lt;li&gt;GDPR이나 CCPA 같은 사생활 보호 및 데이터 보호법은 실서비스 시 필수 요건이나, 이번 아키텍처 설계 단계에서는 복잡성을 줄이기 위해 생략한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;12-기능-요구사항&quot;&gt;1.2. 기능 요구사항&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;주변 친구 목록 제공&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자는 반경 5마일 이내의 활성 상태 친구 목록을 볼 수 있어야 하며, 목록에는 &lt;strong&gt;거리&lt;/strong&gt;와 마지막 갱신 시각(Timestamp)이 표시된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;실시간 위치 갱신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;친구들의 위치는 수 초 주기로 실시간으로 자동 업데이트되어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;타임아웃 처리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;10분 이상 비활성화된 친구는 목록에서 즉시 제거한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;위치 이력 저장&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;대용량 쓰기 작업을 감당하며 유저의 이동 이력을 영속성 있게 보관해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;13-비기능-요구-사항&quot;&gt;1.3. 비기능 요구 사항&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;낮은 지연 시간(low latency)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;친구의 위치 변화가 주변 사용자에게 반영되기까지의 시간이 수 초 이내여야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;안정성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;대규모 트래픽 속에서 일부 위치 데이터가 전송 중에 유실되는 것 정도는 용인할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;결과적 일관성(eventual consistency)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;강한 일관성을 위해 시스템 전체를 멈출 필요는 없다. 몇 초 뒤에 데이터가 일치하는 결과적 일관성이면 충분하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;14-핵심-트래픽-규모-추정qps-계산&quot;&gt;1.4. 핵심 트래픽 규모 추정(QPS 계산)&lt;/h2&gt;

&lt;p&gt;규모 추정을 위해 고려해야 할 제약사항과 가정을 정의해보자.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;일일 활성 사용자(DAU)&lt;/strong&gt;: 10억 명의 10%인 &lt;strong&gt;1억 명&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;동시 접속 사용자&lt;/strong&gt;: DAU의 10%인 &lt;strong&gt;1,000만 명&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;위치 전송 주기&lt;/strong&gt;: &lt;strong&gt;30초마다&lt;/strong&gt; 자신의 현재 위치를 전송&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;평균 친구 수&lt;/strong&gt;: 사용자당 &lt;strong&gt;400명&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;위치 정보 갱신 QPS(Query Per Second) 계산&lt;/strong&gt;&amp;gt;&lt;br /&gt;
동시 접속자 1,000만 명이 30초에 한 번씩 서버로 위치 정보를 전송하므로, 서버가 받아내야 하는 실시간 write 트래픽의 규모는 아래와 같다.&lt;/p&gt;

\[\text{위치 갱신 QPS} = \frac{10,000,000 \text{ 명}}{30 \text{ 초}} \approx 333,333.3 \text{ QPS}\]

&lt;p&gt;따라서 시스템은 &lt;strong&gt;초당 약 334,000건&lt;/strong&gt;의 위치 요청을 감당할 수 있을 정도로 설계해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-개략적-아키텍처-설계&quot;&gt;2. 개략적 아키텍처 설계&lt;/h1&gt;

&lt;p&gt;주변 친구 서비스는 사용자의 위치가 실시간으로 끊임없이 변한다는 동적인 특성을 가진다.&lt;br /&gt;
이 때문에 고정된 사업장 주소를 다루는 &lt;a href=&quot;https://assu10.github.io/dev/2026/05/24/architecture-proximity/&quot;&gt;정적 근접성 서비스&lt;/a&gt;와는 전혀 다른 접근이 필요하다.&lt;br /&gt;
수많은 활성 사용자 간에 실시간으로 위치 변경 데이터를 전송(Push)하기 위한 개략적 아키텍처에 대해 설계해 보자.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-p2p-모델의-한계와-공용-백엔드-도입-배경&quot;&gt;2.1. P2P 모델의 한계와 공용 백엔드 도입 배경&lt;/h2&gt;

&lt;p&gt;이번 문제는 메시지의 효과적 전송을 가능하게 할 설계안을 요구한다.&lt;/p&gt;

&lt;p&gt;실시간으로 서로의 위치를 공유하는 가장 단순한 방법은 서버를 거치지 않고 단말끼리 직접 통신하는 &lt;strong&gt;P2P(Peer-to-Peer)&lt;/strong&gt; 방식이다.&lt;br /&gt;
활성 상태인 인근 모든 친구와 연결을 유지하며 데이터를 주고받는 구조이다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
%% 노드 스타일 정의
  classDef peer fill:#ffffff,stroke:#333333,stroke-width:2px;

%% 노드 정의 (위치에 맞춰 명명)
  TopLeft[&quot;📱 Peer 1&quot;]:::peer
  TopMid[&quot;📱 Peer 2&quot;]:::peer
  TopRight[&quot;📱 Peer 3&quot;]:::peer

  MidLeft[&quot;📱 Peer 4&quot;]:::peer
  MidRight[&quot;📱 Peer 5&quot;]:::peer
  FarRight[&quot;📱 Peer 6&quot;]:::peer

  BotLeft[&quot;📱 Peer 7&quot;]:::peer
  BotRight[&quot;📱 Peer 8&quot;]:::peer

%% 연결 관계 정의 (그물형 네트워크)
  TopLeft --- TopMid
  TopLeft --- MidLeft
  TopLeft --- BotLeft

  TopMid --- MidRight
  TopMid --- TopRight

  TopRight --- FarRight
  TopRight --- MidRight
  TopRight --- BotRight
  TopRight --- BotLeft

  MidLeft --- TopLeft
  MidLeft --- BotLeft
  MidLeft --- MidRight

  MidRight --- BotLeft
  MidRight --- BotRight
  MidRight --- FarRight

  FarRight --- BotRight

  BotLeft --- BotRight
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;🚨P2P 방식의 한계&lt;/strong&gt;&lt;br /&gt;
모바일 단말은 통신 연결 상태가 불안정할 때가 많고, 배터리나 전력 소모 측면에서 극도로 제한된 자원을 가진다.&lt;br /&gt;
수백 명의 친구 단말과 직접 mesh 네트워크를 구성하여 상시 통신을 유지하는 것은 모바일 환경에서 실용적이지 못하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;💡대안: 공용 백엔드 아키텍처&lt;/strong&gt;&lt;br /&gt;
이 문제를 해결하기 위해 중앙에서 메시지 허브 역할을 해줄 공용 백엔드를 도입한다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;%%{init: {&apos;flowchart&apos;: {&apos;nodeSpacing&apos;: 80, &apos;rankSpacing&apos;: 80, &apos;padding&apos;: 30, &apos;htmlLabels&apos;: true}}}%%
graph LR
%% 노드 스타일 정의
  classDef device fill:#ffffff,stroke:#333333,stroke-width:2px,shape:rectangle;
  classDef backend fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;

%% 노드 배치
  User[&quot;📱 사용자&quot;]:::device

subgraph BackendBox [&quot; &quot;]
Backend[&quot;💻 공용 백엔드&amp;lt;br/&amp;gt;(Common Backend)&quot;]:::backend
end

subgraph Friends [&quot;친구 그룹&quot;]
FriendA[&quot;📱 친구 A&quot;]:::device
FriendB[&quot;📱 친구 B&quot;]:::device
FriendC[&quot;📱 친구 C&quot;]:::device
end

%% 연결선
User --&amp;gt; Backend
Backend --&amp;gt; FriendA
Backend --&amp;gt; FriendB
Backend --&amp;gt; FriendC

%% 서브그래프 스타일 (테두리 숨기기 등)
style BackendBox fill:none,stroke:none;
style Friends fill:#f9f9f9,stroke:#cccccc,stroke-dasharray: 5 5;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;공용 백엔드는 다음과 같은 핵심 역할을 수행한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;모든 활성 상태 사용자의 위치 변경 내역을 수신&lt;/li&gt;
  &lt;li&gt;위치 변경을 수신할 때마다 해당 사용자의 모든 활성 상태 친구를 조회&lt;/li&gt;
  &lt;li&gt;두 사용자 간의 거리가 설정된 임계치인 5마일 이내인 경우에만 변경 내역을 해당 친구의 단말로 전송&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;⚠️대규모 확장의 핵심 병목 구간&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;공용 백엔드 방식 역시 단순하게 구현하면 거대한 규모를 감당하기 어렵다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;동시 접속자 1,000만 명이 30초마다 위치를 갱신하면 &lt;strong&gt;초당 334,000건의 위치 변경&lt;/strong&gt;을 처리해야 한다.&lt;/li&gt;
  &lt;li&gt;유저당 평균 400명의 친구가 있고 그 중 10%가 인근에서 활성화 상태라면, 백엔드가 처리해야 할 초당 위치 정보 전송 건수는 334,000 * 400 * 10% = 1,336만 건에 달한다.&lt;br /&gt;
이 엄청난 양의 패킷을 지연 없이 단말로 밀어주기 위한 정교한 컴포넌트 설계가 필수적이다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-전체-컴포넌트-구조와-데이터-흐름&quot;&gt;2.2. 전체 컴포넌트 구조와 데이터 흐름&lt;/h2&gt;

&lt;p&gt;트래픽 병목을 분산하기 위해 &lt;strong&gt;무상태(Stateless) API 레이어&lt;/strong&gt;와 실시간 전송을 담당하는 &lt;strong&gt;상태유지(Stateful) 웹소켓 레이어&lt;/strong&gt;를 분리한 개략적 설계안이다.&lt;/p&gt;

&lt;p&gt;RESTful API 처리 흐름 예&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
%% 노드 스타일 정의
  classDef client fill:#ffffff,stroke:#333333,stroke-width:2px;
  classDef lb fill:#ffffff,stroke:#333333,stroke-width:2px;
  classDef server fill:#f4f9ff,stroke:#2196f3,stroke-width:2px;
  classDef database fill:#fff9f4,stroke:#ff9800,stroke-width:2px;

%% 1단계: 최상단 클라이언트
  Client[&quot;📱 모바일 사용자&quot;]:::client

%% 2단계: 로드밸런서
  LB[&quot;⚖️ 로드밸런서&quot;]:::lb

%% 3단계: 서버 레이어
  WS_Server[&quot;💻 웹소켓 서버&amp;lt;br&amp;gt;(양방향 위치 정보)&quot;]:::server
  API_Server[&quot;💻 API 서버&amp;lt;br&amp;gt;(사용자 관리&amp;lt;br&amp;gt;친구 관리&amp;lt;br&amp;gt;인증 및 기타 기능)&quot;]:::server

%% 4단계: 하단 컴포넌트 레이어
  Redis[(&quot;🛢️ 레디스 펍/섭&amp;lt;br&amp;gt;(Publish/Subscribe,&amp;lt;br&amp;gt;Pub/Sub)&quot;)]:::database
  Cache[&quot;💾 캐시&amp;lt;br&amp;gt;(위치 정보 캐시)&quot;]:::database
  DB_Location[(&quot;🛢️ 위치 이동 이력&amp;lt;br&amp;gt;데이터베이스&quot;)]:::database
  DB_User[(&quot;🛢️ 사용자 데이터베이스&amp;lt;br&amp;gt;(사용자 정보, 친구 관계)&quot;)]:::database

%% --------------------------------------------------------
%% 화살표 흐름 정의
%% --------------------------------------------------------

%% 모바일 사용자 &amp;lt;-&amp;gt; 로드밸런서
  Client &amp;lt;--&amp;gt;|&quot;(WebSocket, WS)&quot;| LB
  Client -- &quot;① http&quot; --&amp;gt; LB

%% 로드밸런서 -&amp;gt; 서버 레이어
  LB &amp;lt;--&amp;gt; WS_Server
  LB --&amp;gt;|②| API_Server

%% 웹소켓 서버 &amp;lt;-&amp;gt; 레디스 펍/섭
  WS_Server &amp;lt;--&amp;gt; Redis

%% 웹소켓 서버 -&amp;gt; 하단 저장소들
  WS_Server --&amp;gt; Cache
  WS_Server --&amp;gt; DB_Location
  WS_Server --&amp;gt; DB_User

%% API 서버 -&amp;gt; 사용자 데이터베이스
  API_Server --&amp;gt;|③| DB_User

%% 하단 컴포넌트 좌-&amp;gt;우 정렬 순서 강제 고정
  Redis ~~~ Cache ~~~ DB_Location ~~~ DB_User

%% --------------------------------------------------------
%% 숫자가 붙은 화살표(①, ②, ③)만 빨간색 스타일 적용
%% --------------------------------------------------------
  linkStyle 1 stroke:red,stroke-width:2px,color:red;
  linkStyle 3 stroke:red,stroke-width:2px,color:red;
  linkStyle 8 stroke:red,stroke-width:2px,color:red;
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;221-각-컴포넌트의-역할&quot;&gt;2.2.1. 각 컴포넌트의 역할&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;로드밸런서&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RESTful API 요청 및 상태유지(Stateful) 웹소켓 연결 요청을 앞단에서 받아 적절한 서버 클러스터로 부하를 분산한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;RESTful API 서버&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stateless API 서버 클러스터로, 회원 관리, 친구 추가/삭제, 인증 등 전통적인 HTTP 요청/응답 트래픽을 처리한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;웹소켓 서버&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;실시간 위치 변경을 처리하는 상태유지(Stateful) 서버 클러스터이다.&lt;br /&gt;
클라이언트는 앱 구동 시 웹소켓 서버 중 한 대와 연결을 맺고 이를 계속 유지한다.&lt;br /&gt;
인근 친구의 위치가 바뀌면 이 상시 연결 채널을 통해 단말로 실시간 푸시가 이루어진다.&lt;br /&gt;
또한 최초 진입 시 클라이언트 초기화 작업도 담당한다. 모바일 클라이언트가 시작되면, 온라인 상태인 모든 주변 친구의 위치를 해당 클라이언트로 전송한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;레디스 위치 정보 캐시&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;활성 상태 사용자의 가장 최신 위치(위도, 경도, 타임스탬프)를 보관하는 인메모리 저장소이다.&lt;br /&gt;
각 데이터에 TTL을 설정하여, 10분간 위치가 갱신되지 않으면 비활성 상태로 간주하고 캐시에서 자동으로 삭제되도록 제어한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;사용자 DB&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;사용자 기본 프로필 및 친구 관계 매핑 정보를 보관하는 영속성 저장소이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;위치 이동 이력 DB&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;유저의 과거 이동 동선을 누적 저장하는 곳으로, 실시간 서비스 성능에는 영향을 주지 않도록 비동기 처리 구조를 지향한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;레디스 Pub/Sub 서버&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;사용자 간의 위치 변경 이벤트를 중계하는 초경량 메시지 버스 레이어이다.&lt;br /&gt;
각 활성 사용자의 전용 채널(Channel)을 개설하고, 그 친구들이 해당 채널을 구독하는 형태로 실시간 라우팅을 구현한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;레디스 Pub/Sub을 통한 실시간 위치 전파 메커니즘&lt;/strong&gt;&lt;br /&gt;
웹소켓 서버를 통해 수신한 특정 사용자의 위치 정보 변경 이벤트는 해당 사용자에게 배정된 Pub/Sub 채널에 발행된다.&lt;br /&gt;
특정 사용자의 위치가 변경되면 해당 사용자의 모든 친구의 웹소켓 연결 핸들러가 호출되고, 각 핸들러는 위치 변경 이벤트를 수신할 친구가 활성 상태인 경우 거리를 다시 계산한다.&lt;br /&gt;
새로 계산한 거리가 검색 반경 이내면 갱신된 위치와 갱신 시각(timestamp)을 웹소켓 연결을 통해 해당 친구의 클라이언트 앱으로 보낸다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
%% 노드 스타일 정의
  classDef user fill:#ffffff,stroke:#333333,stroke-width:2px;
  classDef channel fill:#fff9f4,stroke:#ff9800,stroke-width:2px;
  classDef friend fill:#ffffff,stroke:#333333,stroke-width:2px;
  classDef invisible fill:none,stroke:none,font-weight:bold;

%% ★ 박스 위에 띄울 투명 텍스트 노드
  TitleText[&quot;위치 정보가 갱신되었다는&amp;lt;br&amp;gt;이벤트를 발행하려는 사용자들&quot;]:::invisible
  TitleText ~~~ Publishers

%% 1. 발행자 영역 (좌측)
  subgraph Publishers [&quot;위치 정보 발행자&quot;]
    User1[&quot;👤 사용자 1&quot;]:::user
    User2[&quot;👤 사용자 2&quot;]:::user
  end

%% 2. 레디스 펍/섭 영역 (중앙)
  subgraph RedisPubSub [&quot;레디스 펍/섭 (Redis Pub/Sub)&quot;]
    Ch1[(&quot;🛢️ 사용자 1의 채널&quot;)]:::channel
    Ch2[(&quot;🛢️ 사용자 2의 채널&quot;)]:::channel
  end

%% 3. 구독자 영역 (우측)
  subgraph Subscribers [&quot;구독 관계의 친구들 (Subscribers)&quot;]
    Friend1[&quot;👥 친구 1&quot;]:::friend
    Friend2[&quot;👥 친구 2&quot;]:::friend
    Friend3[&quot;👥 친구 3&quot;]:::friend
  end

%% 데이터 흐름
  User1 --&amp;gt;|이벤트 발행| Ch1
  User2 --&amp;gt;|이벤트 발행| Ch2
  Ch1 --&amp;gt;|친구에게 전달| Friend1
  Ch2 --&amp;gt;|친구에게 전달| Friend2
  Ch2 --&amp;gt;|친구에게 전달| Friend3

  style Publishers fill:none,stroke:#999999,stroke-width:1px,stroke-dasharray: 5 5;
  style RedisPubSub fill:none,stroke:#999999,stroke-width:1px,stroke-dasharray: 5 5;
  style Subscribers fill:none,stroke:#999999,stroke-width:1px,stroke-dasharray: 5 5;
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;222-사용자-위치-변경-시-일어나는-일&quot;&gt;2.2.2. 사용자 위치 변경 시 일어나는 일&lt;/h3&gt;

&lt;p&gt;모바일 클라이언트가 주기적으로 자신의 위치를 바꿀 때 전체 시스템 컴포넌트가 연쇄적으로 반응하는 과정이다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;항구적&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;네트워크 설계에서 ‘항구적 연결’이란 &lt;strong&gt;지속성 연결&lt;/strong&gt;을 뜻한다.&lt;br /&gt;
일반적인 HTTP 통신은 클라이언트가 요청을 보내고 서버가 응답하면 그 즉시 TCP 커넥션을 끊어버리는 일회성 구조를 가진다.&lt;br /&gt;
반면, 웹소켓 프로토콜은 최초에 한 번 커넥션을 맺고 나면 한쪽이 의도적으로 연결을 끊기 전까지 &lt;strong&gt;양방향 파이프라인이 계속 유지&lt;/strong&gt;된다.&lt;br /&gt;
서버가 원할 때 언제든 단말로 패킷을 보낼 수 있는 실시간 푸시의 기반이 된다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
%% 노드 스타일 정의
  classDef client fill:#ffffff,stroke:#333333,stroke-width:2px;
  classDef lb fill:#ffffff,stroke:#333333,stroke-width:2px;
  classDef server fill:#f4f9ff,stroke:#2196f3,stroke-width:2px;
  classDef database fill:#fff9f4,stroke:#ff9800,stroke-width:2px;

%% 1단계: 최상단 클라이언트
  Client[&quot;📱 모바일 사용자&quot;]:::client

%% 2단계: 로드밸런서
  LB[&quot;⚖️ 로드밸런서&quot;]:::lb

%% 3단계: 서버 레이어
  WS_Server[&quot;💻 ➆ 웹소켓 서버&amp;lt;br&amp;gt;(양방향 위치 정보)&quot;]:::server
  API_Server[&quot;💻 API 서버&amp;lt;br&amp;gt;(사용자 관리&amp;lt;br&amp;gt;친구 관리&amp;lt;br&amp;gt;인증 및 기타 기능)&quot;]:::server

%% 4단계: 하단 컴포넌트 레이어
  Redis[(&quot;🛢️ 레디스 펍/섭&amp;lt;br&amp;gt;(Publish/Subscribe,&amp;lt;br&amp;gt;Pub/Sub)&quot;)]:::database
  Cache[&quot;💾 캐시&amp;lt;br&amp;gt;(위치 정보 캐시)&quot;]:::database
  DB_Location[(&quot;🛢️ 위치 이동 이력&amp;lt;br&amp;gt;데이터베이스&quot;)]:::database
  DB_User[(&quot;🛢️ 사용자 데이터베이스&amp;lt;br&amp;gt;(사용자 정보, 친구 관계)&quot;)]:::database

%% ★ 위치 고정 트릭: 하단 컴포넌트 좌-&amp;gt;우 정렬 순서를 최상단에 배치하여 위치를 먼저 확정
  Redis ~~~ Cache
  Cache ~~~ DB_Location
  DB_Location ~~~ DB_User

%% --------------------------------------------------------
%% 화살표 흐름 정의
%% --------------------------------------------------------

%% 모바일 사용자 &amp;lt;-&amp;gt; 로드밸런서
  Client &amp;lt;--&amp;gt;|&quot;① (WebSocket, WS)&quot;| LB
  Client -- &quot;http&quot; --&amp;gt; LB

%% 로드밸런서 -&amp;gt; 서버 레이어
  LB &amp;lt;--&amp;gt;|②| WS_Server
  LB --&amp;gt; API_Server

%% 웹소켓 서버 &amp;lt;-&amp;gt; 레디스 펍/섭 (두 선 모두 ⑤ 대입)
  WS_Server --&amp;gt;|⑤| Redis
  Redis --&amp;gt;|⑥| WS_Server

%% 웹소켓 서버 -&amp;gt; 하단 저장소들
  WS_Server --&amp;gt;|④| Cache
  WS_Server --&amp;gt;|③| DB_Location
  WS_Server --&amp;gt; DB_User

%% API 서버 -&amp;gt; 사용자 데이터베이스
  API_Server --&amp;gt; DB_User

%% --------------------------------------------------------
%% 스타일 수정 (순서 변경에 따른 빨간색 인덱스 완벽 재정렬)
%% --------------------------------------------------------
  linkStyle 3 stroke:red,stroke-width:2px,color:red;
  linkStyle 5 stroke:red,stroke-width:2px,color:red;
  linkStyle 7 stroke:red,stroke-width:2px,color:red;
  linkStyle 8 stroke:red,stroke-width:2px,color:red;
  linkStyle 9 stroke:red,stroke-width:2px,color:red;
  linkStyle 10 stroke:red,stroke-width:2px,color:red;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;① 위치 전송:&lt;/strong&gt; 모바일 클라이언트가 위도, 경도 좌표를 담은 패킷을 로드밸런서로 전송한다. 이 통신은 상시 열려있는 웹소켓 채널을 이용한다.&lt;br /&gt;
&lt;strong&gt;② 이벤트 전달:&lt;/strong&gt; 로드밸런서는 이미 수립되어 있는 전용 커넥션을 타고 해당 이벤트를 담당 웹소켓 서버 인스턴스로 넘긴다.&lt;br /&gt;
&lt;strong&gt;③ 이력 로깅:&lt;/strong&gt; 웹소켓 서버는 유저의 동선 트래킹을 위해 이 위치 정보를 비동기식으로 위치 이동 이력 DB에 적재한다.&lt;br /&gt;
&lt;strong&gt;④ 캐시 업데이트 및 메모리 갱신:&lt;/strong&gt; 웹소켓 서버는 레디스 위치 캐시의 해당 유저 좌표를 신규 좌표로 업데이트하고 TTL도 새로 초기화한다. &lt;strong&gt;이와 동시에 웹소켓 서버 내부의 연결 핸들러 세션 변수(로컬 메모리)에도 이 값을 즉시 덮어쓴다.&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;⑤ 이벤트 발행:&lt;/strong&gt; 웹소켓 서버는 레디스 Pub/Sub 서버 상에 개설된 해당 유저 전용 채널로 “위치 변경 이벤트”를 발행한다. (③~⑤ 단계는 내부적으로 &lt;strong&gt;병렬&lt;/strong&gt; 처리)&lt;br /&gt;
&lt;strong&gt;⑥ 브로드캐스트:&lt;/strong&gt; 레디스 Pub/Sub 서버가 해당 유저 채널을 구독 중인 모든 수신처(즉, 현재 온라인 상태인 친구들의 웹소켓 서버 핸들러)로 이 이벤트를 전파한다.&lt;br /&gt;
&lt;strong&gt;⑦ 실시간 거리 재계산:&lt;/strong&gt; 이 이벤트를 수신한 친구 측의 웹소켓 서버는 이벤트를 보낸 유저의 새 좌표와, 자신이 관리 중인 타겟 유저의 좌표(④에서 세션 변수에 담아둔 로컬 메모리 값) 사이의 직선 거리를 연산한다.&lt;br /&gt;
&lt;strong&gt;➇ 조건별 푸시:&lt;/strong&gt; 계산 결과 거리가 서비스 반경(5마일) 이내라면, 웹소켓 연결을 통해 새 위치와 타임스탬프 정보를 친구의 모바일 단말 화면으로 즉시 push 한다. 검색 범위를 벗어났다면 패킷을 전송하지 않고 드롭한다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;💡④에서 저장하는 세션 변수 저장은 왜 하는 것일까?&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;⑦의 연산 과정과 직결되며, &lt;a href=&quot;#32-웹소켓-클라이언트-초기화&quot;&gt;3.2. 웹소켓 클라이언트 초기화&lt;/a&gt; 에서 더 심도있게 다룬다.&lt;br /&gt;
내 위치가 바뀔 때마다 매번 무거운 레디스 캐시나 DB를 다시 쿼리해서 대조하는 것은 비용이 너무 크기 때문에, 웹소켓 서버가 커넥션 핸들러 인스턴스 내부 변수에 유저들의 
최근 좌표를 항상 들고 있다가 이벤트가 날아오는 즉시 메모리 상에서 초고속으로 연산(⑦단계)하기 위해 저장해둔다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;223-친구에게-위치-변경-내역이-전파되는-상세-라우팅-흐름&quot;&gt;2.2.3. 친구에게 위치 변경 내역이 전파되는 상세 라우팅 흐름&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;#223-친구에게-위치-변경-내역이-전파되는-상세-라우팅-흐름&quot;&gt;2.2.2. 사용자 위치 변경 시 일어나는 일&lt;/a&gt; 중 &lt;strong&gt;레디스 Pub/Sub(⑤~⑥)을 관통하여 수많은 친구 유저들에게 메시지가&lt;/strong&gt; 
&lt;strong&gt;어떻게 종단간(End-to-End)으로 확산&lt;/strong&gt;되는지 구체적인 토폴로지를 다시 한번 자세히 보자.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
%% 노드 스타일 정의
  classDef client fill:#ffffff,stroke:#333333,stroke-width:2px;
  classDef ws fill:#ffffff,stroke:#333333,stroke-width:2px;
  classDef channel fill:#fff9f4,stroke:#ff9800,stroke-width:2px;

%% 1단계: 최상단 이벤트 발생 사용자
  User1[&quot;📱 사용자 1&quot;]:::client
  User5[&quot;📱 사용자 5&quot;]:::client

%% 2단계: 웹소켓 서버 (상단 수신부)
  subgraph WS_Top [&quot;웹소켓 서버&quot;]
    WS_Conn1[&quot;사용자 1의&amp;lt;br/&amp;gt;WS 연결&quot;]:::ws
    WS_Conn5[&quot;사용자 5의&amp;lt;br/&amp;gt;WS 연결&quot;]:::ws
  end

%% 3단계: 레디스 펍/섭 메시지 브로커 (중앙)
  subgraph Redis_Layer [&quot;레디스 펍/섭&quot;]
    Ch1[(&quot;🛢️ 사용자 1의 채널&quot;)]:::channel
    Ch5[(&quot;🛢️ 사용자 5의 채널&quot;)]:::channel
  end

%% 4단계: 웹소켓 서버 (하단 송신부)
  subgraph WS_Bottom [&quot;웹소켓 서버&quot;]
    WS_Conn2[&quot;사용자 2의&amp;lt;br/&amp;gt;WS 연결&quot;]:::ws
    WS_Conn3[&quot;사용자 3의&amp;lt;br/&amp;gt;WS 연결&quot;]:::ws
    WS_Conn4[&quot;사용자 4의&amp;lt;br/&amp;gt;WS 연결&quot;]:::ws
    WS_Conn6[&quot;사용자 6의&amp;lt;br/&amp;gt;WS 연결&quot;]:::ws
  end

%% 5단계: 최하단 메시지 수신 친구들
  User2[&quot;📱 사용자 2&quot;]:::client
  User3[&quot;📱 사용자 3&quot;]:::client
  User4[&quot;📱 사용자 4&quot;]:::client
  User6[&quot;📱 사용자 6&quot;]:::client

%% --------------------------------------------------------
%% 화살표 흐름 및 원본 넘버링 반영
%% --------------------------------------------------------

%% 상단 사용자 -&amp;gt; 웹소켓 서버 수신
  User1 --&amp;gt;|&quot;① 사용자 1의 위치&quot;| WS_Conn1
  User5 --&amp;gt; WS_Conn5

%% 웹소켓 서버 -&amp;gt; 레디스 채널 발행
  WS_Conn1 --&amp;gt;|&quot;② 발행&quot;| Ch1
  WS_Conn5 --&amp;gt;|&quot;발행&quot;| Ch5

%% 레디스 채널 -&amp;gt; 하단 웹소켓 서버 구독
  Ch1 --&amp;gt;|&quot;③ 구독&quot;| WS_Conn2
  Ch1 --&amp;gt;|&quot;구독&quot;| WS_Conn3
  Ch1 --&amp;gt;|&quot;구독&quot;| WS_Conn4

  Ch5 --&amp;gt;|&quot;구독&quot;| WS_Conn4
  Ch5 --&amp;gt;|&quot;구독&quot;| WS_Conn6

%% 하단 웹소켓 서버 -&amp;gt; 최종 수신자 앱으로 전송
  WS_Conn2 --&amp;gt;|&quot;④ 친구의 위치 정보 변경 내역&quot;| User2
  WS_Conn3 --&amp;gt; User3
  WS_Conn4 --&amp;gt; User4
  WS_Conn6 --&amp;gt; User6

%% 서브그래프 테두리 스타일
  style WS_Top fill:none,stroke:#999999,stroke-width:1px,stroke-dasharray: 5 5;
  style Redis_Layer fill:none,stroke:#999999,stroke-width:1px,stroke-dasharray: 5 5;
  style WS_Bottom fill:none,stroke:#999999,stroke-width:1px,stroke-dasharray: 5 5;

%% --------------------------------------------------------
%% 숫자가 붙은 화살표(①, ②, ③, ④)만 빨간색 스타일 적용
%% --------------------------------------------------------
  linkStyle 0 stroke:red,stroke-width:2px,color:red;
  linkStyle 2 stroke:red,stroke-width:2px,color:red;
  linkStyle 4 stroke:red,stroke-width:2px,color:red;
  linkStyle 9 stroke:red,stroke-width:2px,color:red;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;① 위치 유입:&lt;/strong&gt; 사용자 1의 물리적 위치가 이동하면, 변경된 내역이 사용자 1과 실시간 상시 연결을 유지하고 있는 웹소켓 서버 레이어로 최초 전송된다.&lt;br /&gt;
&lt;strong&gt;② 메시지 인입(Publish):&lt;/strong&gt; 수신측 웹소켓 서버 인스턴스는 해당 데이터를 랩핑하여 레디스 Pub/Sub 서버 내부에 개설된 &lt;strong&gt;사용자 1 전용 채널&lt;/strong&gt;로 이벤트를 즉시 발행한다.&lt;br /&gt;
&lt;strong&gt;③ 분산 브로드캐스트(Subscribe):&lt;/strong&gt; 레디스 Pub/Sub 서버는 채널의 구독 관계를 해석하여 이 변경 내역을 모든 구독자에게 동시 전파한다. 이때 구독자는 &lt;strong&gt;사용자 1과 친구 관계를 맺고 있으면서 현재 온라인을&lt;/strong&gt; 
&lt;strong&gt;유지 중인 모든 친구들의 웹소켓 연결 핸들러&lt;/strong&gt;가 된다.&lt;br /&gt;
&lt;strong&gt;④ 조건별 최종 푸시:&lt;/strong&gt; 위치 변경 내역을 보낸 유저와 구독자 사이의 직선 거리가 5마일 이내이면, 새로운 위치 좌표와 타임스탬프 정보 파이프라인을 타고 최종 사용자 2의 모바일 단말 스크린으로 전송된다.&lt;/p&gt;

&lt;p&gt;이 브로드캐스트 프로세스는 해당 채널을 링크하고 있는 모든 구독자들에게 완전히 독립적이고 병렬적으로 반복 적용된다.&lt;/p&gt;

&lt;p&gt;앞서 한 사용자당 평균 400명의 친구가 있고 그 중 10% 내외가 인근에서 활성 상태일 것으로 예상했으므로, 한 사용자의 발걸음이 옮겨질 때마다 백엔드 내부에서는 
&lt;strong&gt;평균 40건 안팎의 실시간 위치 전송 이벤트가 순식간에 사방으로 뻗어나가게 된다.&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-실시간-통신을-위한-웹소켓-api-설계&quot;&gt;2.3. 실시간 통신을 위한 웹소켓 API 설계&lt;/h2&gt;

&lt;p&gt;사용자는 웹소켓 프로토콜을 통해 위치 정보 내역을 전송하고 수신한다.&lt;br /&gt;
최소한 아래 API 인터페이스가 구비되어야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;[서버 수신 API] 주기적인 위치 정보 갱신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;기능: 클라이언트가 30초마다 위경도 좌표를 전송할 때 사용한다.&lt;/li&gt;
      &lt;li&gt;Request(클라이언트 → 웹소켓 서버): 클라이언트는 위도, 경도, 시각 정보를 전송&lt;/li&gt;
      &lt;li&gt;Response(웹소켓 서버 → 클라이언트): 없음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;[클라이언트 수신 API] 친구 위치 변경 알림(단방향 이벤트)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;기능: 클라이언트가 내 주변 반경 이내에 있는 친구의 위치가 실시간으로 갱신되었을 때 서버가 단말로 밀어주는 이벤트 알림&lt;/li&gt;
      &lt;li&gt;전송되는 데이터(웹소켓 서버 → 클라이언트): 친구 위치 데이터와 변경된 시각을 나타내는 타임스탬프&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;[서버 수신 API] 웹소켓 연결 초기화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;기능: 유저가 앱을 켜고 웹소켓 채널을 최초 수립할 때 호출하는 API&lt;/li&gt;
      &lt;li&gt;Request(클라이언트 → 웹소켓 서버): 클라이언트는 위도, 경도, 시각 정보를 전송&lt;/li&gt;
      &lt;li&gt;Response(웹소켓 서버 → 클라이언트): 클라이언트는 현재 온라인 상태인 내 주변 친구들의 위치 정보 데이터 리스트를 한 번에 동기식으로 내려받음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;[클라이언트 수신 API] 새 친구 구독 알림(단방향 이벤트)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;기능: 서비스 이용 중 새 친구가 추가되거나 콜백 이벤트에 의해 친구 채널과의 동적 연결(구독 관계 확립)이 필요할 때 호출&lt;/li&gt;
      &lt;li&gt;전송되는 데이터(웹소켓 서버 → 클라이언트): 웹소켓 서버는 친구 ID 전송, 이 알림과 함께 해당 친구의 가장 최근 위경도, 시각 정보가 패킷에 포함되어 클라이언트로 최종 전달됨&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;[클라이언트 수신 API] 구독 해지 알림(단방향 이벤트)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;기능: 특정 친구의 관계가 끊어지거나 비활성화되어, 해당 친구의 채널 구독을 중단했음을 서버가 알려주는 이벤트&lt;/li&gt;
      &lt;li&gt;전송되는 데이터(웹소켓 서버 → 클라이언트): 웹소켓 서버는 친구 ID 전송&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;💡’주기적인 위치 정보 갱신’과 ‘웹소켓 연결 초기화’의 인풋 포맷은 왜 완전히 똑같으며, 두 API의 차이는 무엇일까?&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;클라이언트가 웹소켓 서버로 던지는 인풋 데이터만 같은 뿐, 백엔드 내부의 동작과 아웃풋 메커니즘은 완전히 별개로 움직인다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;strong&gt;웹소켓 연결 초기화&lt;/strong&gt;
      &lt;ul&gt;
        &lt;li&gt;유저가 접속하여 ‘나 이제 온라인이 되었으니, 레이더를 구성해’라고 하는 행위&lt;/li&gt;
        &lt;li&gt;웹소켓 서버는 이 요청을 받으면 유저의 정보를 세팅하고, 레디스 캐시를 조회하여 현재 온라인 상태인 &lt;strong&gt;모든 주변 친구의 전체 좌표 세트를 모아서 클라이언트 단말로 동기식 응답을 반환&lt;/strong&gt;함&lt;/li&gt;
        &lt;li&gt;또한, 내 모든 친구의 채널을 일괄 구독하는 무거운 작업이 수반됨&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;주기적인 위치 정보 갱신&lt;/strong&gt;
      &lt;ul&gt;
        &lt;li&gt;초기화가 끝난 유저가 30초마다 단순히 내 현재 좌표만 계속 업데이트하는 가벼운 동기화 작업&lt;/li&gt;
        &lt;li&gt;웹소켓 서버는 이를 받으면 Pub/Sub 서버로 메시지를 던질 뿐, 요청을 보낸 단말 쪽으로는 어떠한 응답 데이터도 돌려주지 않고 통신을 끝냄&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;24-데이터-모델링-및-저장소-선택-배경&quot;&gt;2.4. 데이터 모델링 및 저장소 선택 배경&lt;/h2&gt;

&lt;h3 id=&quot;241-위치-정보-캐시redis&quot;&gt;2.4.1. 위치 정보 캐시(Redis)&lt;/h3&gt;

&lt;p&gt;‘주변 친구’ 기능을 켠 활성 상태 친구의 &lt;strong&gt;가장 최근 위치 하나만&lt;/strong&gt; 보관하면 충분하므로, 초고속 읽기/쓰기가 가능한 레디스를 캐시로 구현한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Key: 사용자 ID&lt;/li&gt;
  &lt;li&gt;Value: {위도, 경도, 시각}&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;💡위치 정보 저장에 일반DB를 사용하지 않는 이유는?&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;이 서비스는 오직 사용자의 &lt;strong&gt;현재 위치&lt;/strong&gt;만을 사용하므로 유저당 레코드 하나만 충분하다.&lt;br /&gt;
레디스는 TTL 기능을 지원하기 때문에, 10분간 갱신이 없는 비활성 유저 정보를 시스템이 수동으로 지우는 로직을 짤 필요 없이 캐시 레이어에서 자동으로 안전하게 파기할 수 있다.&lt;/p&gt;

  &lt;p&gt;또한 위치 데이터는 영속성이 필수가 아니다.&lt;br /&gt;
레디스 서버 하나가 죽더라도 새 장비로 교체한 뒤 유저들이 30초 주기로 쏘는 새 위치 패킷이 한두 번만 인입되면 캐시가 금방 자동으로 채워지는 &lt;strong&gt;자동 Warmed up 구조&lt;/strong&gt;를 띠고 있어 
서비스 영향도가 매우 적다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;242-위치-이동-이력-dbcassandra&quot;&gt;2.4.2. 위치 이동 이력 DB(Cassandra)&lt;/h3&gt;

&lt;p&gt;유저의 실시간 위치 변경 히스토리를 누적 저장하는 테이블이다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;데이터: 사용자 ID, 위도, 경도, 타임스탬프&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;💡왜 대규모 이력 저장을 위해 카산드라가 강력 추천될까?&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;카산드라는 전통적인 RDBMS가 아니라, 대규모 데이터 분산 처리에 특화된 &lt;strong&gt;NoSQL 와이드 컬럼 스토어(Wide-Column Store)&lt;/strong&gt;이다.&lt;/p&gt;

  &lt;p&gt;초당 334,000건에 달하는 &lt;strong&gt;엄청난 쓰기 부하&lt;/strong&gt;를 감당해야 하는 이력 데이터의 특성상 일반 RDBMS는 디스크 I/O 병목이나 트랜잭션 Lock 비용 때문에 서버 한 대로 절대 버티지 못한다.&lt;/p&gt;

  &lt;p&gt;반면 카산드라는 디스크에 데이터를 순차적으로 추가(Append-only)하는 LSM(Log-Structured Merge-tree) 트리 기반의 고속 쓰기 아키텍처를 가진다.&lt;br /&gt;
또한 마스터 노드가 없는 분산 구조이므로, 데이터 양이 늘어나면 장비를 Scale-out 하기만 하면 성능이 선형적으로 늘어난다.&lt;br /&gt;
RDBMS로 구현하려면 유저 ID를 기준으로 수동 샤딩 레이어를 개발자가 직접 구현하고 운영 관리에 큰 리소스를 쏟아야 하지만, 카산드라는 내장된 파티셔닝 키 설계를 통해 부하를 모든 샤드(노드)에 알아서 고르게 분산시켜 주므로 
운영 관리 측면에서 압도적으로 유리하다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;💡LSM(Log-Structured Merge-tree) 트리&lt;/strong&gt;&lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;&lt;strong&gt;메모리에 먼저 저장&lt;/strong&gt;: 데이터가 들어오면 디스크가 아닌 &lt;strong&gt;메모리에 가장 먼저&lt;/strong&gt; 초고속으로 기록함&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;차례대로 이어쓰기(Append-only)&lt;/strong&gt;: 메모리가 가득 차면 디스크 파일 끝에 &lt;strong&gt;차례대로 이어쓰기&lt;/strong&gt;를 함, 위치를 찾아 헤매는 과정이 없어서 디스크 쓰기 속도가 압도적으로 빠름&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;나중에 몰아서 정리(Compaction)&lt;/strong&gt;: 백그라운드 스레드가 디스크에 쌓인 파일들을 &lt;strong&gt;나중에 하나로 합치고 최신 데이터만 남기며 정리&lt;/strong&gt;함&lt;/li&gt;
  &lt;/ul&gt;

  &lt;p&gt;즉, 디스크를 매번 뒤져가며 수정하는 대신, &lt;strong&gt;“일단 메모리에 빠르게 모았다가 디스크 끝에 차곡차곡 이어쓰고, 정리는 나중에 백그라운드에서 몰아서 하는” Write 특화 구조&lt;/strong&gt;이다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;💡B-Tree(RDBMS) vs LSM 트리(NoSQL)&lt;/strong&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;특징&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;B-Tree(전통적 RDBMS)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;LSM 트리(카산드라 등 NoSQL)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;디스크 쓰기 메커니즘&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;지정된 페이지 위치에 덮어쓰기&lt;strong&gt;(랜덤 쓰기)&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;파일 끝에 차례대로 이어쓰기&lt;strong&gt;(순차 쓰기)&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Write 성능&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;인덱스 정렬 및 Lock 비용으로 상대적으로 &lt;strong&gt;느림&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;메모리 기록 후 일괄 처리하므로 &lt;strong&gt;압도적으로 빠름&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Read 성능&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;정렬된 인덱스 경로를 타고 바로 찾아가므로 &lt;strong&gt;매우 빠름&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;여러 SSTable(Sorted String Table) 파일을 뒤져야 하므로 상대적으로 &lt;strong&gt;느림&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;최적화된 서비스 형태&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;금융, 결제 등 &lt;strong&gt;읽기/수정&lt;/strong&gt;이 잦고 일관성이 중요한 서비스&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;로그, SNS 피드, &lt;strong&gt;실시간 위치 트래킹&lt;/strong&gt; 등 대규모 쓰기가 폭발하는 서비스&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;💡SSTable(Sorted String Table)&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;LSM 트리 구조에서 디스크에 저장되는 파일 포맷으로, 말 그대로 “Key를 기준으로 정렬된(Sorted), 문자열(String)들의 테이블”이라는 뜻이다.&lt;br /&gt;
메모리에 대량으로 쌓여있던 실시간 위치 데이터가 디스크로 한 번에 내려올 때 바로 이 SSTable 형태로 저장된다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-시스템-확장성을-고려한-상세-설계&quot;&gt;3. 시스템 확장성을 고려한 상세 설계&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;#2-개략적-아키텍처-설계&quot;&gt;2. 개략적 아키텍처 설계&lt;/a&gt;는 대다수의 시스템에서 잘 작동하지만, 여기서 목표로 하는 동시 접속자 1,000만 명 규모에서는 대량의 병목 현상이 발생하게 된다.&lt;br /&gt;
여기서는 대용량 트래픽 속에서 각 컴포넌트들을 어떻게 확장하고, 시스템 내부에서 어떤 연쇄 반응을 통해 자원을 최적화할 수 있는지 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-stateless-api-서버-vs-stateful-웹소켓-서버-확장-전략graceful-draining&quot;&gt;3.1. Stateless API 서버 vs Stateful 웹소켓 서버 확장 전략(Graceful Draining)&lt;/h2&gt;

&lt;h3 id=&quot;311-stateless-api-서버의-확장&quot;&gt;3.1.1. Stateless API 서버의 확장&lt;/h3&gt;

&lt;p&gt;RESTful API 서버의 규모 확장 방법은 이미 널리 알려져 있다.&lt;br /&gt;
서버가 세션 상태를 품고 있지 않기 때문에, CPU 사용률이나 부하 상태, I/O 상태에 따라 Auto-Scaling 그룹을 지정하여 서버 클러스터의 규모를 자동으로 늘리고 줄이는 동적 확장이 자유롭다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;312-stateful-웹소켓-서버의-확장&quot;&gt;3.1.2. Stateful 웹소켓 서버의 확장&lt;/h3&gt;

&lt;p&gt;웹소켓 서버 클러스터 역시 사용률에 따라 규모를 자동으로 늘리는 것은 그다지 어렵지 않다.&lt;br /&gt;
하지만 웹소켓 서버는 유저와의 연결 지속성을 보장해야 하는 &lt;strong&gt;Stateful 서버&lt;/strong&gt;이기 때문에, 기존 서버를 제거할 때는 매우 신중해야 한다.&lt;/p&gt;

&lt;p&gt;서버 장비를 안전하게 제거하려면 우아하게 종료(Graceful Draining)하는 연쇄 프로세스가 필수적이다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;무작위로 장비를 끄는 것이 아니라, 로드밸런서가 인식하는 해당 노드의 상태를 ‘연결 종료 중(draining)’으로 먼저 변경&lt;/li&gt;
  &lt;li&gt;해당 서버로는 더 이상 새로운 웹소켓 연결이 만들어지지 않음&lt;/li&gt;
  &lt;li&gt;기존에 연결되어 있던 유저들의 커넥션 세션이 자연스럽게 하나씩 종료되기를 기다림&lt;/li&gt;
  &lt;li&gt;모든 웹소켓 연결이 안전하게 종료되면 서버 제거&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;결국 Stateful 서버 클러스터의 규모를 자동으로 확장하려면, 이러한 Draining 메커니즘을 정교하게 제어할 수 있는 좋은 로드밸런서가 앞단에 있어야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-웹소켓-클라이언트-초기화&quot;&gt;3.2. 웹소켓 클라이언트 초기화&lt;/h2&gt;

&lt;p&gt;모바일 클라이언트가 기동되면 웹소켓 클러스터 내의 서버 가운데 한 대와 지속성 웹소켓 연결을 맺는다.&lt;br /&gt;
‘지속성 연결’이라 부르는 이유는 이 연결이 오랜 시간 유지되기 때문이다.&lt;/p&gt;

&lt;p&gt;웹소켓 연결이 초기화되면 클라이언트는 해당 모바일 단말을 이용 중인 사용자의 위치 정보를 전송하며, 그 정보를 받은 웹소켓 연결 핸들러들은 아래 작업을 순차적으로 수행한다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
  actor Client as 📱 모바일 클라이언트
  participant WS as 💻 웹소켓 핸들러
  participant Cache as 💾 위치 정보 캐시 (Redis)
  participant UserDB as 🛢️ 사용자 DB
  participant PubSub as 🛢️ 레디스 펍/섭

  Client-&amp;gt;&amp;gt;WS: 웹소켓 연결 및 현재 내 위치 좌표 전송
  WS-&amp;gt;&amp;gt;Cache: ① 위치 정보 캐시의 해당 사용자 위치 갱신
  WS-&amp;gt;&amp;gt;WS: ② 핸들러 내부 변수에 해당 위치 저장
  WS-&amp;gt;&amp;gt;UserDB: ③ 사용자 DB에서 해당 사용자의 모든 친구 정보 조회
  UserDB--&amp;gt;&amp;gt;WS: 친구 ID 리스트 반환
  WS-&amp;gt;&amp;gt;Cache: ④ 위치 정보 캐시에 모든 친구 위치 일괄 요청
  Cache--&amp;gt;&amp;gt;WS: 활성화 상태인 친구들의 좌표 반환
  Note over WS: ⑤ 내 좌표와 친구 좌표 간 직선거리 계산
  WS-&amp;gt;&amp;gt;Client: 반경 내 친구 상세 정보, 위치, 확인 시각 푸시
  WS-&amp;gt;&amp;gt;PubSub: ⑥ 모든 친구의 레디스 서버 펍/섭 채널 구독
  WS-&amp;gt;&amp;gt;PubSub: ⑦ 내 전용 채널에 현재 위치 발행 (Publish)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;① &lt;strong&gt;위치 정보 캐시에 보관된 해당 사용자의 위치를 갱신&lt;/strong&gt;함&lt;br /&gt;
② 해당 위치 정보는 바로 뒤이은 계산 과정에 이용되므로 &lt;strong&gt;연결 핸들러 내의 변수에 저장&lt;/strong&gt;해 둠&lt;br /&gt;
③ &lt;strong&gt;사용자 DB를 뒤져 해당 사용자의 모든 친구 정보를 가져옴&lt;/strong&gt;&lt;br /&gt;
④ &lt;strong&gt;위치 정보 캐시에 일괄 요청을 보내어 모든 친구의 위치를 한 번에 가져옴&lt;/strong&gt;&lt;br /&gt;
캐시에 보관하는 모든 항목의 TTL은 비활성화 타임아웃 시간과 동일한 값으로 설정되어 있으므로 비활성화 친구의 위치는 캐시에 없으며, 자연스럽게 걸러짐&lt;br /&gt;
⑤ 캐시가 돌려준 친구 위치 각각에 대해 웹소켓 서버는 &lt;strong&gt;해당 친구와 사용자 사이의 거리를 계산&lt;/strong&gt;함&lt;br /&gt;
그 거리가 검색 반경 이내이면 해당 친구의 상세 정보, 위치, 해당 위치가 마지막으로 확인된 시각을 웹소켓 연결을 통해 클라이언트에 반환함&lt;br /&gt;
⑥ 웹소켓 서버는 각 친구의 레디스 Pub/Sub 채널을 구독함&lt;br /&gt;
채널 구독 비용은 저렴하므로 사용자는 &lt;strong&gt;친구의 활성화/비활성화 상태에 관계없이 모든 친구 채널을 구독&lt;/strong&gt;할 수 있으며, 이는 아키텍처 구조를 단순하게 만들어줌&lt;br /&gt;
⑦ 사용자의 현재 위치를 레디스 Pub/Sub 서버의 전용 채널을 통해 모든 친구에게 발행함&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;💡⑤에서 클라이언트에게 주는 ‘해당 친구의 정보’를 클라이언트는 어디에 사용할까?&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;클라이언트 단말 앱은 이 데이터를 받아 화면 지도 위에 친구들의 핀 아이콘을 동적으로 그려내고 레이더 화면 목록을 채우는 데 사용한다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-위치-정보-캐시-샤딩과-장애-복구warmed-up-전략&quot;&gt;3.3. 위치 정보 캐시 샤딩과 장애 복구(Warmed Up) 전략&lt;/h2&gt;

&lt;h3 id=&quot;331-사용자-db의-scale-out&quot;&gt;3.3.1. 사용자 DB의 Scale-out&lt;/h3&gt;

&lt;p&gt;사용자 DB에는 사용자 상세 정보와 친구 관계 데이터가 저장되며, 이 거대한 양의 데이터는 한 대의 RDBMS 서버로는 감당할 수 없다.&lt;br /&gt;
하지만 사용자 ID를 기준으로 DB를 샤딩하면 RDBMS라 하더라도 부하를 모든 샤드에 고르게 분산시켜 Scale-out할 수 있으며, DB 운영 관리도 간편해진다.&lt;/p&gt;

&lt;p&gt;주의할 점은 웹소켓 서버들이 DB를 직접 쿼리하는 것이 아니라, 반드시 격리된 &lt;strong&gt;내부 API 서버 인터페이스를 통해 이용&lt;/strong&gt;해야만 시스템 안정성을 확보할 수 있다는 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;332-위치-정보-캐시redis-cache의-한계와-샤딩&quot;&gt;3.3.2. 위치 정보 캐시(Redis Cache)의 한계와 샤딩&lt;/h3&gt;

&lt;p&gt;위치 정보 캐시를 레디스를 사용하며, 각 항목의 key에는 TTL을 설정한다. 이 TTL은 해당 사용자의 위치 정보가 갱신될 때마다 계속 초기화되므로 최대 메모리 사용량은 일정 한도 
아래로 유지된다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;💡왜 최대 사용량이 일정 한도 아래로 유지될까?&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;계속 초기화되면 수명이 늘어나니까 데이터가 안 없어지고 무한히 쌓여서 메모리가 터지는 거 아닌가? 하는 생각할 수도 있다.&lt;/p&gt;

  &lt;p&gt;&lt;strong&gt;데이터는 ‘추가’되는 것이 아니라 ‘덮어쓰기’된다.&lt;/strong&gt;&lt;br /&gt;
30초마다 위치가 갱신될 때, 레디스에 새로운 데이터가 계속 쌓이는 것이 아니다.&lt;br /&gt;
유저당 딱 하나의 key(사용자 ID)만 가지고 값을 그 위에 &lt;strong&gt;덮어쓰기&lt;/strong&gt; 하는 구조이다.&lt;br /&gt;
예를 들어 사용자 A가 1시간 동안 앱을 쓰면서 위치를 100번 갱신하더라도, 레디스에 들어있는 사용자 A의 데이터는 늘 딱 1개이다.(= 활성 유저가 아무리 활발히 움직여도 용량은 늘어나지 않음)&lt;/p&gt;

  &lt;p&gt;&lt;strong&gt;앱을 끈 유저는 10분 뒤에 ‘삭제’된다.&lt;/strong&gt;&lt;br /&gt;
유저가 앱을 종료하면 이 유저의 key는 더 이상 TTL이 갱신되지 않으므로 10분이 지나면 이 유저의 key가 메모리에서 삭제(Evict)된다.&lt;/p&gt;

  &lt;p&gt;즉, 레디스 캐시 메모리에 머무는 데이터의 총 개수는 아무리 많아도 ‘최근 10분간 한 번이라도 위치를 전송한 활성 사용자 수’를 절대 넘지 못한다.&lt;br /&gt;
레디스 최대 메모리 사용량 = 피크 타임 동시 접속자 수 * 100 byte&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;만일 천만 명의 사용자가 활성화 상태이며 위치 정보 보관에 유저당 100byte가 필요하다고 할 때 총 용량은 몇이며, 단일 레디스 서버로 버틸 수 있는지 알아보자.&lt;/p&gt;

&lt;p&gt;먼저 데이터 용량 측면과 초당 처리량(QPS) 측면을 쪼개서 계산해보아야 한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;메모리 용량 계산&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;10,000,000명 * 100byte = 1GB&lt;/li&gt;
      &lt;li&gt;1GB면 최신 레디스 서버 단 한 대로도 아주 넉넉하게 캐시 메모리에 올릴 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이제 &lt;strong&gt;CPU 처리량(QPS) 병목&lt;/strong&gt;을 계산해보자.&lt;br /&gt;
용량은 충분하지만 트래픽이 너무 많다.&lt;br /&gt;
천만 명의 활성 사용자가 30초마다 변경된 위치 정보를 계속 전송하면 레디스 서버가 감당해야 할 갱신 연산수는 &lt;strong&gt;초당 334,000건&lt;/strong&gt;에 달한다.&lt;br /&gt;
이는 최신 고사양 서버를 쓴다고 해도 단일 인스턴스의 싱글 스레드 연산으로는 엄청난 부담이 된다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;해답은 캐시 데이터 샤딩&lt;/strong&gt;이다.&lt;br /&gt;
각 사용자의 위치 정보는 서로 독립적인 데이터이므로, &lt;strong&gt;사용자 ID를 기준으로 여러 레디스 서버에 샤딩&lt;/strong&gt;하면 쓰기 트래픽 부하 또한 모든 노드로 고르게 분배할 수 있어서 
병목을 원천 해결할 수 있다.&lt;br /&gt;
또한 캐시 서버 한 대에 장애가 발생하더라도 새 서버로 바꾼 뒤 30초 주기의 위치 정보가 새로 채워지기를 기다리면 되는 &lt;strong&gt;자연 예열(Warmed Up)&lt;/strong&gt; 구조이므로 일부 유실이 
발생하더라도 시스템 전체가 붕괴하지 않고 유연하게 복구된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;34-레디스-pubsub-서버의-메모리-및-cpu-병목&quot;&gt;3.4. 레디스 Pub/Sub 서버의 메모리 및 CPU 병목&lt;/h2&gt;

&lt;p&gt;여기서는 모든 온라인 친구에게 실시간으로 위치 변경 내역을 전달하는 핵심 라우팅 계층으로 &lt;strong&gt;레디스 Pub/Sub&lt;/strong&gt; 서버를 활용한다.&lt;br /&gt;
레디스 Pub/Sub은 채널을 생성하고 관리하는 비용이 매우 저렴하여 대규모 메시지 라우팅에 아주 유리하다.&lt;/p&gt;

&lt;p&gt;구독자가 없는 채널로 전송된 메시지는 즉시 버려지며, 채널 관리를 위해 내부 해시 테이블과 연결 리스트에 최소한의 포인터 정보만 기록하므로 오프라인 사용자의 채널은 CPU 자원을 전혀 소모하지 않는다.&lt;br /&gt;
이를 기반으로 &lt;strong&gt;‘주변 친구’ 기능을 활용하는 모든 사용자(약 1억명)에게 전용 채널을 하나씩 미리 부여&lt;/strong&gt;하는 단순한 설계를 취할 수 있다.&lt;br /&gt;
친구가 온라인이든 오프라인이든 상관없이 무조건 구독 관계를 맺어둠으로써, 유저가 켜질 때마다 구독을 맺고 끊는 복잡한 제어 로직을 생략할 수 있게 된다.&lt;/p&gt;

&lt;p&gt;모든 유저에게 채널을 주면 메모리가 터지지는 않을까?&lt;br /&gt;
결론은 &lt;strong&gt;메모리는 전혀 병목이 되지 않는다.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;전체 유저의 10%인 &lt;strong&gt;1억 개&lt;/strong&gt;의 채널이 존재&lt;/li&gt;
  &lt;li&gt;평균적으로 한 유저의 친구 중 &lt;strong&gt;100명&lt;/strong&gt;이 동시에 주변 친구 기능을 켠 활성 상태라고 가정&lt;/li&gt;
  &lt;li&gt;레디스가 내부 자료구조(해시 테이블 및 연결 리스트)에서 구독자 한 명을 추적하기 위해 사용하는 포인터 메모리는 &lt;strong&gt;약 20byte&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 때 필요한 총 메모리 용량 수식은 아래와 같다.&lt;br /&gt;
\(\text{총 필요 메모리} = \frac{100,000,000 \text{ 개 (채널)} \times 20 \text{ 바이트} \times 100 \text{ 명}}{10^9} = 200 \text{ GB}\)&lt;/p&gt;

&lt;p&gt;여기서 분모에 위치한 \(10^9\)은 byte 단위를 GB로 변환하기 위함이다.(1GB = \(10^9\)Byte)&lt;/p&gt;

&lt;p&gt;계산 결과 총 &lt;strong&gt;200GB&lt;/strong&gt;의 메모리가 필요하다. 최근 엔터프라이즈 서버들은 한 대에 100GB 이상의 메모리를 장착하는 경우가 흔하므로, 모든 유저의 상시 구독 관계를 올리는 데 
&lt;strong&gt;고작 레디스 서버 2대면 충분하다는 뜻이다.&lt;/strong&gt;&lt;br /&gt;
즉, 아키텍처 단순화를 위해 투입할 만한 가치가 충분하며 메모리는 여유롭다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;341-병목은-메모리가-아니라-cpu-사용량&quot;&gt;3.4.1. 병목은 메모리가 아니라 CPU 사용량&lt;/h3&gt;

&lt;p&gt;메모리와 달리 CPU 연산 및 네트워크 대역폭은 심각한 병목을 마주한다.&lt;br /&gt;
주변 친구 기능을 위해 Pub/Sub 서버가 구독자들에게 보내야 하는 실시간 위치 정보 업데이트 양은 무려 &lt;strong&gt;초당 1,400만 건&lt;/strong&gt;에 달한다.&lt;/p&gt;

&lt;p&gt;초당 1,400만건에 대한 계산은 &lt;a href=&quot;#21-p2p-모델의-한계와-공용-백엔드-도입-배경&quot;&gt;2.1. P2P 모델의 한계와 공용 백엔드 도입 배경&lt;/a&gt; 을 참고하면 된다.&lt;/p&gt;

&lt;p&gt;기가비트 네트워크 카드를 탑재한 최신 고사양 서버 한 대가 보수적으로 감당할 수 있는 동시 구독 전송 건수를 100,000건이라고 가정해보자.&lt;br /&gt;
\(\text{필요한 레디스 펍/섭 서버 수} = \frac{14,000,000 \text{ 건 (초당 업데이트)}}{100,000 \text{ 건 (서버당 한계)}} = 140 \text{ 대}\)&lt;/p&gt;

&lt;p&gt;이 추정치에 따르면 최소 140대 안팎의 레디스 서버가 필요하다.&lt;br /&gt;
즉, 레디스 Pub/Sub 레이어의 핵심 과제는 메모리가 아닌 &lt;strong&gt;CPU 연산 부하 및 네트워크 I/O를 가르는 일&lt;/strong&gt;이며, 이를 처리하기 위해 거대한 &lt;strong&gt;분산 레디스 Pub/Sub 클러스터&lt;/strong&gt;가 반드시 필요하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;35-분산-레디스-펍섭-클러스터와-안정-해시consistent-hash-ring&quot;&gt;3.5. 분산 레디스 펍/섭 클러스터와 안정 해시(Consistent Hash Ring)&lt;/h2&gt;

&lt;p&gt;수백 대에 달하는 레디스 Pub/Sub 서버로 채널을 유연하게 분산하기 위해, 모든 채널이 독립적이라는 특성을 활용하여 &lt;strong&gt;발행할 사용자 ID를 기준으로 서버를 샤딩&lt;/strong&gt;한다.&lt;br /&gt;
이 때 서버의 동적 추가/제거 시 메시지 유실과 대규모 재구독 오버헤드를 막기 위해 &lt;strong&gt;안정 해시(Consistent Hash Ring)&lt;/strong&gt; 아키텍처를 도입한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;💡안정 해시 링의 개념&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;일반적인 나머지 연산(Modular) 기반의 샤딩 방식(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash(key) % 서버대수&lt;/code&gt;)은 서버가 한 대 추가되거나 죽었을 때, 완전히 다른 결과 값을 반환하여 전 세계 유저들의 
채널 위치를 한 순간에 바꿔버리는 치명적인 결함이 있다.&lt;/p&gt;

&lt;p&gt;반면 &lt;strong&gt;안정 해시&lt;/strong&gt;는 가상의 거대한 원형 링 위에 서버 노드들의 해시 값을 먼저 배치하고, 데이터 키(사용자 ID)의 해시 값이 링 위에서 시계 방향으로 순회하다가 처음 만나는 
서버 노드에 데이터를 할당하는 알고리즘이다.&lt;br /&gt;
이 방식을 쓰면 서버가 추가되거나 사라져도 링 전체 데이터가 유실되지 않고, &lt;strong&gt;오직 해당 서버와 인접한 소수의 데이터만 옆 서버로 재배치&lt;/strong&gt;되는 엄청난 이점을 얻는다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0605/hashring.png&quot; alt=&quot;안정 해시(Consistent hash)&quot; /&gt;&lt;/p&gt;

&lt;p&gt;레디스 자체의 내장 Pub/Sub 기능은 클러스터링 환경에서 모든 노드에 메시지를 브로드캐스트하는 비효율적인 구조를 가진다.&lt;br /&gt;
따라서 이 시스템의 해시 링 라우팅 규칙은 &lt;strong&gt;&lt;a href=&quot;https://etcd.io/&quot;&gt;etcd&lt;/a&gt;, &lt;a href=&quot;https://zookeeper.apache.org/&quot;&gt;주키퍼&lt;/a&gt; 같은 서비스 탐색(Service Discovery) 컴포넌트를 
중심축에 두고 웹소켓 서버 레이어에서 구현&lt;/strong&gt;한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;etcd나 주키퍼 같은 소규모 key-value 저장소에 현재 살아있는 레디스 Pub/Sub 서버 목록을 해시 링 데이터 형태로 저장
    &lt;ul&gt;
      &lt;li&gt;예) key: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config/pub_sub_ring&lt;/code&gt; / value: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[&quot;p_1&quot;, &quot;p_2&quot;, &quot;p_3&quot;, &quot;p_4&quot;]&lt;/code&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;웹소켓 서버들은 이 원본 해시 링의 상태를 상시 구독(Watch)하며 자신의 로컬 메모리에 사본으로 캐시한다.&lt;/li&gt;
  &lt;li&gt;애플리케이션 코드 레벨에서 사용자 ID가 인입되면, 프로그래밍 언어별로 검증된 오픈소스 안정 해시 라이브러리(또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TreeMap&lt;/code&gt;같은 자료구조를 활용한 커스텀 클래스 구현체)를 사용하여 해시 링 사본과 대조한다.&lt;br /&gt;
이를 통해 어떤 레디스 Pub/Sub 노드로 이벤트를 쏘거나 구독해야 할지 초고속으로 판별이 가능하다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;레디스 펍/섭 서버는 메시지를 발행할 채널이나 구독할 채널을 정해야 할 때 이 해시 링을 참조한다.
예를 들면 위 그림에서 채널 2는 레디스 펍/섭 서버 1번에서 관리되고 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;메시지를 발행할 레디스 Pub/Sub 서버 선정 및 발행 과정&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;웹소켓 서버가 특정 사용자 채널에 위치 정보 변경 내역을 발행하는 구체적인 아키텍처 제어 흐름은 다음과 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0605/hashring2.png&quot; alt=&quot;메시지를 발행할 레디스 펍/섭 서버 선정&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;① 레디스 Pub/Sub 서버 선정&lt;/strong&gt;: 웹소켓 서버는 해시 링을 참조하여 메시지를 발행할 레디스 Pub/Sub 서버를 선정한다.&lt;br /&gt;
정확한 정보는 서비스 탐색 컴포넌트에 보관되어 있으나 성능 효율을 높이고 싶다면 해시 링 사본을 웹소켓 서버에 캐시하는 것도 괜찮다.&lt;br /&gt;
다만 그 경우에는 웹소켓 서버는 해시 링 원본에 구독 관계를 설정하여 사본의 상태를 항상 원본과 동일하게 유지하도록 해야 한다.&lt;br /&gt;
&lt;strong&gt;② 위치 정보 변경 내역 발행&lt;/strong&gt;: 웹소켓 서버는 해당 서버가 관리하는 사용자 채널(그림상 채널 2)에 위치 정보 변경 내역을 발행한다.&lt;/p&gt;

&lt;p&gt;내가 구독해야 할 채널이 존재하는 레디스 Pub/Sub 서버를 매칭하고 찾아내는 과정 역시 이와 완전히 동일한 메커니즘으로 처리된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;36-레디스-펍섭-서버-클러스터-규모-확장-시-주의사항&quot;&gt;3.6. 레디스 펍/섭 서버 클러스터 규모 확장 시 주의사항&lt;/h2&gt;

&lt;p&gt;레디스 Pub/Sub 채널을 타고 흐르는 실시간 위치 패킷 자체는 배달 직후 메모리에서 증발하므로 Stateless 데이터에 가깝다.&lt;br /&gt;
하지만 &lt;strong&gt;누가 어떤 채널을 구독하고 있는지에 대한 매핑 리스트를 메모리에 상시 쥐고 있기 때문에 Pub/Sub 서버 클러스터는 엄연한 Stateful 서버 클러스터로 취급&lt;/strong&gt;해야 한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;오버 프로비저닝(Over-provisioning)&lt;/strong&gt;이란 피크 타임의 최대 트래픽 부하보다 시스템 자원(서버 대수, 사양)을 &lt;strong&gt;항상 훨씬 더 여유롭고 넉넉하게 미리 띄워두는 운영 전략&lt;/strong&gt;을 뜻한다.&lt;br /&gt;
오버 프로비저닝을 해두면 서버 자원을 넉넉하게 마련해두었기 때문에 트래픽이 높아져도 &lt;strong&gt;“부하 때문에 서버를 더 늘리거나 줄여야 할 원인” 자체가 시스템 내부에서 발생하지 않는다.&lt;/strong&gt;&lt;br /&gt;
따라서 오버 프로비저닝을 해두면 불필요한 크기 변화를 피할 수 있다.&lt;/p&gt;

&lt;p&gt;일반적인 Stateless API 서버들은 트래픽 패턴에 맞춰 실시간으로 대수를 조절하지만, Stateful Pub/Sub 서버 클러스터에서 그런 행위는 자살 행위와 다름없다.&lt;br /&gt;
Stateful 클러스터 환경에서 오버 프로비저닝은 불필요한 크기 변화를 피하기 위해 비용을 지불하고 &lt;strong&gt;인프라의 절대적인 정적 안정성&lt;/strong&gt;을 구매하는 고도의 트레이드 오프 전략이다.&lt;/p&gt;

&lt;p&gt;안정 해시를 썼다 할지라도 클러스터의 크기를 조정하는 순간(노드 추가/제거), 수많은 채널이 해시 링 위의 다른 레디스 서버들로 대거 이동하게 된다.&lt;br /&gt;
etcd가 웹소켓 서버들에게 링이 바뀌었음을 알리는 순간, 수천만 명의 연결을 쥐고 있던 웹소켓 서버들이 일제히 새 레디스 노드를 향해 &lt;strong&gt;엄청난 재구독 트래픽 폭풍&lt;/strong&gt;을 일으킨다.&lt;/p&gt;

&lt;p&gt;이 무거운 재구독 연산을 처리하느라 정작 유저들이 실시간으로 보내는 위치 정보 메시지 처리가 대거 누락되고 시스템 전체가 마비될 위험성이 크다.&lt;br /&gt;
따라서 불필요한 Scale in/out을 원천 차단하기 위해 애초에 자원을 과도할 정도로 넉넉하게 상시 유지(오버 프로비저닝)하는 것이 비용 대비 시스템 안정성 측면에서 훨씬 이득이다.&lt;/p&gt;

&lt;p&gt;만일 불가피하게 클러스터 크기를 조절해야 한다면, 하루 중 시스템 부하가 가장 낮은 새벽 시간대를 골라서 작업해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;분산 Pub/Sub 클러스터 크기 조절 순서&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;시스템에 가해지는 충격을 최소화하면서 클러스터의 규모를 안전하게 확장하는 3가지 실행 시나리오이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;새로운 링 크기를 계산&lt;/strong&gt;한다.
    &lt;ul&gt;
      &lt;li&gt;계산 결과에 따라 해시 링의 크기가 늘어나는 경우, 인프라 상에 물리적인 &lt;strong&gt;새 레디스 서버를 준비&lt;/strong&gt;하고 구동시킨다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;서비스 탐색 컴포넌트(etcd)에 저장된 &lt;strong&gt;해시 링의 key에 해당하는 value를 새로운 내용으로 갱신&lt;/strong&gt;한다.&lt;/li&gt;
  &lt;li&gt;변경사항이 전파되는 동안 대시보드를 모니터링한다.
    &lt;ul&gt;
      &lt;li&gt;이 때 일제히 재구독 연산을 수행하느라 &lt;strong&gt;웹소켓 클러스터의 CPU 사용량이 어느 정도 순간적으로 튀는 현상&lt;/strong&gt;이 명확하게 관측되어야 정상적으로 확장이 진행되고 있는 것이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Pub/Sub 노드 5와 6을 추가할 때의 해시 링 값 변화&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;만일 기존에 4대의 레디스로 운영되던 환경에 Pub/Sub 5번과 6번, 2개의 새로운 노드를 추가한다면 서비스 탐색 설정 저장소의 데이터 규칙은 다음과 같이 실시간으로 매핑 변경된다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;key: /config/pub_sub_ring&lt;/li&gt;
  &lt;li&gt;value: [“p_1”, “p_2”, “p_3”, “p_4”] → [“p_1”, “p_2”, “p_3”, “p_4”, “p_5”, “p_6”]&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;온콜(On-call) 엔지니어의 서버 교체 시나리오&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;온콜&lt;/strong&gt;은 시스템에 비상 얼럿이 떴을 때 24시간 언제든 즉각 긴급 대응할 수 있도록 대기하는 &lt;strong&gt;장애 조치 담당 엔지니어 교대 근무 로테이션&lt;/strong&gt;을 의미한다.&lt;/p&gt;

&lt;p&gt;다행히 기존 레디스 Pub/Sub 서버를 완전히 새 서버로 1:1 교체하는 운영 작업은 클러스터 크기 자체를 조정할 때보다 &lt;strong&gt;장애 충격이나 위험성이 훨씬 낮다.&lt;/strong&gt;&lt;br /&gt;
해시 링의 구조적 토폴로지가 통째로 바뀌는 것이 아니기 때문에, 링 전체 채널들이 대규모로 이동을 하는 사태가 발생하지 않고 &lt;strong&gt;오직 교체되는 특정 서버의 채널들만 핀포인트로 조치&lt;/strong&gt;하면 되기 때문이다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
  classDef component fill:#f4f9ff,stroke:#2196f3,stroke-width:2px;
  classDef action fill:#fff9f4,stroke:#ff9800,stroke-width:2px;

  Alert[&quot;🚨 레디스 펍/섭 노드 장애 감지&quot;] --&amp;gt; Engineer[&quot;👨‍💻 온콜 엔지니어 긴급 투입&quot;]
  Engineer --&amp;gt; Step1[&quot;① etcd 해시 링 값 수정&amp;lt;br&amp;gt;(장애 노드를 대기 노드로 교체)&quot;]:::action
  Step1 --&amp;gt; Step2[&quot;② 서비스 탐색 컴포넌트가&amp;lt;br&amp;gt;모든 웹소켓 서버에 변경 통지&quot;]:::action
  Step2 --&amp;gt; Step3[&quot;③ 웹소켓 연결 핸들러들이&amp;lt;br&amp;gt;자신이 쥔 구독 목록 전수 검사&quot;]:::action
  Step3 --&amp;gt; Step4[&quot;④ 장애 대상 채널들만 골라내어&amp;lt;br&amp;gt;새 레디스 노드로 재구독 확립&quot;]:::action

  style Alert fill:#ffebee,stroke:#c62828,stroke-width:2px;
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
  &lt;li&gt;갱신 전: [“p_1”, “p_2”, “p_3”, “p_4”]&lt;/li&gt;
  &lt;li&gt;갱신 후: [“p_1_new”, “p_2”, “p_3”, “p_4”]&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0605/hashring3.png&quot; alt=&quot;펍/섭 서버 교체&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;서비스 탐색 노드 교체:&lt;/strong&gt; 서비스 탐색 컴포넌트(etcd)의 해시 링 key에 해당하는 value 값을 즉시 갱신하여, &lt;strong&gt;장애가 발생한 기존 노드를 미리 대기 중이던 예비 노드로 교체한다.&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;예) [“p_1”, “p_2”, “p_3”, “p_4”] → [“p_1_new”, “p_2”, “p_3”, “p_4”]&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;웹소켓 클러스터 전파:&lt;/strong&gt; 교체 사실은 감시(Watch) 메커니즘을 통해 모든 웹소켓 서버로 즉각 통지되며, 통지를 받은 웹소켓 서버들은 백엔드 내부에서 실행 중인 &lt;strong&gt;각 연결 핸들러 세션들에게 새로운 Pub/Sub 서버의 채널을 다시 구독하도록 알린다.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;자체 재구독 검토 및 복구:&lt;/strong&gt; 통지를 받은 각 웹소켓 서버의 수많은 연결 핸들러 세션들은 자신이 구독 중이던 채널 목록을 들고 있으므로, 이 통지를 받는 즉시 그 모든 채널들을 새로운 해시 링 원본과 대조하여 &lt;strong&gt;“내가 관리하던 채널 중 새 서버(p_1_new)로 구독 관계를 다시 설정해야 하는 것이 있는지”를 검토하고 타겟 채널만 재연결&lt;/strong&gt;을 수립한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이러한 정교한 격리 복구 메커니즘 덕분에 멀쩡한 다른 Pub/Sub 노드들은 아무런 타격을 입지 않고 부하 폭풍 없이 실시간 위치 전파 서비스를 안전하게 유지할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;37-예외-케이스-처리-및-기능-확장&quot;&gt;3.7. 예외 케이스 처리 및 기능 확장&lt;/h2&gt;

&lt;p&gt;운영 시엔 정상적인 흐름 외에도 다양한 비즈니스 변화와 인프라 병목을 유발하는 엣지 케이스들이 존재한다.&lt;br /&gt;
시스템의 안정성을 유지하면서도 이를 유연하게 처리하는 아키텍처 제어 전략에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;371-친구-추가삭제-및-변경-처리&quot;&gt;3.7.1. 친구 추가/삭제 및 변경 처리&lt;/h3&gt;

&lt;p&gt;사용자가 앱을 사용하는 도중에 친구를 새롭게 추가하거나 삭제하는 이벤트가 발생하면, 클라이언트 단말은 현재 연결을 맺고 있는 웹소켓 서버의 연결 핸들러에게 해당 사실을 
즉시 전파해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동적 채널 구독 콜백&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;앱 내에서 새 친구가 추가되면 이를 감지하는 애플리케이션 콜백을 등록하여, 이 콜백은 호출되면 웹소켓 채널을 통해 서버로 “이 친구의 ID를 내 구독 리스트에 추가해”라는 메시지를 보낸다&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;최초 위치 동기화&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;요청을 받은 웹소켓 서버는 레디스 Pub/Sub 서버로 가서 해당 친구의 전용 채널을 즉시 구독한다.&lt;/li&gt;
      &lt;li&gt;이와 동시에, 만약 그 친구가 현재 활성화 상태라면 위치 정보 캐시에서 &lt;strong&gt;그 친구의 가장 최근 위경도 좌표 및 타임스탬프&lt;/strong&gt;를 조회하여 응답 메시지에 담아 클라이언트로 돌려준다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;구독 해지 및 권한 철회&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;반대로 친구를 삭제하거나, 상대방이 나에게 위치 공유 권한을 차단하는 예외 상황도 동일한 메커니즘으로 동작한다.&lt;/li&gt;
      &lt;li&gt;콜백이 트리거되는 즉시 웹소켓 연결 핸들러가 해당 친구 채널에 대한 구독을 끊어버림으로써 실시간 위치 공유를 즉각 중단시킨다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;372-친구가-많은-사용자&quot;&gt;3.7.2. 친구가 많은 사용자&lt;/h3&gt;

&lt;p&gt;만일 친구가 수천 명에 이르는 헤비 유저가 실시간으로 위치를 바꾼다면 시스템 전체에 부하가 오지 않을까 하는 의문이 들 수 있다.&lt;/p&gt;

&lt;p&gt;인스타그램이나 트위터 같은 단방향 팔로우 모델은 연예인 한 명이 움직일 때 수백만 명에게 메시지를 보내야 하므로 치명적인 병목이 생긴다.&lt;br /&gt;
여기서는 페이스북(최대 5,000명 상한)처럼 &lt;strong&gt;서로 동의하에 맺어지는 양방향 친구 관계&lt;/strong&gt;만 존재한다고 가정한다.&lt;br /&gt;
따라서 극단적인 부하는 발생하지 않는다.&lt;/p&gt;

&lt;p&gt;특정 헤비 유저의 친구가 5,000명이고 이들이 모두 온라인 상태라 하더라도, 그 5,000명의 웹소켓 커넥션을 쥐고 있는 연결 핸들러들은 단 하나의 서버가 아니라 
&lt;strong&gt;클러스터 내의 수십, 수백 대의 웹소켓 서버 노드들에 골고루 분산&lt;/strong&gt;되어 존재하여 핫스팟(Hotspot) 문제는 발생하지 않을 것이다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;💡핫스팟(Hotspot)&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;분산 인프라 환경에서 특정 서버 노드가 특정 DB 저장소 한 곳에만 트래픽 부하가 비정상적으로 집중되어 시스템이 마비되는 현상&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;373-주변의-임의-사용자-노출을-위한-지오해시-pubsub-풀-구조&quot;&gt;3.7.3. 주변의 임의 사용자 노출을 위한 지오해시 Pub/Sub 풀 구조&lt;/h3&gt;

&lt;p&gt;만일 비즈니스가 확대되어 친구 관계인 사람 뿐 아니라, 내 주변에 있는 &lt;strong&gt;완전히 모르는 제3의 임의 사용자들&lt;/strong&gt;까지 화면 지도 위에 실시간으로 노출해주어야 한다면 
기존 설계안을 어떻게 수정해야 할까?&lt;/p&gt;

&lt;p&gt;기존에 구축해 둔 사용자 ID 기반의 전용 채널 구조를 크게 훼손하지 않으면서 이 기능을 녹여내는 해법은, 바로 지리적 격자 단위인
&lt;strong&gt;&lt;a href=&quot;https://assu10.github.io/dev/2026/05/24/architecture-proximity/#223-%EC%A7%80%EC%98%A4%ED%95%B4%EC%8B%9Cgeohash&quot;&gt;지오해시&lt;/a&gt; 경계에 따라 대규모 레디스 Pub/Sub 채널 풀&lt;/strong&gt;을 개설하는 것이다.&lt;/p&gt;

&lt;p&gt;아래 그림처럼 전 세계 지도를 격자 공간으로 분할한 뒤, 격자마다 고유한 지오해시 코드(예: 9q8znd)를 부여하고 해당 코드 이름으로 레디스 채널을 하나씩 미리 만들어둔다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0605/geohash.png&quot; alt=&quot;지오해시 별 펍/섭 채널&quot; /&gt;&lt;/p&gt;

&lt;p&gt;해당 격자 내의 모든 무작위 사용자들은 자기 격자의 채널을 상시 구독하게 만든다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0605/topic.png&quot; alt=&quot;임의의 주변 사용자에게 위치 변경 내역 전송&quot; /&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
  classDef component fill:#ffffff,stroke:#333333,stroke-width:2px;
  classDef channel fill:#fff9f4,stroke:#ff9800,stroke-width:2px;

  WS[&quot;💻 사용자 2의 웹소켓 연결 핸들러&quot;] --&amp;gt;|① 위치 변경 감지| Step1[&quot;🌐 지오해시 ID 계산&amp;lt;br&amp;gt;(ex: 9q8znd)&quot;]
  Step1 --&amp;gt;|② 위치 정보 발행| Ch[&quot;🛢️ 레디스 펍/섭 채널&amp;lt;br&amp;gt;(지오해시: 9q8znd)&quot;]
  Ch --&amp;gt;|③ 브로드캐스트 푸시| Match[&quot;📱 격자 내 임의의 구독 유저들&amp;lt;br&amp;gt;(사용자 1, 사용자 3 등)&quot;]

  style Step1 fill:#f4f9ff,stroke:#2196f3,stroke-width:2px;
  style Match fill:#f9f9f9,stroke:#cccccc,stroke-dasharray: 5 5;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;① 지오해시 코드 산출:&lt;/strong&gt; 특정 사용자의 물리적 위치가 변경되면, 해당 사용자를 담당하는 웹소켓 연결 핸들러는 위경도 좌표를 바탕으로 현재 속한 지오해시 ID를 계산한다.   &lt;br /&gt;
&lt;strong&gt;② 채널 발행 및 공유:&lt;/strong&gt; 핸들러는 계산된 지오해시 채널(9q8znd)로 사용자의 새 좌표를 발행한다.&lt;br /&gt;
&lt;strong&gt;③ 실시간 전파:&lt;/strong&gt; 같은 격자에 머물며 해당 지오해시 채널을 구독하고 있던 근방의 임의의 다른 유저들이 이 메시지를 수신하게 되며, 화면 레이더망에 서로의 위치를 실시간으로 업데이트해 줄 수 있게 된다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚨격자 경계 사각지대 예외 처리 디테일&lt;/strong&gt;&lt;br /&gt;
사용자가 격자의 아주 미세한 모서리나 경계선 부근에 서 있는 경우, 실제 물리적 거리는 코앞인데 격자 코드가 다르다는 이유로 바로 옆방에 있는 사람을 놓치는 사각지대가 발생한다.&lt;br /&gt;
이 치명적인 엣지 케이스를 극복하기 위해, 
&lt;strong&gt;모든 클라이언트 단말은 자신이 위치한 중심 격자 채널 1개 뿐 아니라, 자신을 둘러싸고 있는 인접한 8개의 지오해시 격자 채널까지 포함하여 총 9개의 채널을 동시에 상시 구독&lt;/strong&gt;하도록 넓혀 설계한다.&lt;br /&gt;
이렇게 하면 경계선 경합 문제 없이 완벽하게 주변의 모든 무작위 사용자를 포착할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-레디스-pubsub을-대체할-대안-얼랭erlang&quot;&gt;4. 레디스 Pub/Sub을 대체할 대안: 얼랭(Erlang)&lt;/h1&gt;

&lt;p&gt;여기서는 앞서 초당 1,400만 건의 실시간 메시지 부하를 버티기 위해 140대 규모의 분산 레디스 Pub/Sub 클러스터를 구축하고, 
안정 해시 링과 etcd 서비스 탐색 레이어까지 도입하는 거대한 엔지니어링 설계를 하였다.&lt;/p&gt;

&lt;p&gt;하지만 인프라의 복잡도가 너무 높고 ‘재구독 부하’같은 잠재적 위험 요소를 늘 안고 가야 한다.&lt;br /&gt;
만약 이 &lt;strong&gt;모든 인프라 운영 부담과 복잡한 분산 라우팅 코드를 한번에 삭제할 수 있는 강력한 대안&lt;/strong&gt;이 있다면 어떨까?&lt;/p&gt;

&lt;p&gt;바로 텔레콤 및 실시간 채팅 아키텍처의 절대 강자, &lt;strong&gt;&lt;a href=&quot;https://www.erlang.org/&quot;&gt;얼랭(Erlang)&lt;/a&gt;&lt;/strong&gt; 모델이다.&lt;/p&gt;

&lt;p&gt;얼랭은 1980년대 스웨덴의 통신장비 기업인 에릭슨(Ericsson)에서 수천만 개의 전화 통화를 끊김 없이 실시간으로 연결하고 제어하기 위해 개발한 함수형 프로그래밍 언어(Runtime OS)이다.&lt;/p&gt;

&lt;p&gt;이 시스템 아키텍처에서 얼랭이 완벽한 게임 체인저가 되는 이유는 크게 3가지이다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;초경량 프로세스(Actor) 모델:&lt;/strong&gt; 얼랭의 프로세스는 OS의 무거운 스레드가 아니라, 얼랭 가상머신(BEAM)이 독립적으로 관리하는 ‘초경량 초소형 프로세스’이다. 생성 비용이 무시해도 될 정도로 작으며, 프로세스 1개가 차지하는 메모리는 고작 &lt;strong&gt;2~3KB&lt;/strong&gt; 수준에 불과하다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;내장형 메시지 패싱(Message Passing):&lt;/strong&gt; 얼랭 프로세스들은 각자 독립된 Mailbox를 가진다. 프로세스 간에 메시지를 주고받는 행위가 라이브러리나 외부 저장소 없이 &lt;strong&gt;언어 자체의 원시 기능&lt;/strong&gt;으로 완벽히 지원된다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;플러그앤플레이 분산 환경:&lt;/strong&gt; 여러 대의 서버에 얼랭 노드를 실행하고 서로 연결해 주기만 하면, 물리적으로 다른 서버에 떠 있는 프로세스일지라도 마치 같은 메모리 공간에 있는 것처럼 이름을 지정해 초고속으로 메시지를 보낼 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-얼랭을-활용한-분산-메시지망-구조&quot;&gt;4.1. 얼랭을 활용한 분산 메시지망 구조&lt;/h2&gt;

&lt;p&gt;얼랭 아키텍처를 도입하면 기존의 ‘웹소켓 서버 + 레디스 Pub/Sub 클러스터 + etcd 해시 링’으로 무겁게 쪼개져 있던 분리 계층이 하나의 일체형 분산 시스템으로 완전히 통합된다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
  classDef node fill:#f4f9ff,stroke:#2196f3,stroke-width:2px;
  classDef process fill:#fff9f4,stroke:#ff9800,stroke-width:2px;

  subgraph Erlang_Cluster [💻 하나의 거대한 분산 얼랭 클러스터]
    Server1[&quot;💻 얼랭 서버 노드 1&quot;]:::node
    Server2[&quot;💻 얼랭 서버 노드 2&quot;]:::node
    
    P_A[&quot;👤 유저 A 프로세스&amp;lt;br&amp;gt;(메모리: 2KB)&quot;]:::process
    P_B[&quot;👤 친구 B 프로세스&amp;lt;br&amp;gt;(메모리: 2KB)&quot;]:::process
    P_C[&quot;👤 친구 C 프로세스&amp;lt;br&amp;gt;(메모리: 2KB)&quot;]:::process

    Server1 --- P_A
    Server2 --- P_B
    Server2 --- P_C

    P_A --&amp;gt;|Native 메시지 직접 송신| P_B
    P_A --&amp;gt;|클러스터 네트워크 통신| P_C
  end

  Client_A[&quot;📱 유저 A 단말&quot;] --&amp;gt;|Websocket| P_A
  Client_B[&quot;📱 친구 B 단말&quot;] --&amp;gt;|Websocket| P_B
  Client_C[&quot;📱 친구 C 단말&quot;] --&amp;gt;|Websocket| P_C
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;얼랭 기반 실시간 위치 분산 중계 메커니즘&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;1:1 매핑 프로세스 생성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;1,000만 명의 활성 사용자가 웹소켓을 연결하는 즉시, 분산 얼랭 클러스터 내에 &lt;strong&gt;유저당 정확히 1개씩의 초경량 프로세스&lt;/strong&gt;를 동적으로 생성한다.&lt;/li&gt;
      &lt;li&gt;메모리 계산: 1,000만명 * 2.5KB = &lt;strong&gt;고작 25GB 내외&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;수백 대의 레디스 서버 없이 서버 몇 대의 RAM 만으로 1,000만 명의 상태유지 세션을 통째로 올릴 수 있다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;상시 연결망 구성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저 A가 로그인하면 유저 A의 프로세스는 사용자 DB를 통해 친구 목록을 가져온 뒤, 클러스터 내부에서 활성화되어 있는 친구 B와 C의 프로세스 주소(PID)를 직접 찾아내어 서로를 링크해둔다.&lt;/li&gt;
      &lt;li&gt;외부 Pub/Sub 채널 개념이 사라지고, 프로세스 대 프로세스의 직접적인 연결망이 구축된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;네이티브 메시지 릴레이&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;유저 A가 30초 주기로 최신 위치를 전송하면, 유저 A 프로세스는 자신과 링크된 친구 프로세스들의 주소로 &lt;strong&gt;위치 변경 이벤트 패킷을 다이렉트로 던진다.&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;실시간 연산 및 푸시&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;메시지를 수신한 친구 B와 C의 프로세스는 각자의 Mailbox에서 패킷을 꺼내 즉시 직선 거리를 계산하고, 반경 5마일 이내일 경우 자신이 쥐고 있는 웹소켓 파이프라인을 통해 최종 클라이언트 단말로 데이터를 응답한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;얼랭 모델이 가져다주는 이점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;인프라 비용의 혁신적인 절감:&lt;/strong&gt; 초당 1,400만 건의 트래픽을 처리하기 위해 관리해야 했던 140대 이상의 레디스 Pub/Sub 노드, 대기 노드, 오버 프로비저닝의 예산이 완전히 0이 된다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;재구독 부하 차단:&lt;/strong&gt; etcd의 해시 링 경계선이 출렁거릴 때 수천만 건의 채널 주인이 바뀌며 인프라를 마비시키던 재구독 부하가 원천 차단된다.
    &lt;ul&gt;
      &lt;li&gt;얼랭 환경에서는 서버 노드가 추가되거나 죽더라도, 살아있는 프로세스들끼리 내장된 장애 복구 메커니즘을 통해 조용하게 연결 주소를 갱신할 뿐이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;검증된 레퍼런스:&lt;/strong&gt; 전 세계 10억 명 이상이 사용하는 글로벌 실시간 서비스인 WhatsApp, Discord, WeChat이 바로 이 얼랭(또는 파생 언어인 Elixir) 아키텍처를 핵심 기반으로 삼아 단 몇 대의 고성능 서버만으로 수천만 명의 실시간 동시 접속과 커넥션을 지연 없이 처리하고 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-최종-아키텍처-다이어그램&quot;&gt;5. 최종 아키텍처 다이어그램&lt;/h1&gt;

&lt;p&gt;아래는 모바일 클라이언트가 위치를 변경했을 때, 백엔드 인프라 내부의 전 영역이 어떻게 유기적으로 연쇄 반응을 일으키는지 보여준다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
  classDef client fill:#ffffff,stroke:#333333,stroke-width:2px;
  classDef lb fill:#f5f5f5,stroke:#666666,stroke-width:2px;
  classDef stateful fill:#f4f9ff,stroke:#2196f3,stroke-width:2px;
  classDef stateless fill:#e8f5e9,stroke:#4caf50,stroke-width:2px;
  classDef cache fill:#fffde7,stroke:#fbc02d,stroke-width:2px;
  classDef storage fill:#ffebee,stroke:#c62828,stroke-width:2px;
  classDef discovery fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px;

  %% Clients &amp;amp; Edge
  ClientA[&quot;📱 모바일 클라이언트 A&amp;lt;br&amp;gt;(유저 1000만 명)&quot;]:::client
  LB[&quot;⚖️ 레이어 7 로드밸런서&amp;lt;br&amp;gt;(Graceful Draining 지원)&quot;]:::lb

  %% Core Servers
  subgraph Stateful_Layer [💻 상태유지 웹소켓 클러스터]
    WS_Server[&quot;💻 웹소켓 서버 (Node 1~N)&amp;lt;br&amp;gt;- 세션별 로컬 변수 보관&amp;lt;br&amp;gt;- 직선거리 5마일 연산 루틴&quot;]:::stateful
  end

  subgraph Stateless_Layer [💻 무상태 비즈니스 클러스터]
    API_Server[&quot;💻 내부 API 서버&amp;lt;br&amp;gt;- 프로필/인증 처리&amp;lt;br&amp;gt;- 오토스케일링 그룹&quot;]:::stateless
  end

  %% Discovery &amp;amp; Routing
  subgraph Service_Discovery [🌐 서비스 탐색 계층]
    etcd[&quot;🌐 etcd 해시 링 원본&amp;lt;br&amp;gt;/config/pub_sub_ring&quot;]:::discovery
  end

  %% In-Memory Cache &amp;amp; Message Broker
  subgraph PubSub_Cluster [🛢️ 분산 레디스 펍/섭 클러스터]
    Redis_PS[&quot;🛢️ Redis Pub/Sub (140대+)&amp;lt;br&amp;gt;- 유저 ID 기준 샤딩&amp;lt;br&amp;gt;- 채널 관리 전용 노드&quot;]:::cache
  end

  subgraph Cache_Layer [💾 인메모리 캐시 레이어]
    Redis_Cache[&quot;💾 Redis 위치 캐시&amp;lt;br&amp;gt;- 10분 TTL 자동 만료&amp;lt;br&amp;gt;- 유저 ID 샤딩 (1GB 용량)&quot;]:::cache
  end

  %% Persistent Storage
  subgraph Storage_Layer [🛢️ 영구 저장소 레이어]
    Cassandra[&quot;🛢️ 카산드라 DB&amp;lt;br&amp;gt;- LSM 트리 고속 쓰기&amp;lt;br&amp;gt;- 위치 이동 이력 로깅&quot;]:::storage
    User_DB[&quot;🛢️ 사용자 DB (RDBMS)&amp;lt;br&amp;gt;- 유저 ID 기반 샤딩&amp;lt;br&amp;gt;- 친구 관계 매핑&quot;]:::storage
  end

  %% Connections &amp;amp; Data Flow
  ClientA &amp;lt;--&amp;gt;|상시 개방 지속성 채널| LB
  LB &amp;lt;--&amp;gt;|① 위치 변경 내역 전달| WS_Server
  LB --&amp;gt;|HTTP API 요청| API_Server

  WS_Server --&amp;gt;|② 비동기 로깅| Cassandra
  WS_Server --&amp;gt;|③ 최신 좌표 갱신 &amp;amp; TTL 리셋| Redis_Cache
  
  etcd -.-&amp;gt;|④ 해시 링 사본 Watch/싱크| WS_Server
  WS_Server --&amp;gt;|⑤ 해시 링 참조 후 위치 발행| Redis_PS
  Redis_PS --&amp;gt;|⑥ 온라인 친구들에게 브로드캐스트| WS_Server
  
  API_Server &amp;lt;--&amp;gt;|내부 API 통신 격리| User_DB
  WS_Server -- &quot;친구 목록 요청&quot; --&amp;gt; API_Server

  class Stateful_Layer,Stateless_Layer,PubSub_Cluster,Cache_Layer,Storage_Layer,Service_Discovery internalSubgraphs;
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;핵심-요약-및-takeaway&quot;&gt;핵심 요약 및 Takeaway&lt;/h1&gt;

&lt;p&gt;대규모 트래픽을 감당하는 실시간 서비스 아키텍처를 설계할 때 반드시 고려해야 할 시스템 디자인 핵심이다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Stateful 서버의 제1원칙은 ‘정적 안정성’이다.&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;RESTful API 서버처럼 트래픽 추이에 따라 실시간으로 웹소켓이나 레디스 Pub/Sub 서버를 늘리고 줄이는 것은 매우 위험하다.&lt;/li&gt;
      &lt;li&gt;경계선이 바뀔 때마다 밀려오는 재구독 부하는 인프라를 일시에 마비시킨다.&lt;/li&gt;
      &lt;li&gt;인프라 비용을 더 지불하더라도 오버 프로비저닝을 통해 서버의 크기 변화 원인 자체를 차단하는 것이 훨씬 성숙한 설계이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;용량(Storage) 병목과 연산(CPU/IO) 병목을 분리하여 산정하라.&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;1,000만 명의 위치 정보는 고작 1GB 내외로 레디스 한 대의 메모리에 넉넉히 들어간다.&lt;/li&gt;
      &lt;li&gt;하지만 초당 33만 건의 쓰기와 초당 1,400만 건의 Pub/Sub 중계 연산은 단일 싱글 스레드 레디스로 절대 감당할 수 없다.&lt;/li&gt;
      &lt;li&gt;대규모 설계 시에는 항상 용량이 아닌 &lt;strong&gt;초당 처리량(QPS) 부하를 기준으로 샤딩 규모를 산정&lt;/strong&gt;해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;LSM 트리와 TTL 캐시로 대규모 쓰기와 메모리 누수를 극복하라.&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;매초 쏟아지는 위치 이력을 RDBMS에 저장하면 디스크 I/O 병목으로 시스템이 다운된다.&lt;/li&gt;
      &lt;li&gt;순차 이어쓰기(Append-only) 구조를 가진 &lt;strong&gt;카산드라의 LSM 트리&lt;/strong&gt;로 쓰기 성능을 극대화한다.&lt;/li&gt;
      &lt;li&gt;레디스에는 &lt;strong&gt;10분 TTL&lt;/strong&gt; 전략을 부여하여 앱을 끈 유저의 데이터가 메모리를 잠식하지 않도록 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;인프라 복잡도와 기술 스택의 트레이드 오프를 이해하라.&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;레디스 Pub/Sub 모델은 검증되고 대중적인 오픈소스 조합(Redis + etcd)을 활용하지만, 분산 샤딩 링과 서비스 디스커버리 계층이 추가되어 인프라 복잡도가 높아진다.&lt;/li&gt;
      &lt;li&gt;반면, &lt;strong&gt;얼랭 아키텍처&lt;/strong&gt;는 언어 자체의 초경량 액터 프로세스 모델과 메시지 패싱 기능만으로 이 거대한 중계 인프라 레이어를 압축해버린다.&lt;/li&gt;
      &lt;li&gt;엔지니어링 팀의 역량과 인프라 관리 비용을 저울질하여 최선의 대안을 선택하는 통찰이 필요하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0605/final.png&quot; alt=&quot;mind map&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬, 산 람 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000211656186&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links_vol2.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://techcrunch.com/2014/04/17/facebook-nearby-friends/&quot;&gt;Facebook Launches “Nearby Friends” With Opt-In Real-Time Location Sharing To Help You Meet Up&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/develop/pubsub/&quot;&gt;Redis Pub/Sub&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://etcd.io/&quot;&gt;etcd&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://zookeeper.apache.org/&quot;&gt;주키퍼&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.toptal.com/developers/big-data/consistent-hashing&quot;&gt;안정 해시&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.erlang.org/&quot;&gt;얼랭&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.erlang.org/doc/system/design_principles.html&quot;&gt;OTP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Fri, 05 Jun 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/06/05/architecture-nearby/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/06/05/architecture-nearby/</guid>
        
        <category>architecture</category>
        
        <category>system-design</category>
        
        <category>proximity-service</category>
        
        <category>websocket</category>
        
        <category>redis</category>
        
        <category>cassandra</category>
        
        <category>erlang</category>
        
        <category>distributed-system</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Architecture(2) - 근접성 서비스(Geohash, Quadtree)</title>
        <description>&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-요구사항-및-규모-추정&quot;&gt;1. 요구사항 및 규모 추정&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-기능-요구사항&quot;&gt;1.1. 기능 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#12-비기능-요구사항&quot;&gt;1.2. 비기능 요구사항&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#13-개략적-규모-추정back-of-the-envelope-calculation&quot;&gt;1.3. 개략적 규모 추정(Back-of-the-envelope calculation)&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-개략적-아키텍처와-지리-정보-인덱싱-알고리즘&quot;&gt;2. 개략적 아키텍처와 지리 정보 인덱싱 알고리즘&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-개략적-설계안-및-핵심-컴포넌트&quot;&gt;2.1. 개략적 설계안 및 핵심 컴포넌트&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#211-엔드포인트-및-api-설계&quot;&gt;2.1.1. 엔드포인트 및 API 설계&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#212-데이터-모델링-및-mysql-채택-이유&quot;&gt;2.1.2. 데이터 모델링 및 MySQL 채택 이유&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#2121-데이터-스키마&quot;&gt;2.1.2.1. 데이터 스키마&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#213-시스템-개략적-설계안&quot;&gt;2.1.3. 시스템 개략적 설계안&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#214-핵심-서비스-및-규모-확장성-전략&quot;&gt;2.1.4. 핵심 서비스 및 규모 확장성 전략&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-주변-사업장-검색-알고리즘&quot;&gt;2.2. 주변 사업장 검색 알고리즘&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#221-2차원-검색의-한계&quot;&gt;2.2.1. 2차원 검색의 한계&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#222-균등-격자even-grid&quot;&gt;2.2.2. 균등 격자(Even grid)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#223-지오해시geohash&quot;&gt;2.2.3. 지오해시(Geohash)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#2231-격자-가장자리edge-이슈&quot;&gt;2.2.3.1. 격자 가장자리(Edge) 이슈&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#2232-표시할-사업장이-충분하지-않은-경우&quot;&gt;2.2.3.2. 표시할 사업장이 충분하지 않은 경우&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#224-쿼드트리quadtree&quot;&gt;2.2.4. 쿼드트리(Quadtree)&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#2241-쿼드트리를-저장하는데-필요한-메모리는&quot;&gt;2.2.4.1. 쿼드트리를 저장하는데 필요한 메모리는?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#2242-쿼드트리-구축에-소요되는-시간은&quot;&gt;2.2.4.2. 쿼드트리 구축에 소요되는 시간은?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#2243-쿼드트리로-주변-사업장을-검색하려면&quot;&gt;2.2.4.3. 쿼드트리로 주변 사업장을 검색하려면?&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#2244-쿼드트리-운영-시-고려사항&quot;&gt;2.2.4.4. 쿼드트리 운영 시 고려사항&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#2245-실제-사용되는-쿼드트리-사례&quot;&gt;2.2.4.5. 실제 사용되는 쿼드트리 사례&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#225-구글-s2&quot;&gt;2.2.5. 구글 S2&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-지오해시-vs-쿼드트리&quot;&gt;2.3. 지오해시 vs 쿼드트리&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-상세-설계-db-샤딩-초고효율-공간-캐싱&quot;&gt;3. 상세 설계: DB 샤딩, 초고효율 공간 캐싱&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-데이터베이스-규모-확장성-및-샤딩-전략&quot;&gt;3.1. 데이터베이스 규모 확장성 및 샤딩 전략&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#311-사업장-테이블-및-지오해시-테이블-설계&quot;&gt;3.1.1. 사업장 테이블 및 지오해시 테이블 설계&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#312-지리-정보-색인의-규모-확장&quot;&gt;3.1.2. 지리 정보 색인의 규모 확장&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-대규모-트래픽을-담당하는-고성능-캐시-아키텍처&quot;&gt;3.2. 대규모 트래픽을 담당하는 고성능 캐시 아키텍처&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#321-캐시-계층-도입-기준&quot;&gt;3.2.1. 캐시 계층 도입 기준&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#322-올바른-공간-캐시-key-설계&quot;&gt;3.2.2. 올바른 공간 캐시 key 설계&lt;/a&gt;
            &lt;ul&gt;
              &lt;li&gt;&lt;a href=&quot;#3221-사용자-raw-좌표위도-경도를-캐시-key로-쓰는-경우의-참사&quot;&gt;3.2.2.1. 사용자 raw 좌표(위도, 경도)를 캐시 key로 쓰는 경우의 참사&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#3222-지오해시-정적-격자를-캐시-key-로-채택해결책&quot;&gt;3.2.2.2. 지오해시 정적 격자를 캐시 key 로 채택(해결책)&lt;/a&gt;&lt;/li&gt;
              &lt;li&gt;&lt;a href=&quot;#3223-캐시-구조-및-메모리-계산-산정&quot;&gt;3.2.2.3. 캐시 구조 및 메모리 계산 산정&lt;/a&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-글로벌-가용성-및-부가-비즈니스-로직-처리&quot;&gt;3.3. 글로벌 가용성 및 부가 비즈니스 로직 처리&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#331-다중-region-및-가용성-구역-배치&quot;&gt;3.3.1. 다중 Region 및 가용성 구역 배치&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#332-실시간-영업-여부-및-유형별-필터링-기능-구현&quot;&gt;3.3.2. 실시간 영업 여부 및 유형별 필터링 기능 구현&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-최종-아키텍처-다이어그램&quot;&gt;4. 최종 아키텍처 다이어그램&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-요구사항-및-규모-추정&quot;&gt;1. 요구사항 및 규모 추정&lt;/h1&gt;

&lt;p&gt;근접성 서비스(Proximity Service)는 사용자의 현재 지리적 위치(위도와 경도)를 기반으로 특정 반경 내에 있는 음식점, 주유소 등의 사업장 목록을 검색하고, 
정보를 제공하는 위치 기반 서비스(LBS, Location-Based Service)이다.
대표적으로 주변 식당을 찾는 ‘옐프(Yelp)’나 가까운 주유소를 검색하는 ‘구글 맵’이 이에 해당한다.&lt;/p&gt;

&lt;p&gt;대규모 아키텍처를 설계할 때 가장 먼저 해야 할 일은 시스템의 한계와 범위를 명확히 하는 것이다.&lt;br /&gt;
기획 및 기술적 제약 조건을 바탕으로 도출된 핵심 요구사항은 아래와 같다고 가정한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-기능-요구사항&quot;&gt;1.1. 기능 요구사항&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;위치 기반 검색
    &lt;ul&gt;
      &lt;li&gt;사용자의 GPS 좌표(위도/경도)와 선택한 반경에 매칭되는 사업장 목록을 정확하게 반환해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;동적 반경 변경
    &lt;ul&gt;
      &lt;li&gt;사용자는 UI에서 검색 반경을 &lt;strong&gt;0.5km, 1km, 2km, 5km, 20km&lt;/strong&gt; 로 자유롭게 변경할 수 있어야 하며, 최대 허용 반경은 &lt;strong&gt;20km&lt;/strong&gt;로 제한한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;사업장 데이터 관리
    &lt;ul&gt;
      &lt;li&gt;사업장 소유주는 자신의 사업장 정보를 시스템에 추가/삭제/갱신할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;비실시간 반영 허용
    &lt;ul&gt;
      &lt;li&gt;소유주가 수정한 정보가 검색 결과에 실시간으로 반영될 필요는 없으며, &lt;strong&gt;다음 날까지만 반영&lt;/strong&gt;되어도 무방하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;상세 정보 조회
    &lt;ul&gt;
      &lt;li&gt;고객은 검색된 사업장의 상세 페이지를 조회할 수 있어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;화면 자동 갱신 비활성화
    &lt;ul&gt;
      &lt;li&gt;사용자가 이동 중이더라도 이동 속도가 아주 빠르지 않으므로, 현재 위치 기준으로 화면을 상시 자동 갱신할 필요는 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;12-비기능-요구사항&quot;&gt;1.2. 비기능 요구사항&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;낮은 응답 지연(Low Latency)
    &lt;ul&gt;
      &lt;li&gt;사용자가 주변 검색을 할 때 답답함을 느끼지 않도록 매우 신속하게 결과를 반환해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;고가용성 및 규모 확장성
    &lt;ul&gt;
      &lt;li&gt;인구 밀집 지역(예: 강남역)에서 특정 시간대(점심/퇴근 시간)에 트래픽이 급증해도 시스템이 동작해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;데이터 보호 및 사생활 보장
    &lt;ul&gt;
      &lt;li&gt;사용자 위치 정보는 민감한 개인정보이므로, 이를 안전하게 보호하고 관련 법령을 준수해야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;GDPR(General Data Protection Regulation)&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;GDPR(유럽 일반 개인정보보호법)은 유럽연합(EU)이 제정한 세계에서 가장 강력한 개인정보 보호 법령이다.&lt;br /&gt;
사용자의 위치 데이터와 같은 민감 정보는 GDPR의 핵심 규제 대상이다.&lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;핵심 원칙: 기업은 사용자의 위치 정보를 수집할 때 &lt;strong&gt;명시적 동의&lt;/strong&gt;를 받아야 하며, 목적 달성 후에는 지체 없이 &lt;strong&gt;파기 또는 익명화&lt;/strong&gt;해야 한다.&lt;/li&gt;
    &lt;li&gt;잊힐 권리(Right to be Forgotten): 사용자가 요청하면 시스템 내에 저장된 사용자의 위치 이력 등 모든 개인 데이터를 완전히 삭제할 수 있는 구조를 아키텍처 설계 단계부터 반영해야 한다.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;13-개략적-규모-추정back-of-the-envelope-calculation&quot;&gt;1.3. 개략적 규모 추정(Back-of-the-envelope calculation)&lt;/h2&gt;

&lt;p&gt;시스템 인프라의 규모를 결정하기 위해 대략적인 트래픽과 처리량(QPS)를 산정해본다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;기본 가정 수치&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;일일 능동 사용자 수(DAU, Daily Active User): 1억명(\(10^8\))&lt;/li&gt;
      &lt;li&gt;총 등록 사업장 수: 2억 개(\(2 * 10^8\))&lt;/li&gt;
      &lt;li&gt;사용자당 일평균 검색 횟수: 5회&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;QPS(Query Per Second) 계산&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하루 시간인 86,400초를 계산 편의상 \(10^5\)초로 올림하여 계산한다.&lt;/li&gt;
      &lt;li&gt;하루 총 검색 수 = \(10^8\)명 * 5회 = 5 * \(10^8\)회&lt;/li&gt;
      &lt;li&gt;평균 QPS = \(\frac{5 * 10^8회}{10^5초} = 5,000\)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;최종적으로 시스템은 &lt;strong&gt;평균 5,000 QPS&lt;/strong&gt;를 처리할 수 있어야 하며, 트래픽 피크 타임을 고려하면 이보다 2~3배 높은 대역폭을 감당할 수 있는 확장성이 필요하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;이 시스템은 전형적인 &lt;strong&gt;읽기 중심(Read-heavy)&lt;/strong&gt; 시스템으로, 초당 5,000번 이상의 읽기 요청을 지연 없이 처리하는 것이 핵심이다.&lt;br /&gt;
위치 정보는 &lt;strong&gt;GDPR/CCPA 규정&lt;/strong&gt;을 철저히 준수하도록 설계 단계부터 보안 및 익명화 전략을 수립해야 한다.&lt;br /&gt;
데이터의 실시간 동기화 요구사항이 낮으므로(익일 반영), DB 쓰기 부하보다는 &lt;strong&gt;조회 성능 최적화&lt;/strong&gt;와 &lt;strong&gt;캐싱 전략&lt;/strong&gt;에 집중해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-개략적-아키텍처와-지리-정보-인덱싱-알고리즘&quot;&gt;2. 개략적 아키텍처와 지리 정보 인덱싱 알고리즘&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;&amp;lt;위치 기반 서비스(LBS)와 공간 인덱싱의 필요성&amp;gt;&lt;/strong&gt;&lt;br /&gt;
대규모 근접성 서비스를 안정적으로 구축하기 위해서는 수억 개의 지리적 좌표 데이터 속에서 사용자의 현재 위치와 인접한 사업장을 ms 단위로 찾아내는 &lt;strong&gt;공간 인덱싱(Spatial Indexing)&lt;/strong&gt; 기술이 
필수적이다.&lt;br /&gt;
Stateless 아키텍처 구조의 서버 설계와 더불어, 2차원의 위도/경도 데이터를 1차원 선형 인덱스로 변환하는 고성능 알고리즘의 trade-off를 이해해야 최적의 시스템을 설계할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-개략적-설계안-및-핵심-컴포넌트&quot;&gt;2.1. 개략적 설계안 및 핵심 컴포넌트&lt;/h2&gt;

&lt;h3 id=&quot;211-엔드포인트-및-api-설계&quot;&gt;2.1.1. 엔드포인트 및 API 설계&lt;/h3&gt;

&lt;p&gt;근접성 서비스의 핵심인 주변 검색 API는 대량의 트래픽과 데이터 반환을 안전하게 제어할 수 있도록 &lt;a href=&quot;https://developer.atlassian.com/server/confluence/pagination-in-the-rest-api/&quot;&gt;&lt;strong&gt;페이징&lt;/strong&gt;&lt;/a&gt; 처리를 필수로 포함한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;주변 사업장 검색 API&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Endpoint: GET /v1/search/nearby&lt;/li&gt;
      &lt;li&gt;Request Parameters:
        &lt;ul&gt;
          &lt;li&gt;latitude(검색할 위도, Decimal, 필수)&lt;/li&gt;
          &lt;li&gt;longitude(검색할 경도, Decimal, 필수)&lt;/li&gt;
          &lt;li&gt;radius(검색 반경, Int, 선택 항목이며 기본값은 5km)&lt;/li&gt;
          &lt;li&gt;Response Body:
            &lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;total&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;businesses&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;business_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;123&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;맛있는 식당&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;address&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;            &lt;/div&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;사업장 관리용 CRUD API&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;상세 정보 조회: GET /v1/businesses/:id&lt;/li&gt;
      &lt;li&gt;사업장 추가: POST /v1/businesses&lt;/li&gt;
      &lt;li&gt;사업장 수정: PUT /v1/businesses/:id&lt;/li&gt;
      &lt;li&gt;사업장 삭제: DELETE /v1/businesses/:id&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Tip!&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;실제 프로덕션 환경을 구축할 때는 구글 장소(Places)나 옐프(Yelp) 사업장 검색 API의 파라미터 구조를 벤치마킹하는 것이 큰 도움이 됩니다.&lt;/p&gt;

  &lt;ul&gt;
    &lt;li&gt;구글 장소 API: https://developers.google.com/maps/documentation/places/web-service/legacy/search?hl=ko&lt;/li&gt;
    &lt;li&gt;옐프 사업장 API: https://docs.developer.yelp.com/reference/v3_business_search&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;212-데이터-모델링-및-mysql-채택-이유&quot;&gt;2.1.2. 데이터 모델링 및 MySQL 채택 이유&lt;/h3&gt;

&lt;p&gt;주변 검색과 상세 조회가 시스템 연산의 대부분을 차지하는 &lt;strong&gt;압도적인 읽기 중심(Read-heavy)&lt;/strong&gt; 시스템이다.&lt;br /&gt;
반면, 소유주가 장소 정보를 편집하는 쓰기 연산의 비율은 극히 낮다.&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;읽기 중심에서 MySQL이 바람직한 이유와 타 DB와의 비교&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;왜 모든 데이터를 MongoDB나 Redis에 넣지 않는가?&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;정형 데이터의 안정성&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;사업장 이름, 주소 등의 스키마가 매우 명확한 정형 데이터이다. RDBMS인 MySQL은 이러한 고정 스키마 데이터를 가장 안정적으로 관리한다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Redis의 한계&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;Redis는 모든 데이터를 메모리에 상주시키는 &lt;strong&gt;In-memory DB&lt;/strong&gt;이다.&lt;/li&gt;
          &lt;li&gt;2억 개의 사업장 상세 데이터를 전부 Redis에 상시 보관하면 &lt;strong&gt;메모리 유지 비용이 천문학적으로 발생&lt;/strong&gt;한다.&lt;/li&gt;
          &lt;li&gt;따라서 Redis는 뒤에 나올 지오해시 인덱스나 캐싱 용도로만 사용하고, 마스터 데이터는 디스크 기반의 RDBMS에 두는 것이 비용 효율적이다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;MongoDB의 한계&lt;/strong&gt;
        &lt;ul&gt;
          &lt;li&gt;NoSQL인 MongoDB 역시 뛰어난 읽기 성능과 지리 색인을 지원하지만, 정형 데이터의 엄격한 일관성과 다중 사본(Read Replica) 확장 신뢰성 측면에서 오랜 기간 검증된 MySQL이 구조적 안정감을 준다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Oracle, MSSQL에 비해 MySQL이 추천되는 대규모 아키텍처적 이유&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수만~수십만 QPS의 읽기 요청을 분산하려면 수십 대의 Replica를 Scale-out해야 한다.&lt;/li&gt;
      &lt;li&gt;코어 단위나 서버 대수 단위로 &lt;strong&gt;라이선스 비용이 청구되는 Oracle, MSSQL은 인프라 확장 시 비용 폭탄&lt;/strong&gt;으로 이어진다.&lt;/li&gt;
      &lt;li&gt;반면 오픈소스인 MySQL은 비용 부담없이 무제한으로 Replica를 증설할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2121-데이터-스키마&quot;&gt;2.1.2.1. 데이터 스키마&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;business 테이블&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;business_id를 PK로 하며, 이름/주소 등 사업장 상세 정보를 담는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;지리적 위치 색인 테이블&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;위치 기반 공간 연산 속도를 끌어올리기 위한 인덱스 전용 테이블이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;213-시스템-개략적-설계안&quot;&gt;2.1.3. 시스템 개략적 설계안&lt;/h3&gt;

&lt;p&gt;이 시스템은 위치 기반 서비스(LBS)와 사업장 관련 서비스, 두 부분으로 구성된다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    %% 스타일 정의
    classDef userStyle fill:#fff,stroke:#333,stroke-width:2px;
    classDef lbStyle fill:#fff,stroke:#333,stroke-width:1.5px;
    classDef serviceStyle fill:#fff,stroke:#333,stroke-width:1.5px;
    classDef masterDbStyle fill:#777,stroke:#333,stroke-width:2px,color:#fff;
    classDef replicaDbStyle fill:#fff,stroke:#333,stroke-width:1.5px;

    %% 노드 배치
    User((사용자))
    LB[로드밸런서]
    
    LBS[LBS 위치 기반 서비스]
    BizService[사업장 서비스]

    subgraph DB_Cluster [데이터베이스 클러스터]
        direction LR
        Replica1[(사본 데이터베이스 1)]
        Replica2[(사본 데이터베이스 2)]
        Replica3[(사본 데이터베이스 3)]
        Master[(주 데이터베이스)]
    end

    %% 스타일 적용
    class User userStyle;
    class LB lbStyle;
    class LBS,BizService serviceStyle;
    class Master masterDbStyle;
    class Replica1,Replica2,Replica3 replicaDbStyle;

    %% 흐름 연결
    User --&amp;gt; LB
    
    LB --&amp;gt;|/v1/search/nearby| LBS
    LB --&amp;gt;|&quot;/v1/businesses/{:id}&quot;| BizService

    LBS --&amp;gt;|읽기 요청 분산| Replica1
    LBS --&amp;gt;|읽기 요청 분산| Replica2
    LBS --&amp;gt;|읽기 요청 분산| Replica3
    
    BizService --&amp;gt;|쓰기 요청| Master

    %% 복제 흐름 (주 -&amp;gt; 사본)
    Master -.-&amp;gt;|비동기 데이터 복제| Replica1
    Master -.-&amp;gt;|비동기 데이터 복제| Replica2
    Master -.-&amp;gt;|비동기 데이터 복제| Replica3
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;214-핵심-서비스-및-규모-확장성-전략&quot;&gt;2.1.4. 핵심 서비스 및 규모 확장성 전략&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1) 위치 기반 서비스(LBS)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;특정 반경 내 사업장을 조회하는 컴포넌트이며, 오직 &lt;strong&gt;읽기 요청만 수없이 발생&lt;/strong&gt;한다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Stateless 서비스&lt;/strong&gt;이므로 인구 밀집 지역의 트래픽 폭증 시 즉각적인 Scale-out이 가능하다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2) 사업장 서비스&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;소유주의 정보 관리(쓰기) 및 고객의 상세 조회(읽기)를 담당한다.&lt;/li&gt;
  &lt;li&gt;쓰기 QPS는 상대적으로 낮아 시스템 부하가 적다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3) 아키텍처 확장성 포인트&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;복제 지연(Replication Lag) 허용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Master DB와 Replica 간의 비동기 복제로 인한 미세한 데이터 불일치가 발생할 수 있다.&lt;/li&gt;
      &lt;li&gt;그러나 비기능 요구사항에서 검토했듯 사업장 정보는 실시간 반영이 불필요하므로 이러한 구조가 가용성 면에서 완벽히 유리하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;오토스케일링&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사업장 서비스와 LBS 모두 Stateless이므로 피크 타임(예: 점심 시간)에 맞추어 유연하게 서버 대수를 자동으로 조절할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-주변-사업장-검색-알고리즘&quot;&gt;2.2. 주변 사업장 검색 알고리즘&lt;/h2&gt;

&lt;p&gt;대규모 시스템에서 공간 정보를 다루는 대표적인 알고리즘 5가지의 핵심 원리와 한계에 대해 알아본다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;실제로 많은 회사가 &lt;a href=&quot;https://redis.io/docs/latest/commands/GEOHASH/&quot;&gt;레디스 지오해시(Geohash in Redis)&lt;/a&gt;나 PostGIS 확장을 설치한 Postgres DB를 활용한다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;221-2차원-검색의-한계&quot;&gt;2.2.1. 2차원 검색의 한계&lt;/h3&gt;

&lt;p&gt;가장 단순하게 위도와 경도 범위를 지정하여 SQL 상에서 격자 구역을 쿼리하는 방식이다.&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;business_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;latitude&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;longitude&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;business&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;longitude&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BETWEEN&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;my_lat&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;radius&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;my_lat&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;radius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;latitude&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;BETWEEN&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;my_long&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;radius&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;AND&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;my_long&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;radius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 쿼리는 데이터가 많아질수록 인덱스를 타더라도 테이블 전체를 풀스캔하듯 비효율적으로 작동한다.&lt;br /&gt;
위도와 경도에 인덱스를 만들어도 데이터가 2차원적이므로 컬럼별로 가져온 결과도 여전히 엄청난 양이다.
위도 컬럼의 데이터 집합 1과 경도 컬럼의 데이터 집합 2는 빠르게 추출 가능하지만, 주어진 반경 내 사업장을 얻으려면 두 집합의 교집합을 구해야 하는데,
이 연산은 각 집합에 속한 데이터의 양 때문에 효율적일 수 없다.&lt;/p&gt;

&lt;p&gt;이렇게 인덱스를 하는 것의 문제는 오직 한 차원의 검색 속도만 개선할 수 있다는 것이다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;위도와 경도를 복합키로 만들어도 해결되지 않는다.&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;DB의 복합 인덱스는 기본적으로 &lt;strong&gt;1차원 선형 구조(B-Tree 계열)&lt;/strong&gt;이다.&lt;br /&gt;
인덱스를 (latitude, longitude) 순서로 묶어 생성하게 되면, DB는 첫 번째 조건인 latitude 범위에 해당하는 데이터를 먼저 일렬로 찾은 뒤,&lt;br /&gt;
그 추출된 방대한 데이터 그룹 안에서 일일이 longitude 범위를 필터링해야 한다.&lt;/p&gt;

  &lt;p&gt;즉, 두 차원의 데이터가 독립적으로 사각형 범위를 지니기 때문에 하나의 차원 인덱스 속도만 개선될 뿐,&lt;br /&gt;
나머지 한 축은 인덱스의 혜택을 온전히 받지 못하고 거대한 교집합 연산 비용을 치러야 한다.&lt;/p&gt;

  &lt;p&gt;따라서 2차원 데이터를 1차원 선형 데이터로 변환해 주는 특수 인덱싱이 필요하다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;그럼 자연스럽게 2차원 데이터를 1차원에 대응시킬 방법이 있을까? 를 알아보기 전에 인덱스를 만드는 방법들부터 알아보자.&lt;/p&gt;

&lt;p&gt;지리적 정보에 인덱스를 만드는 방법은 두 종류이다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph TD
    Idx[인덱스]
    
    %% 1단계 분류 (선으로 연결)
    Idx --- Hash(해시 hash)
    Idx --- Tree(트리 tree)

    %% 해시 기반 방안
    Hash --- Grid[균등 격자&amp;lt;br&amp;gt;even grid]
    Hash --- Geohash[지오해시&amp;lt;br&amp;gt;Geohash]
    Hash --- Cartesian[카르테시안 계층&amp;lt;br&amp;gt;Cartesian tiers]

    %% 트리 기반 방안
    Tree --- Quad[쿼드트리&amp;lt;br&amp;gt;Quadtree]
    Tree --- S2[구글 S2]
    Tree --- RTree[R 트리&amp;lt;br&amp;gt;R-tree]

    %% 4개 노드 진한 색상 강조
    style Grid fill:#444,stroke:#222,color:#fff
    style Geohash fill:#444,stroke:#222,color:#fff
    style Quad fill:#444,stroke:#222,color:#fff
    style S2 fill:#444,stroke:#222,color:#fff
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;각 색인법은 구현 방법은 다르지만 지도를 작은 영역으로 분할하고, 고속 검색이 가능하도록 색인을 만든다는 아이디어는 같다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;222-균등-격자even-grid&quot;&gt;2.2.2. 균등 격자(Even grid)&lt;/h3&gt;

&lt;p&gt;지도를 단순하게 일정한 크기의 바둑판 격자로 나누는 직관적인 접근법이다.&lt;/p&gt;

&lt;p&gt;균등 격자의 문제점은 아래와 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;불균등한 분포&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;강남역에는 수만 개의 상점이 모여있지만, 사막 한가운데 격자에는 상점이 단 하나도 없다.&lt;/li&gt;
      &lt;li&gt;전 세계를 동일 크기로 나누면 격자별 데이터 편차가 너무 심해진다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;인접 격자 탐색 분리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;격자 ID 할당 체계에 계층이나 연관성이 없어 내 격자 바로 옆 격자의 식별자를 계산해 내기가 매우 까다롭다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;223-지오해시geohash&quot;&gt;2.2.3. 지오해시(Geohash)&lt;/h3&gt;

&lt;p&gt;2차원의 위도/경도 좌표 데이터를 격자 기반의 재귀적 분할을 통해 &lt;strong&gt;1차원 문자열&lt;/strong&gt;로 인코딩하는 방식이다.&lt;/p&gt;

&lt;p&gt;본초 자오선(Prime Meridian)은 영국 그리니치 천문대를 통과하는 경도 \(0^\circ\)의 기준선이다.&lt;br /&gt;
지구를 동반구(경도 \(0^\circ \sim 180^\circ\))와 서반구(경도 \(-180^\circ \sim 0^\circ\))로 가르는 수직 기준선이 된다.&lt;br /&gt;
지오해시는 바로 이 본초 자오선과 적도(위도 \(0^\circ\))를 기준으로 전 세계를 4개의 사분면으로 조각내며 비트 매핑을 시작한다.&lt;br /&gt;
즉, 2차원의 위도,경도 데이터를 1차원의 문자열로 반환하고, 비트를 하나씩 늘려가면서 재귀적으로 세계를 더 작은 격자로 분할해나가는 방식이다.&lt;/p&gt;

&lt;p&gt;먼저 전 세계를 본초 자오선과 적도 기준 사분면으로 나눈다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;위도 범위 [-90, 0]은 0에 대응&lt;/li&gt;
  &lt;li&gt;위도 범위 [0, 90]은 1에 대응&lt;/li&gt;
  &lt;li&gt;경도 범위 [-180, 0]은 0에 대응&lt;/li&gt;
  &lt;li&gt;경도 범위 [0, 180]은 1에 대응&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;그 격자를 또 다시 사분면으로 나누고, 이 때 각 격자는 경도와 위도 비트를 위처럼 순서대로 반복한다.
이 절차를 원하는 정밀도(precision)를 얻을 때까지 반복한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0524/geohash.png&quot; alt=&quot;지오해시&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.movable-type.co.uk/scripts/geohash.html&quot;&gt;지오해시&lt;/a&gt;는 이진 분할을 반복하며 얻은 비트들을 모아 &lt;strong&gt;Base32&lt;/strong&gt; 형태의 문자열로 나타낸다. 문자의 길이가 길어질수록 격자는 좁아지고 정밀도는 정교해진다.&lt;/p&gt;

&lt;p&gt;예) 구글 본사 지오해시 (길이 = 6)
1001 10110 01001 10000 11011 11010 (base32 이진 표기) → 9q9hvu (base32)&lt;/p&gt;

&lt;p&gt;지오해시는 12단계의 정밀도를 갖는데, 이 정밀도가 격자 크기를 결정한다.&lt;/p&gt;

&lt;p&gt;[지오해시 길이와 격자 크기]&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;지오해시 길이&lt;/th&gt;
      &lt;th&gt;격자 너비 × 높이&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;1&lt;/td&gt;
      &lt;td&gt;5,009.4km × 4,992.6km (지구 전체)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2&lt;/td&gt;
      &lt;td&gt;1,252.3km × 624.1km&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;156.5km × 156km&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;39.1km × 19.5km&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;4.9km × 4.9km&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;6&lt;/td&gt;
      &lt;td&gt;1.2km × 609.4m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;7&lt;/td&gt;
      &lt;td&gt;152.9m × 152.4m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;8&lt;/td&gt;
      &lt;td&gt;38.2m × 19m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;9&lt;/td&gt;
      &lt;td&gt;4.8m × 4.8m&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;10&lt;/td&gt;
      &lt;td&gt;1.2m × 59.5cm&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;11&lt;/td&gt;
      &lt;td&gt;14.9cm × 14.9cm&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;12&lt;/td&gt;
      &lt;td&gt;3.7cm × 1.9cm&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;그럼 최적 정밀도는 어떻게 정할까?
사용자가 지정한 반경으로 그린 원을 덮는 최소 크기 격자로 만드는 지오해시 길이를 구해야 한다.&lt;/p&gt;

&lt;p&gt;[검색 반경과 지오해시 길이]&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;반지름 (킬로미터)&lt;/th&gt;
      &lt;th&gt;지오해시 길이&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;0.5km (0.31마일)&lt;/td&gt;
      &lt;td&gt;6&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;1km (0.62마일)&lt;/td&gt;
      &lt;td&gt;5&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;2km (1.24마일)&lt;/td&gt;
      &lt;td&gt;5&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5km (3.1마일)&lt;/td&gt;
      &lt;td&gt;4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;20km (12.42마일)&lt;/td&gt;
      &lt;td&gt;4&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2231-격자-가장자리edge-이슈&quot;&gt;2.2.3.1. 격자 가장자리(Edge) 이슈&lt;/h4&gt;

&lt;p&gt;지오해시는 공통 접두어(prefix)가 길수록 두 지점이 물리적으로 가깝다는 특성을 가지지만, &lt;strong&gt;그 역은 성립하지 않을 수 있다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0524/geo1.png&quot; alt=&quot;지오해시&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;-- 대단히 위험한 쿼리 예시&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;SELECT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;geohash_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;WHERE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;geohash&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;LIKE&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;u17e0%&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;두 사업장이 단 30km 거리를 두고 마주보고 있더라도, 한 사업장은 적도 위(u000), 한 사업장은 적도 아래(ezzz) 격자에 걸치게 되면 두 지오해시 문자열은 
&lt;strong&gt;공통 접두어가 단 한 글자도 존재하지 않게 된다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이에 대한 해결책은 내 위치가 속한 격자 하나만 검색하면 안 되며, 내 격자를 에워싸고 있는 &lt;strong&gt;인접 격자 8개(총 9개 격자)의 사업장을 상시 함께 조회&lt;/strong&gt;하여 취합해야 한다.&lt;br /&gt;
인접 격자를 구하는 연산은 상수 시간(\(O(1)\)) 내에 매우 빠르게 처리된다.&lt;/p&gt;

&lt;p&gt;격자 가장자리의 또 다른 문제는 두 지점이 공통 prefix 길이는 길지만 서로 다른 격자에 놓이는 경우이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0524/geo2.png&quot; alt=&quot;지오해시&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2232-표시할-사업장이-충분하지-않은-경우&quot;&gt;2.2.3.2. 표시할 사업장이 충분하지 않은 경우&lt;/h4&gt;

&lt;p&gt;현재 격자와 주변 격자를 다 보아도 표시할 사업장이 충분하지 않으면 어떻게 해야 할까?&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;주어진 반경 내 사업장만 반환한다.
    &lt;ul&gt;
      &lt;li&gt;구현하기 쉽지만 사용자의 욕구를 만족하기 충분한 수의 사업장 정보를 반환하지 못함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;검색 반경을 키운다.
    &lt;ul&gt;
      &lt;li&gt;지오해시의 마지막 비트를 삭제하여 얻은 새 지오해시 값을 사용하여 주변 사업장을 검색한다.&lt;/li&gt;
      &lt;li&gt;그래도 사업장 수가 충분하지 않으면 또 한 비트를 지워서 범위를 다시 확장한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;224-쿼드트리quadtree&quot;&gt;2.2.4. 쿼드트리(Quadtree)&lt;/h3&gt;

&lt;p&gt;쿼드트리는 자식 노드를 정확히 4개씩(2 * 2) 가질 수 있는 메모리 기반 트리 자료 구조이다.&lt;br /&gt;
“하나의 격자 내부 사업장 수가 100개를 초과하면 공간을 사분면으로 쪼갠다”는 조건 기반으로 공간을 분할한다.&lt;/p&gt;

&lt;p&gt;쿼드트리를 사용한다는 것은 결국 질의에 답하는 데 사용될 트리 구조를 메모리 안에서 만드는 것이다.
쿼드트리는 메모리 안에 놓이는 자료 구조일 뿐 DB가 아니라는 것에 유의하자.&lt;/p&gt;

&lt;p&gt;이 자료구조는 각각의 LBS 서버에 존재해야 하며, 서버가 시작되는 시점에 구축된다.&lt;/p&gt;

&lt;p&gt;전 세계에 백만개의 사업장이 있다고 해보자.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0524/quad1.png&quot; alt=&quot;쿼드트리&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그 과정을 좀 더 자세히 시각화하면 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0524/quad2.png&quot; alt=&quot;쿼드트리&quot; /&gt;&lt;/p&gt;

&lt;p&gt;트리의 루트 노드는 세계 전체 지도이고, 이 루트 노드를 사분면 각각을 나타내는 하위 노드로, 어떤 노드도 사업장 100개를 넘지 않을 때까지 재귀적으로 분할한다.&lt;/p&gt;

&lt;div class=&quot;language-kotlin highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 쿼드트리 구축 핵심 메커니즘 예시&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;buildQuadtree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;TreeNode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;countNumberOfBusinessesInCurrentGrid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;subdivide&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 공간을 4개의 자식 노드로 분할&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;child&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;children&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nf&quot;&gt;buildQuadtree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;child&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;// 재귀 호출&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2241-쿼드트리를-저장하는데-필요한-메모리는&quot;&gt;2.2.4.1. 쿼드트리를 저장하는데 필요한 메모리는?&lt;/h4&gt;

&lt;p&gt;이 질문에 답하려면 어떤 데이터가 쿼드트리에 보관되는지 먼저 알아야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Leaf Node에 저장되는 데이터&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;격자를 식별하는데 사용할 좌상단과 우하단 꼭짓점 좌표: 32 byte(8byte * 4)
        &lt;ul&gt;
          &lt;li&gt;2차원 평면의 격자 범위를 특정하려면 &lt;strong&gt;2개 점&lt;/strong&gt;의 좌표가 필요하다. 즉, ‘좌상단 꼭짓점’과 ‘우하단 꼭짓점’이다.&lt;/li&gt;
          &lt;li&gt;그런데 점 하나는 내부적으로 &lt;strong&gt;(위도,경도)&lt;/strong&gt;라는 2개의 실수형(double, 8 byte) 데이터 쌍으로 이루어져 있다.&lt;/li&gt;
          &lt;li&gt;따라서 \(\text{점 2개} \times \text{좌표 2개(위도, 경도)} = \text{총 4개의 실수 데이터}\)가 필요하기 때문에 물리적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8byte * 4 = 32byte&lt;/code&gt;가 노드에 할당되는 것이다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;격자 내부 사업장 ID 목록: ID당 8byte * 100 (한 격자에 허용되는 사업장 수의 최댓값)
        &lt;ul&gt;
          &lt;li&gt;대개 ID(식별자)는 미래의 대규모 데이터 확장성을 고려하여 64bit 정수형(= 8byte)을 사용하기 때문에 하나의 사업장 ID는 8 byte(Java의 Long, DB의 BIGINT)&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;총 832 byte&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Internal Node에 저장되는 데이터&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;격자를 식별하는데 사용할 좌상단과 우하단 꼭짓점 좌표: 32 byte(8 byte * 4)&lt;/li&gt;
      &lt;li&gt;하위 노드 4개를 가리킬 포인터: 32 byte(8 byte * 4)
        &lt;ul&gt;
          &lt;li&gt;메모리 주소를 가리키는 포인터 크기는 OS와 CPU의 아키텍처에 따라 결정된다.&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;포인터 1개의 크기 = 8byte&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;우리가 사용하는 대부분의 서버용 OS와 CPU는 64비트 아키텍처이다.&lt;/li&gt;
              &lt;li&gt;64비트 시스템에서 메모리의 특정 주소를 가리키는 포인터 변수 1개의 크기는 예외없이 정확히 64비트, 즉 &lt;strong&gt;8byte&lt;/strong&gt;가 된다.&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;하위 노드가 4개인 이유&lt;/strong&gt;
            &lt;ul&gt;
              &lt;li&gt;쿼드트리는 이름 그대로 공간을 늘 4개(사분면)로 분할하는 트리구조이다.&lt;/li&gt;
              &lt;li&gt;하나의 부모(Internal node)는 쪼개진 4개의 자식 노드 주소를 모두 알고 있어야 한다.&lt;/li&gt;
              &lt;li&gt;따라서 부모 노드가 자식 노드 4개의 메모리 주소를 저장하기 위해 필요한 공간은 아래와 같이 계산된다.&lt;/li&gt;
              &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;포인터 크기(8byte) * 자식 노드 개수(4개) = 32byte&lt;/code&gt;&lt;/li&gt;
            &lt;/ul&gt;
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;총 64 byte&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;한 격자에 허용되는 사업장 수의 최댓값에 좌우되기는 하지만 그 값은 트리 안에 저장하지 않아도 된다.  &lt;br /&gt;
메모리에 생성된 쿼드트리 구조 자체가 이미 조건(최대 100개)에 맞춰 하위 노드로 쪼개져 분할을 끝마친 상태이기 때문이다.&lt;br /&gt;
즉, 노드 인스턴스 하나하나마다 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_limit = 100&lt;/code&gt; 이라는 상숫값 정보를 변수로 중복해서 저장하여 아까운 메모리 공간을 낭비할 필요가 없다는 뜻이다.&lt;br /&gt;
인프라 설계 측면에서 순수 데이터만 적재하려는 극단적인 공간 최적화 관점이다.&lt;/p&gt;

&lt;p&gt;이제 각 노드가 어떤 데이터를 저장하는지 알았으니 메모리를 계산해보자.
전 세계에는 이백만개의 사업장이 있다고 가정한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;격자 안에는 최대 100개의 사업장이 있을 수 있음&lt;/li&gt;
  &lt;li&gt;Leaf node의 수 =~ \(\frac{200m}{100}\) =~ 2백만(2m)&lt;/li&gt;
  &lt;li&gt;Internal node의 수 = 2m * \(\frac{1}{3}\) =~ 0.67m
    &lt;ul&gt;
      &lt;li&gt;Internal node의 수가 왜 Leaf node 수의 \(\approx \frac{1}{3}\) 인지는 &lt;a href=&quot;https://stackoverflow.com/questions/35976444/how-many-leaves-has-a-quadtree&quot;&gt;쿼드트리에는 얼마나 많은 Leaf node가 있는가&lt;/a&gt; 를 참고하면 된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;총 메모리 요구량 = 2m * 832byte + 0.67m * 64byte =~ 1.71GB
트리를 구축하는데 드는 부가적인 메모리를 감안하더라도 총 메모리 요구량은 꽤 작다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;Internal node의 수가 왜 Leaf node 수의 \(\approx \frac{1}{3}\) 인 이유&lt;/strong&gt;&amp;gt;&lt;br /&gt;
단 1개의 노드에서 시작해 트리가 확장되는 과정을 따라가보면 이해가 된다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;단계 0(최초 상태)&lt;/strong&gt;: 루트 노드 1개만 존재한다. 아직 쪼개지지 않았으니 Leaf node이다.
    &lt;ul&gt;
      &lt;li&gt;Internal node: 0, Leaf node: 1&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단계 1(1번 분할)&lt;/strong&gt;: 루트 노드에 데이터가 많아서 4개로 쪼갠다. 이제 루트는 Internal node가 되고, 아래 4개는 자식이 없는 Leaf node가 된다.
    &lt;ul&gt;
      &lt;li&gt;Internal node: 1, Leaf node: 4 → (\(1 = \frac{(4-1)}{3}\) 성립)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단계 2(2번 분할)&lt;/strong&gt;: 4개의 Leaf node 중 단 1개만 데이터가 또 많아서 4개로 추가 분할되었다고 해보자.
    &lt;ul&gt;
      &lt;li&gt;기존 Leaf node 4개 중 1개가 Internal node로 승격되었으니 Internal node = 1 + 1 = 2개&lt;/li&gt;
      &lt;li&gt;Leaf node는 기존 3개에서 새로 생긴 Leaf node 4개가 더해지므로 Leaf node = 3 + 4 = 7개&lt;/li&gt;
      &lt;li&gt;Internal node: 2, Leaf node = 7 → (\(2 = \frac{(7-1)}{3}\) 성립)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이렇게 쿼드트리는 공간을 한 번 쪼갤 때마다 Internal node는 1개씩 늘어나는 반면, Leaf node는 순증가분 기준으로 항상 3개씩(4-1) 늘어나는 특징이 있다.&lt;br /&gt;
따라서 트리가 아무리 거대해져도 Internal node의 총 개수는 언제나 Leaf node 개수의 \(\approx \frac{1}{3}\) 에 수렴하게 된다.&lt;br /&gt;
수억 개의 데이터를 다루는 아키텍처 설계에서도 이 불변의 법칙 덕분에 정확한 메모리 예측이 가능하다.&lt;/p&gt;

&lt;p&gt;쿼드트리 인덱스가 메모리를 적게 사용하지만 읽기 연산 양이 많아지면 서버 한 대의 CPU나 네트워크 대역폭으로는 감당하기 어려워지므로 읽기 연산을 여러 대 쿼드트리 서버로 분리하는 것이 좋다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2242-쿼드트리-구축에-소요되는-시간은&quot;&gt;2.2.4.2. 쿼드트리 구축에 소요되는 시간은?&lt;/h4&gt;

&lt;p&gt;각 Leaf node에는 대략 100개의 사업장 ID가 저장된다.
전체 사업장 수를 n이라 하면 트리를 구축하는 시간 복잡도는 \(\frac{n}{100}\log\frac{n}{100}\) 이다.&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;\(\frac{n}{100}\log\frac{n}{100}\)의 상세 설명&lt;/strong&gt;&amp;gt;&lt;br /&gt;
전체 사업장 개수가 n이고, 한 Leaf node가 감당하는 최대 용량이 100개이므로, 최종적으로 트리가 다 쪼개졌을 때 형성되는 Leaf node의 총 개수는 대략 \(m = \frac{n}{100}\) 개가 된다.&lt;br /&gt;
완전히 균형 잡힌 사분트리 구조에서 새로운 요소를 탐색하고 분할 깊이를 찾아내려가는 과정의 깊이(Height)는 \(\log m\), 즉 \(\log(\frac{n}{100})\) 수준에 수렴한다.&lt;br /&gt;
따라서 이 조각난 공간 단위를 기준으로 전체 인덱싱 트리를 완전히 조립하고 배치하는 총 구축 작업의 시간 복잡도는 Leaf 개수와 깊이(Height)의 곱인 \(O(m \log m) \rightarrow \frac{n}{100}\log\frac{n}{100}\) 스케일을 형성하게 된다.&lt;/p&gt;

&lt;p&gt;200m개의 사업장 정보를 인덱싱하는 쿼드트리 구축에는 몇 분 정도가 소요될 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2243-쿼드트리로-주변-사업장을-검색하려면&quot;&gt;2.2.4.3. 쿼드트리로 주변 사업장을 검색하려면?&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;메모리에 쿼드트리 인덱스를 구축한다.&lt;/li&gt;
  &lt;li&gt;검색 시작점이 포함된 Leaf node를 만날 때까지, 트리의 루트 노드부터 탐색한다.
    &lt;ul&gt;
      &lt;li&gt;해당 노드에 100개의 사업장이 있다면? → 해당 노드만 반환한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;그렇지 않다면? → 충분한 사업장 수가 확보될 때까지 인접 노드도 추가한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2244-쿼드트리-운영-시-고려사항&quot;&gt;2.2.4.4. 쿼드트리 운영 시 고려사항&lt;/h4&gt;

&lt;p&gt;쿼드트리는 메모리 내에서 동작하므로 탐색 속도가 압도적으로 빠르다는 장점이 있지만, ‘모든 인덱스 데이터가 가동 중인 서버의 메모리(RAM)에만 존재한다’는 특성 때문에 
배포와 데이터 변경 관점에서 까다로운 운영 리스크를 동반한다.&lt;br /&gt;
수억 개의 장소 데이터를 메모리에 올리고 동기화하는 과정에서 발생하는 인프라 병목 현상과 아키텍처적 해결책을 명확히 이해해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1.서버 초기 기동 지연 및 DB 과부하(Thundering Herd)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚨문제점: 무거운 배포와 DB 병목&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;쿼드트리 인덱스는 서버가 메모리에 직접 구축해야 하는 자료구조이므로, &lt;strong&gt;LBS(위치 기반 서비스) 서버가 시작되는 시점에 전체 인덱스를 빌드&lt;/strong&gt;해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;서비스 중단 리스크&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;트리를 새로 구축하는 동안 클라이언트의 검색 트래픽을 처리할 수 없음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DB Thundering Herd 현상&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;서버를 배포하거나 Scale-out할 때, 수십 대의 LBS 서버가 대규모 사업장 정보(약 2억개)를 가져오기 위해 &lt;strong&gt;DB에 동시에 READ 질의&lt;/strong&gt;를 던짐&lt;/li&gt;
      &lt;li&gt;이로 인해 Master 혹은 Replica DB의 CPU가 100%를 찍으며 시스템 전체가 마비될 수 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;💡해결 전략: Blue/Green 배포 및 기동 시차 분산&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Blue/Green 배포&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;구 버전 서버 레이어(Blue)가 트래픽을 안정적으로 처리하는 동안, 새 버전 서버 레이어(Green)를 백그라운드에 띄워 쿼드트리 인덱스 빌드를 완전히 끝마친 후 트래픽 라우팅을 한 번에 전환함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단계적 기동(Staggered Startup)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;새 서버들을 동시에 기동하지 않고 몇 분의 시차를 두고 순차적으로 띄워서 DB 레이어가 감당할 수 있는 수준으로 읽기 부하 제어&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2.데이터 추가/삭제 시 인덱스 갱신 및 동시성 제어 전략&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;사용자가 새로운 사업장을 등록하거나 삭제할 때, 메모리에 상주하는 쿼드트리 인덱스를 어떻게 동기화할 것인가에 대한 문제이다.&lt;br /&gt;
여기에는 3가지 트레이드오프가 존재한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;방안 A: 점진적/순차적 갱신(가장 추천하는 현실적 대안)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클러스터 내부에 존재하는 수십 대의 LBS 서버를 한 번에 동기화하지 않고, &lt;strong&gt;롤링 업데이트 방식으로 서버를 몇 개씩만 순차적으로 갱신&lt;/strong&gt;하는 전략이다.
        &lt;ul&gt;
          &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;: 시스템 전체에 가해지는 동기화 부하가 적고 구현이 단순함&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;: 서버마다 인덱스 반영 시점이 미세하게 달라져, 사용자가 접속한 서버에 따라 아주 짧은 시간 동안 일시적으로 오래된 데이터(Stale Data)가 반환될 수 있음&lt;/li&gt;
          &lt;li&gt;&lt;strong&gt;적합성&lt;/strong&gt;: 요구사항 분석 단계에서 ‘신규 사업장 정보는 다음 날까지만 반영되어도 무방하다’라고 합의했으므로, 이 미세한 데이터 불일치는 서비스 운영상 전혀 문제가 되지 않음&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;방안 B: 야간 일괄 무효화 및 캐시 갱신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;트래픽이 가장 적은 새벽 시간대에 배치를 돌려 인덱스와 캐시 데이터를 통째로 새로고침하는 방식이다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;⚠️주의 사항(Cache Stampede 위험)&lt;/strong&gt;: 수많은 인덱스 key가 한 번에 만료되거나 무효화되면, 아침 피크 타임이 시작되자마자 수많은 검색 요청이 캐시를 
통과하여 백엔드 서버와 DB로 일시에 몰리는 &lt;strong&gt;캐시 스탬피드&lt;/strong&gt; 현상이 발생하여 서버가 다운될 수 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;방안 C: 실시간 인덱스 갱신&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터가 변경될 때마다 메모리에 있는 쿼드트리 구조를 즉시 변경하는 방식이다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;단점(극심한 복잡성과 Lock 병목)&lt;/strong&gt;: 대규모 트래픽 환경에서 수많은 스레드가 하나의 쿼드트리 자료구조에 동시에 접근하여 읽고 쓰는 멀티스레드 환경이 강제된다. &lt;br /&gt;
 데이터 무결성을 지키려면 &lt;strong&gt;&lt;a href=&quot;https://assu10.github.io/dev/2024/10/20/mutex-semaphore/&quot;&gt;뮤텍스(Mutex)&lt;/a&gt;나 Lock 메커니즘&lt;/strong&gt;을 도입해야 하는데, 이로 인해 쓰기 작업이 수행되는 동안 수만 건의 읽기(검색) 요청이 대기하게 되므로 서비스 응답 지연이 급격히 악화된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;실무에서는 아래와 같이 운영하는 것이 현실적이다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;쿼드트리는 빌드 시 막대한 DB READ 부하를 유발하므로 반드시 &lt;strong&gt;Blue/Green 배포&lt;/strong&gt;와 서버 기동 시차 분산 전략을 결합해야 한다.&lt;/li&gt;
  &lt;li&gt;실시간 동기화는 Lock으로 인한 성능 저하를 초래하므로, 비실시간 비즈니스 요구사항에 맞춰 점진적 갱신(Eventual Consistency)이나 야간 배치를 활용하는 것이 대규모 아키텍처 관점에서 훨씬 성숙한 설계이다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;2245-실제-사용되는-쿼드트리-사례&quot;&gt;2.2.4.5. 실제 사용되는 쿼드트리 사례&lt;/h4&gt;

&lt;p&gt;아래는 &lt;a href=&quot;https://www.educative.io/answers/what-is-a-quadtree-how-is-it-used-in-location-based-services&quot;&gt;실제 사용되는 쿼드트리 구축 사례&lt;/a&gt;이다.
인구 밀집 지역에는 작은 격자를, 그렇지 않은 지역에는 큰 격자를 사용한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0524/quad3.png&quot; alt=&quot;쿼드트리&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;225-구글-s2&quot;&gt;2.2.5. 구글 S2&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://s2geometry.io/&quot;&gt;구글 S2 기하(geometry) 라이브러리&lt;/a&gt;는 아주 유명한 솔루션이다.&lt;br /&gt;
쿼드트리처럼 구글 S2도 메모리 기반이다.&lt;/p&gt;

&lt;p&gt;구글 S2는 지구 전체를 3차원 정육면체로 감싸 안은 뒤, 이를 투영하여 &lt;a href=&quot;https://en.wikipedia.org/wiki/Hilbert_curve&quot;&gt;힐베르트 곡선(Hilbert curve)&lt;/a&gt;이라는 공간 채움 곡선(space-filling curve)을 
따라 1차원 인덱스로 매칭하는 최첨단 geometry 라이브러리이다.&lt;br /&gt;
힐베르트 곡선 내부에서 인접한 두 격자는 1차원으로 인코딩된 결과물에서도 매우 가깝게 붙어있다는 가공할 만한 장점을 지니고 있다.&lt;br /&gt;
1차원 공간 내에서의 검색은 2차원 공간에서의 검색보다 훨씬 효율적이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Geofence&quot;&gt;지오펜스(Geofence)&lt;/a&gt;&lt;/strong&gt;는 지리적(Geographic) 공간과 울타리(Fence)의 합성어로, 현실 세계의 물리적 
특정 지역 위에 설정한 &lt;strong&gt;가상의 인지적 경계선(울타리)&lt;/strong&gt;를 의미한다.&lt;br /&gt;
사용자의 스마트폰 GPS가 이 펜스 영역 안으로 ‘진입’하거나 ‘이탈’할 때 실시간 푸시 알림을 쏘거나 이벤트를 발생시키는 기술에 사용된다.&lt;br /&gt;
구글 S2는 셀의 크기를 유연하게 조정할 수 있는 &lt;a href=&quot;https://s2.inair.space/&quot;&gt;영역 지정 알고리즘(Region Cover Algorithm)&lt;/a&gt;을 탑재하고 있어서 
지오해시처럼 고정된 정밀도를 사용하는 것이 아니라 최소 수준, 최고 수준, 최대 셀 개수 등을 지정할 수 있다.&lt;br /&gt;
이렇게 셀 크기를 유연하게 조정할 수 있기 때문에 복잡한 다각형 형태의 스쿨존이나 정교한 지오펜스 경계를 구현하는데 전 세계에서 가장 최적화되어 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-지오해시-vs-쿼드트리&quot;&gt;2.3. 지오해시 vs 쿼드트리&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;비교 항목&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;지오해시&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;쿼드트리&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;구현 난이도&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;매우 쉬움(문자열 연산 중심)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;까다로움(메모리 내 트리 관리 필요)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;격자 크기&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;정밀도별 고정 크기(동적 조절 불가)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;인구 밀도에 따라 &lt;strong&gt;동적 크기 조절&lt;/strong&gt; 가능&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;색인 갱신 성능&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;매우 빠름(\(O(1)\) 단일 열 제거/추가)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;비교적 무거움(\(O(\log n)\), n은 사업장 개수, 트리 순회 및 Lock 필요)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;특수 기능&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;단순 반경 내 검색 특화&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;내 위치에서 가장 가까운 &lt;strong&gt;k개 장소 찾기&lt;/strong&gt;에 특화&lt;br /&gt;하위 노드 분할 과정이 숫자 k에 기반하는데다가 k개 사업장을 찾을 때까지 검색 범위를 자동으로 조정할 수 있기 때문&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;사용 기업&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Bing 지도, Redis, MongoDB, Elasticsearch&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;엑스트(Yext), Elasticsearch&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;쿼드트리에서 색인 하나를 지우거나 더하려면 루트부터 하위 Leaf node까지 트리의 높이 깊이만큼 반드시 탐색 연산을 수행해야 하므로, 
트리의 높이에 비례하는 &lt;strong&gt;\(O(\log n)\)의 시간 복잡도&lt;/strong&gt;를 가지며, 동시성 제어를 위한 Lock 메커니즘이 수반되어 구조가 무거워진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;대규모 근접성 서비스 핵심 설계 요약&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;대규모 트래픽 분산을 위해 인프라는 Stateless 아키텍처 서버 계층과 오픈소스 MySQL Primary-Secondary 클러스터 조합으로 토대를 구축한다.&lt;/li&gt;
  &lt;li&gt;2차원 공간 조건 쿼리는 복합키 인덱스의 1차원적 한계로 인해 대량의 스캔 유발을 피할 수 없다.&lt;/li&gt;
  &lt;li&gt;대안으로 사용되는 지오해시는 단순 구조와 빠른 갱신 속도가 돋보이지만 가장자리 이슈 처리를 위해 주변 8개 격자를 항상 함께 묶어 질의해야 한다.&lt;/li&gt;
  &lt;li&gt;쿼드트리는 인구 밀도에 맞춰 유연하게 공간을 쪼갤 수 있어 k차 근접 검색에 최상인 반면, 메모리 상의 Lock 관리 및 배포 시 트리 재구축 비용을 신중히 다뤄야 한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-상세-설계-db-샤딩-초고효율-공간-캐싱&quot;&gt;3. 상세 설계: DB 샤딩, 초고효율 공간 캐싱&lt;/h1&gt;

&lt;p&gt;개략적인 구조 설계를 넘어 상세 설계 단계에서는 시스템의 병목을 완전히 제거하는데 집중한다.&lt;br /&gt;
DB의 물리적 한계를 극복하기 위한 수평 분할(샤딩) 기법, GPS 노이즈를 방어하는 정적 격자 기반의 레디스 캐싱 모델, 그리고 글로벌 리전 배치를 통한 데이터 사생활 보호(GDPR) 대응까지 
실무 아키텍처의 디테일을 완성한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-데이터베이스-규모-확장성-및-샤딩-전략&quot;&gt;3.1. 데이터베이스 규모 확장성 및 샤딩 전략&lt;/h2&gt;

&lt;h3 id=&quot;311-사업장-테이블-및-지오해시-테이블-설계&quot;&gt;3.1.1. 사업장 테이블 및 지오해시 테이블 설계&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;사업장 테이블(business)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;등록된 사업장 수가 2억 개에 달하므로 단일 DB 서버의 디스크 용량과 성능 압박을 해결하기 위해 샤딩을 적용하기 좋은 후보이다.&lt;/li&gt;
      &lt;li&gt;가장 직관적이고 효과적인 샤딩 방식은 &lt;strong&gt;고유 식별자인 사업장 ID(business_id)를 샤딩 키&lt;/strong&gt;로 삼아 데이터를 여러 노드로 균등하게 분산하는 것이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;지리 정보 색인 테이블&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;지오해시를 DB 내에 저장하고 활용하는 방법은 크게 두 가지 전략으로 나뉜다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;구성 방안&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;저장 형태&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;장점&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;단점&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;strong&gt;방안 1: JSON 배열 구조&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;하나의 지오해시 코드(key)에 속한 모든 사업장 ID들을 JSON 배열로 묶어 단일 열에 보관&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;특정 격자 내부의 ID 목록을 한 번에 조회할 수 있어 읽기 속도가 최적화됨&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;사업장 추가/수정 시 JSON을 먼저 읽어 ID 중복을 체크해야 하고, 병렬 쓰기 시 데이터 유실을 막기 위한 &lt;strong&gt;Lock 메커니즘이 강제&lt;/strong&gt;됨&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;strong&gt;방안 2: 개별 레코드 구조&lt;/strong&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;동일한 지오해시 구역에 속하더라도 사업장 ID마다 각각 독립된 열을 생성하여 저장&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;지오해시와 사업장 ID를 복합키&lt;/strong&gt;로 설정하면 Lock을 걸 필요가 없어 추가/삭제가 매우 용이함&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;데이터 열의 개수가 사업장 수만큼 늘어남&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;대규모 아키텍처에서는 동시성 제어 병목을 유발하는 Lock을 최대한 배제하는 것이 철칙이므로, 방안 2(복합키를 활용한 개별 레코드 구조)를 채택하는 것이 훨씬 성숙한 설계이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;312-지리-정보-색인의-규모-확장&quot;&gt;3.1.2. 지리 정보 색인의 규모 확장&lt;/h3&gt;

&lt;p&gt;RDBMS의 경우 부하 분산에 두 가지 전략이 흔히 사용된다.&lt;br /&gt;
하나는 사본 DB 서버를 늘리는 것이고, 다른 하나는 샤딩을 사용하는 것이다.&lt;br /&gt;
지리 정보 색인의 규모 확장 시 테이블에 보관되는 데이터의 실제 크기를 고려하지 않고 샤딩 방법을 결정하는 실수를 하곤 한다.&lt;/p&gt;

&lt;p&gt;전체 2억 개의 지리 정보 색인 테이블을 구축하는데 필요한 순수 데이터양은 앞서 &lt;a href=&quot;#2241-쿼드트리를-저장하는데-필요한-메모리는&quot;&gt;2.2.4.1. 쿼드트리를 저장하는데 필요한 메모리는?&lt;/a&gt;에서 계산한 
쿼드트리 메모리 명세와 유사하게 대략 &lt;strong&gt;1.71GB&lt;/strong&gt;에 불과하다. 이 정도 크기는 저렴한 DB 서버 한 대의 메모리도 통째로 수용할 수 있는 용량이다.&lt;/p&gt;

&lt;p&gt;따라서 데이터 적재 공간의 한계보다는, 엄청난 읽기 트래픽으로 인한 &lt;strong&gt;CPU 및 네트워크 대역폭 부족이 진짜 병목&lt;/strong&gt;이 된다.&lt;br /&gt;
이를 해결하기 위해 Read Replica를 다량 증설하여 읽기 부하를 분산하는 전략이 매우 유용하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;💡왜 지오해시 테이블 샤딩 로직은 애플리케이션 계층에 구현해야 하며 까다로울까?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;일반적인 DB 샤딩은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hash(ID) % 서버 대수&lt;/code&gt;와 같이 단일 키의 해시값을 기반으로 특정 서버로 요청을 바로 라우팅한다.&lt;br /&gt;
하지만 지오해시를 통한 주변 검색은 격자 가장자리 이슈를 극복하기 위해 &lt;strong&gt;내 위치 격자를 포함한 인접 8개의 격자(총 9개 지오해시 코드)를 동시에 조회&lt;/strong&gt;해야 한다.&lt;br /&gt;
만약 지오해시 테이블을 무턱대고 샤딩해 버리면, 이 9개의 격자 데이터가 서로 전혀 다른 물리 샤드 서버에 쪼개져 저장될 수 있다.&lt;/p&gt;

&lt;p&gt;RDBMS 자체는 여러 샤드에 흩어진 분산 데이터를 스스로 모아서 합쳐주는 기능을 네이티브하게 처리하지 못한다.&lt;br /&gt;
결국 &lt;strong&gt;애플리케이션 레이어&lt;/strong&gt;에서 9개의 지오해시 타겟 샤드 위치를 직접 계산해 낸 뒤, 각 샤드 서버에 쿼리를 병렬로 찔러 넣고, 돌아온 데이터들을 합산/정렬(Scatter-Gather-패턴)하는 
복잡한 분산 제어로 로직을 개발자가 직접 코드로 짜야 하므로 설계가 대단히 까다로워진다.&lt;/p&gt;

&lt;p&gt;따라서 대규모 데이터 수용이 목적이 아니라면, 샤딩 대신 관리와 개발이 훨씬 간편한 &lt;strong&gt;Read Replica 클러스터 구조&lt;/strong&gt;를 적극 추천한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-대규모-트래픽을-담당하는-고성능-캐시-아키텍처&quot;&gt;3.2. 대규모 트래픽을 담당하는 고성능 캐시 아키텍처&lt;/h2&gt;

&lt;h3 id=&quot;321-캐시-계층-도입-기준&quot;&gt;3.2.1. 캐시 계층 도입 기준&lt;/h3&gt;

&lt;p&gt;무조건 캐시를 도입하는 것은 인프라 비용과 아키텍처 복잡도만 높이는 악수가 될 수 있으므로 도입 전에 데이터 크기를 먼저 보아야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;읽기 중심이고, DB 크기가 상대적으로 작아서 모든 데이터가 한 대 DB 서버에 수용 가능하다면 이 때 질의문 처리 성능은 I/O에 좌우되지 않으므로 메모리 캐시를 사용할 때와 비슷하다.&lt;/li&gt;
  &lt;li&gt;읽기 성능이 병목이라면 Replica DB를 증설해서 읽기 대역폭을 늘릴 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;💡데이터 크기가 작아 DB 한 대에 다 들어가면 왜 질의 처리 성능이 I/O에 좌우되지 않을까?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MySQL(InnoDB) 같은 현대적인 RDBMS는 메모리 내에 &lt;strong&gt;버퍼 풀(Buffer Pool)&lt;/strong&gt;이라는 거대한 캐싱 공간을 운용한다.&lt;br /&gt;
디스크에서 자주 읽는 데이터나 인덱스를 RAM 영역이 버퍼 풀에 상시 상주시키는 장치이다.&lt;/p&gt;

&lt;p&gt;지리 정보 색인 데이터는 고작 1.71GB 규모이므로, DB 서버의 RAM 버퍼 풀 크기 안에 전체 인덱스가 100% 들어맞게 된다.&lt;br /&gt;
즉, 사용자가 조회를 요청할 때 디스크 드라이브를 긁는 느린 물리 I/O 동작이 전혀 발생하지 않고, &lt;strong&gt;오직 초고속 RAM 메모리 상에서만 쿼리 hit가 발생&lt;/strong&gt;한다.&lt;/p&gt;

&lt;p&gt;이 경우 DB 자체가 인메모리 캐시 서버처럼 동작하므로, 굳이 외부에 레디스를 추가로 두지 않아도 I/O 병목이 사라져 메모리 캐시와 맞먹는 압도적인 속도를 내뿜게 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;322-올바른-공간-캐시-key-설계&quot;&gt;3.2.2. 올바른 공간 캐시 key 설계&lt;/h3&gt;

&lt;h4 id=&quot;3221-사용자-raw-좌표위도-경도를-캐시-key로-쓰는-경우의-참사&quot;&gt;3.2.2.1. 사용자 raw 좌표(위도, 경도)를 캐시 key로 쓰는 경우의 참사&lt;/h4&gt;

&lt;p&gt;가장 직관적으로 사용자의 GPS 좌표 문자열(예: 37.123456,127.123456)을 캐시 키로 설정하는 방안이다.&lt;/p&gt;

&lt;p&gt;스마트폰의 GPS 디바이스가 주는 좌표는 사용자가 가만히 서 있어도 기기 고유의 오차와 신호 노이즈 때문에 측정할 때마다 소수점 끝자리가 계속해서 변한다.&lt;br /&gt;
또한 사용자가 단 1m만 걸어가도 좌표 문자열은 완전히 다른 값으로 치환된다.&lt;/p&gt;

&lt;p&gt;만일 이 미세하게 변하는 위도/경도 문자열을 캐시 키로 잡으면 &lt;strong&gt;단, 0.00001도의 차이만으로도 캐시 미스가 발생&lt;/strong&gt;하여 시스템은 무조건 백엔드 DB를 찌르게 된다.&lt;br /&gt;
정작 5km 반경 내에 존재하는 주변 식당 리스트 결과물은 아까 조회한 내용과 100% 동일해도 말이다.&lt;/p&gt;

&lt;p&gt;결국 캐시 적중률(Hit rate)은 0%에 수렴하게 되고, 무의미하게 변경되는 좌표 키들이 캐시 서버의 RAM 용량을 순식간에 낭비(Memory Bloat)하게 만들므로 좌표를 키로 쓰는 것은 완벽하게 무의미하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;3222-지오해시-정적-격자를-캐시-key-로-채택해결책&quot;&gt;3.2.2.2. 지오해시 정적 격자를 캐시 key 로 채택(해결책)&lt;/h4&gt;

&lt;p&gt;사용자 좌표 대신, 그 좌표가 포함되어 있는 고정된 정적 공간 범위인 &lt;strong&gt;지오해시 값을 캐시 키&lt;/strong&gt;로 삼아야 한다.&lt;br /&gt;
사용자가 격자 내부에서 이리저리 흔들리며 움직이더라도 동일한 지오해시 격자 내에 있다면 항상 같은 캐시 키를 매핑하므로 극적인 캐시 적중률 상승을 이끌어낼 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;3223-캐시-구조-및-메모리-계산-산정&quot;&gt;3.2.2.3. 캐시 구조 및 메모리 계산 산정&lt;/h4&gt;

&lt;p&gt;지오해시나 쿼드트리는 같은 격자 내 모든 사업장이 같은 해시값을 갖도록 만들 수 있기 때문에 이 문제를 효과적으로 해결한다.&lt;/p&gt;

&lt;p&gt;추천하는 캐시 키-값은 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.지오해시 격자 기반 캐시&lt;/strong&gt;&lt;br /&gt;
사업장 정보는 자주 변경되지 않으므로 특정 지오해시에 해당하는 사업장 ID 목록을 미리 계산하여 캐시에 저장한다.&lt;br /&gt;
사업장을 추가/수정/삭제하는 연산의 빈도는 상대적으로 낮아서 Lock을 사용할 필요가 없다.&lt;br /&gt;
주어진 요구사항으로 사용자는 500m, 1km, 2km, 5km 검색 반경 가운데 하나를 고를 수 있고, 이 각 검색 반경은 지오해시 길이 4,5,5,6에 해당한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;key: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;geohash&lt;/code&gt;(정밀도 수준 4,5,6에 해당하는 정적 문자열)&lt;/li&gt;
  &lt;li&gt;value: 해당 격자 내부에 존재하는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List&amp;lt;business_id&amp;gt;&lt;/code&gt;(사업장 ID 배열)&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;지오해시 캐시 레이어의 물리 메모리 요구량 산정(5GB)&lt;/strong&gt;&lt;br /&gt;
총 사업장 수 2억개(200m)에 대해 3가지 정밀도(길이 4,5,6)을 모두 지원하기 때문에 메모리를 수학적으로 증명해보자.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;value의 적재 공간: 8byte(BIGINT ID 크기) * 2억개(사업장 수) * 3(3가지 정밀도) = 약 5GB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;key 문자열(지오해시 코드)이 차지하는 용량은 무시할 수 있는 수준이므로, 전체 필요 메모리는 약 &lt;strong&gt;5GB&lt;/strong&gt; 내외로 수렴한다.&lt;br /&gt;
이 정도는 단 한 대의 사양 좋은 레디스 서버로도 충족되지만, 글로벌 고가용성 보장과 전송 지연(Latency) 방지를 위해 전 세계 주요 거점별로 레디스 클러스터를 배치하여 
동일 데이터를 중복 복제 운용하는 것이 좋다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2.사업장 상세 정보 캐시&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;key: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;business_id&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;value: 사업장 이름, 주소 등 정형 메타데이터 객체(JSON)&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-글로벌-가용성-및-부가-비즈니스-로직-처리&quot;&gt;3.3. 글로벌 가용성 및 부가 비즈니스 로직 처리&lt;/h2&gt;

&lt;h3 id=&quot;331-다중-region-및-가용성-구역-배치&quot;&gt;3.3.1. 다중 Region 및 가용성 구역 배치&lt;/h3&gt;

&lt;p&gt;LBS 시스템을 글로벌 멀티 리전 아키텍처로 해야 하는 3가지 실무적 이유는 아래와 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;물리적 속도 한계 극복&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;네트워크 지연을 줄이기 위해 전 세계 사용자 근처에 Edge 인프라와 서버를 전진 배치&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;인구 밀도별 트래픽 분산&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;서울과 같이 인구 밀집도가 높은 메트로 지역은 별도의 독립 리전으로 격리하거나, 단일 리전 내부에서도 다량의 가용성 구역(AZ, Availability Zone)을 엮어 트래픽 부하를 고르게 찢어 분산&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 사생활 보호법(GDPR/CCPA) 대응&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;일부 국가는 사용자의 개인정보가 자국 영토 국경선 밖의 해외 데이터 센터로 유출되는 것을 법으로 금지함&lt;/li&gt;
      &lt;li&gt;해당 국가 영역 내에 독립 리전을 격리 빌드하고, 지리적 DNS 라우팅(GeoDNS)를 걸어 내수 트래픽이 외부로 나가지 못하게 아키텍처 환경을 강제 격리해야 법적 리스크를 피할 수 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;332-실시간-영업-여부-및-유형별-필터링-기능-구현&quot;&gt;3.3.2. 실시간 영업 여부 및 유형별 필터링 기능 구현&lt;/h3&gt;

&lt;p&gt;“지금 영업 중인 500m 이내 일식집만 필터링해서 보여달라”와 같은 동적 조건 검색 요구사항이 들어왔을 때의 설계 기법이다.&lt;/p&gt;

&lt;p&gt;공간 분할(지오해시/쿼드트리) 색인을 통해 1차적으로 걸러져 나오는 특정 반경 내 사업장의 모수 자체는 수십~수백 개 수준으로 매우 적다.&lt;br /&gt;
따라서 지오해시 테이블이나 캐시 단계에서 영업시간 복잡도를 엮어 복잡한 다차원 인덱스를 설계하려 하지 말고,&lt;br /&gt;
우선 공간 색인을 통해 사업장 ID 목록을 가볍게 1차 확보한 후, 해당 사업장들의 상세 메타 데이터 객체를 캐시에서 뽑아와 애플리케이션 메모리 상에서 실시간 조건식으로 필터링(In-Memory Filtering)하는 것이 
아키텍처 결합도는 낮추고 성능을 끌어올리는 방법이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-최종-아키텍처-다이어그램&quot;&gt;4. 최종 아키텍처 다이어그램&lt;/h1&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
  actor Client as 클라이언트 (사용자 앱)
  participant LB as 로드밸런서 (LB)
  participant LBS as 위치 기반 서비스 (LBS 서버)
  participant Redis1 as 지오해시 캐시 (Redis)
  participant Redis2 as 사업장 캐시 (Redis)
  participant DB as 복제 DB 클러스터 (Replica)

  Client-&amp;gt;&amp;gt;LB: 1. 검색 요청 발송 (위도/경도 + 500m 반경)
  LB-&amp;gt;&amp;gt;LBS: 2. 요청 라우팅
  Note over LBS: 3. 검색 반경을 기준으로&amp;lt;br/&amp;gt;최적 지오해시 길이 계산 (500m = 길이 6)
  Note over LBS: 4. 격자 외곽 처리를 위해&amp;lt;br/&amp;gt;중심 격자 및 인접 격자 8개 계산 (총 9개 목록)

  rect rgb(240, 248, 255)
    Note over LBS, Redis1: 5. 9개 지오해시 키(Key)로 병렬 조회 수행
    LBS-&amp;gt;&amp;gt;Redis1: 지오해시 배열 조회 질의
    alt Cache Hit!
      Redis1--&amp;gt;&amp;gt;LBS: 각 격자에 속한 사업장 ID 목록 반환
    else Cache Miss
      Redis1--&amp;gt;&amp;gt;LBS: 데이터 없음 (Miss)
      LBS-&amp;gt;&amp;gt;DB: DB 복제본에 복합키 기반 지오해시 색인 질의
      DB--&amp;gt;&amp;gt;LBS: 사업장 ID 목록 추출 및 반환
      LBS-&amp;gt;&amp;gt;Redis1: 추출된 ID 데이터 캐시 적재
    end
  end

  rect rgb(245, 245, 220)
    Note over LBS, Redis2: 6. 취합된 총 사업장 ID들로 상세 메타데이터 멀티 오퍼레이션 조회
    LBS-&amp;gt;&amp;gt;Redis2: MGET [business_id_list]
    Redis2--&amp;gt;&amp;gt;LBS: 사업장 상세 객체 데이터(이름, 주소, 영업시간 등) 반환
  end

  Note over LBS: 7. 비즈니스 로직 처리&amp;lt;br/&amp;gt;- 사용자-상점 간 구면 거리 계산 및 정렬&amp;lt;br/&amp;gt;- 현재 영업시간 필터링
  LBS--&amp;gt;&amp;gt;LB: 8. 최종 정렬 및 필터링된 결과 전달
  LB--&amp;gt;&amp;gt;Client: 9. 화면에 최적 장소 목록 반환
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0524/final.png&quot; alt=&quot;최종 아키텍처&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;상세 설계 요약&lt;/strong&gt;&amp;gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;샤딩과 복제&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;정형 데이터인 사업장 정보는 유연한 수평 확장을 위해 &lt;strong&gt;사업장 ID 기반 샤딩&lt;/strong&gt;을 적용하고, 지오해시 공간 색인 테이블은 복잡한 다중 샤드를 피하기 위해 관리가 편한 &lt;strong&gt;Read Replica 클러스터&lt;/strong&gt;를 적극 활용&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;정적 격자 캐싱&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;흔들리는 사용자 GPS raw 좌표를 캐시 키로 잡으면 적중률이 무너짐&lt;/li&gt;
      &lt;li&gt;고정된 공간 격자인 &lt;strong&gt;지오해시 문자열을 캐시 키&lt;/strong&gt;로 삼아 유한한 메모리 공간(약 5GB) 안에서 무한 스케일의 조회를 완벽히 제어&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;가용성 및 법률&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;초저지연 Latency 달성과 특정 밀집 국가의 부하 제어, 그리고 &lt;strong&gt;GDPR과 같은 엄격한 데이터 사생활 국가 보호법&lt;/strong&gt;에 유연하게 대처하기 위해 거점별 다중 리전 격리 배치를 선제적으로 수립&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0524/final2.png&quot; alt=&quot;mind map&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 알렉스 쉬, 산 람 저자의 &lt;strong&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://product.kyobobook.co.kr/detail/S000211656186&quot;&gt;가상 면접 사례로 배우는 대규모 시스템 설계 기초 2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developer.atlassian.com/server/confluence/pagination-in-the-rest-api/&quot;&gt;REST API에서의 페이지 분할&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.google.com/maps/documentation/places/web-service/legacy/search?hl=ko&quot;&gt;구글 장소 API&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.developer.yelp.com/reference/v3_business_search&quot;&gt;옐프 사업장 API&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://redis.io/docs/latest/commands/GEOHASH/&quot;&gt;Redis Geohash&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.movable-type.co.uk/scripts/geohash.html&quot;&gt;지오해시&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/35976444/how-many-leaves-has-a-quadtree&quot;&gt;쿼드트리에는 얼마나 많은 Leaf node가 있는가&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.educative.io/answers/what-is-a-quadtree-how-is-it-used-in-location-based-services&quot;&gt;쿼드트리를 활용한 위치 정보 캐시 개선안&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://s2geometry.io/&quot;&gt;구글 S2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Hilbert_curve&quot;&gt;힐베르트 곡선(Hilbert curve)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Geofence&quot;&gt;지오펜스(Geofence)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://s2.inair.space/&quot;&gt;영역 지정 알고리즘(Region Cover Algorithm)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/alex-xu-system/bytebytego/blob/main/system_design_links_vol2.md&quot;&gt;책에 나온 링크들 모음&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 24 May 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/05/24/architecture-proximity/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/05/24/architecture-proximity/</guid>
        
        <category>architecture</category>
        
        <category>system-design</category>
        
        <category>proximity-service</category>
        
        <category>lbs</category>
        
        <category>geohash</category>
        
        <category>quadtree</category>
        
        <category>redis</category>
        
        <category>database-sharding</category>
        
        <category>대규모시스템설계</category>
        
        <category>근접성서비스</category>
        
        <category>위치기반서비스</category>
        
        <category>지오해시</category>
        
        <category>쿼드트리</category>
        
        <category>레디스</category>
        
        <category>캐싱</category>
        
        <category>아키텍처</category>
        
        <category>시스템디자인</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Kubernetes - 모니터링(Metric Server, Prometheus, Grafana, Loki)</title>
        <description>&lt;p&gt;쿠버네티스 모니터링은 클러스터 내의 노드, 파드, 컨테이너 등 다양한 리소스의 상태와 성능 지표(Metric)를 수집하고, 발생한 로그를 분석하여 시스템의 성능을 지속적으로 
관찰하는 프로세스이다.&lt;br /&gt;
단순한 상태 확인을 넘어 &lt;strong&gt;가시성(Observability)&lt;/strong&gt;을 확보하여 장애를 사전에 예방하고 효율적인 리소스 관리를 가능하게 하는 핵심 운영 요소이다.&lt;/p&gt;

&lt;p&gt;쿠버네티스 클러스터의 규모가 확장됨에 따라, 수십에서 수백 개에 이르는 노드와 그 위에서 동작하는 수많은 파드를 개별적으로 관리하는 것은 사실상 불가능에 가깝다.&lt;br /&gt;
시스템의 복잡도가 증가할수록 클러스터 전체의 현황을 한 눈에 파악하고, 특정 지점에서 발생하는 병목이나 오류를 신속하게 탐지할 수 있는 통합된 모니터링 방법론이 필수적이다.&lt;/p&gt;

&lt;p&gt;이번 포스트에서는 쿠버네티스 운영 환경에서 가장 널리 사용되는 표준 모니터링 스택을 구축하는 과정을 단계별로 다룬다.&lt;br /&gt;
리소스의 기초적인 사용량을 확인하는 단계부터, 데이터를 수집하고 시각화하며, 로그를 중앙에서 통합 관리하는 방법까지, &lt;strong&gt;관측 가능한(Observable)&lt;/strong&gt; 쿠버네티스 
환경을 만드는 방법에 대해 알아본다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;메트릭 서버(Metric-Server)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl top&lt;/code&gt; 명령어 등을 통해 노드와 파드의 실시간 CPU 및 메모리 사용량을 확인하는 기초적인 방법&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;프로메테우스(Prometheus)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시계열 데이터 기반으로 클러스터의 방대한 모니터링 메트릭을 수집하고 저장하는 방법&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;그라파나(Grafana)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;수집된 데이터를 직관적인 대시보드로 시각화하여 운영 효율성을 높이는 방법&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;로키(Loki)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;여러 노드에 분산된 파드의 로그를 중앙에서 효율적으로 수집하고 조회하는(PLG Stack) 방법&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-메트릭-서버metric-server를-통한-리소스-확인&quot;&gt;1. 메트릭 서버(Metric-Server)를 통한 리소스 확인&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-메트릭-서버-설치&quot;&gt;1.1. 메트릭 서버 설치&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#111-헬름helm-차트-준비&quot;&gt;1.1.1. 헬름(Helm) 차트 준비&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#112-valuesyaml-수정설정-커스터마이징&quot;&gt;1.1.2. values.yaml 수정(설정 커스터마이징)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#113-설치-및-확인&quot;&gt;1.1.3. 설치 및 확인&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#12-메트릭-서버를-통한-리소스-사용량-확인&quot;&gt;1.2. 메트릭 서버를 통한 리소스 사용량 확인&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#121-노드-및-파드-사용량-조회&quot;&gt;1.2.1. 노드 및 파드 사용량 조회&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#122-단위unit-상세-해석과-전체-용량-확인-방법&quot;&gt;1.2.2. 단위(Unit) 상세 해석과 전체 용량 확인 방법&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-프로메테우스prometheus를-통한-모니터링-데이터-수집&quot;&gt;2. 프로메테우스(Prometheus)를 통한 모니터링 데이터 수집&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-프로메테우스-개념&quot;&gt;2.1. 프로메테우스 개념&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-프로메테우스와-그라파나-설치&quot;&gt;2.2. 프로메테우스와 그라파나 설치&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#221-헬름-차트-준비&quot;&gt;2.2.1. 헬름 차트 준비&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#222-valuesyaml-수정프로메테우스-설정&quot;&gt;2.2.2. values.yaml 수정(프로메테우스 설정)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#223-valuesyaml-수정그라파나-설정&quot;&gt;2.2.3. values.yaml 수정(그라파나 설정)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#224-설치-진행&quot;&gt;2.2.4. 설치 진행&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-프로메테우스를-통한-데이터-확인&quot;&gt;2.3. 프로메테우스를 통한 데이터 확인&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#231-node-exporter-확인&quot;&gt;2.3.1. Node Exporter 확인&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#232-프로메테우스-웹-ui-접속&quot;&gt;2.3.2. 프로메테우스 웹 UI 접속&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-그라파나grafana를-통한-모니터링-데이터-시각화&quot;&gt;3. 그라파나(Grafana)를 통한 모니터링 데이터 시각화&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-그라파나-서비스-타입-변경loadbalancer&quot;&gt;3.1. 그라파나 서비스 타입 변경(LoadBalancer)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#311-서비스-패치patch&quot;&gt;3.1.1. 서비스 패치(Patch)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#312-접속을-위한-포트포워딩-설정&quot;&gt;3.1.2. 접속을 위한 포트포워딩 설정&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-그라파나-접속-및-초기-설정&quot;&gt;3.2. 그라파나 접속 및 초기 설정&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#321-관리자-비밀번호-확인&quot;&gt;3.2.1. 관리자 비밀번호 확인&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#322-로그인-및-기본-대시보드-확인&quot;&gt;3.2.2. 로그인 및 기본 대시보드 확인&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#33-외부-대시보드-임포트&quot;&gt;3.3. 외부 대시보드 임포트&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-로키loki를-활용한-쿠버네티스-로그-확인&quot;&gt;4. 로키(Loki)를 활용한 쿠버네티스 로그 확인&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-로키-개념과-plgpromtaillokigrafana-개념&quot;&gt;4.1. 로키 개념과 PLG(Promtail+Loki+Grafana) 개념&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#42-로키-설치loki-stack&quot;&gt;4.2. 로키 설치(Loki Stack)&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#421-헬름-차트-준비&quot;&gt;4.2.1. 헬름 차트 준비&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#422-valuesyaml-수정&quot;&gt;4.2.2. values.yaml 수정&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#423-설치-및-확인&quot;&gt;4.2.3. 설치 및 확인&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#43-로그-확인테스트-앱-배포&quot;&gt;4.3. 로그 확인(테스트 앱 배포)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#44-그라파나와-로키-연동-및-로그-조회&quot;&gt;4.4. 그라파나와 로키 연동 및 로그 조회&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#441-데이터-소스data-source-추가&quot;&gt;4.4.1. 데이터 소스(Data Source) 추가&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#442-연결-정보-설정&quot;&gt;4.4.2. 연결 정보 설정&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#443-로그-쿼리logql&quot;&gt;4.4.3. 로그 쿼리(LogQL)&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#정리하며&quot;&gt;정리하며..&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;개발 환경&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Guest OS: Ubuntu 24.04.2 LTS&lt;/li&gt;
  &lt;li&gt;Host OS: Mac Apple M3 Max&lt;/li&gt;
  &lt;li&gt;Memory: 48 GB&lt;/li&gt;
  &lt;li&gt;Kubernetes: v1.29.15&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-메트릭-서버metric-server를-통한-리소스-확인&quot;&gt;1. 메트릭 서버(Metric-Server)를 통한 리소스 확인&lt;/h1&gt;

&lt;p&gt;쿠버네티스는 파드의 부하량에 따라 자동으로 파드 수를 조절해주는 &lt;strong&gt;Horizontal Pod Autoscaler(HPA)&lt;/strong&gt;가 존재한다.&lt;br /&gt;
그렇다면 HPA는 무엇을 근거로 스케일 업/다운을 결정할까?&lt;/p&gt;

&lt;p&gt;이를 위해서는 현재 시스템의 리소스 상태를 파악해야 하는데, 이를 가능하게 해주는 것이 바로 &lt;strong&gt;메트릭 서버(Metric-Server)&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;메트릭 서버는 쿠버네티스 클러스터 내의 노드와 파드로부터 CPU, 메모리 사용량 등의 메트릭(성능 지표)을 수집하여 API 서버를 통해 제공하는 핵심 애드온이다.&lt;br /&gt;
이 데이터는 HPA 뿐만 아니라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl top&lt;/code&gt; 명령어를 통해 운영자가 리소스 현황을 파악하는 데에도 사용된다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;애드온(Add-on)&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;기본 기능에 덧붙여 설치하는 확장 프로그램&lt;br /&gt;
대표적인 쿠버네티스 애드온은 아래가 있다.&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;strong&gt;네트워크 플러그인(CNI)&lt;/strong&gt;
      &lt;ul&gt;
        &lt;li&gt;파드끼리 통신할 수 있게 해줌(필수)&lt;/li&gt;
        &lt;li&gt;예) Calico, Flannel&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;DNS 서버(CoreDNS)&lt;/strong&gt;
      &lt;ul&gt;
        &lt;li&gt;IP 주소 대신 도메인 이름으로 서비스를 찾게 해줌(필수)&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;대시보드&lt;/strong&gt;
      &lt;ul&gt;
        &lt;li&gt;명령어가 아닌 웹 UI 화면으로 클러스터를 관리하게 해줌(선택)&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;로깅&lt;/strong&gt;
      &lt;ul&gt;
        &lt;li&gt;컨테이너의 로그를 수집하고 저장함(선택)&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;모니터링&lt;/strong&gt;
      &lt;ul&gt;
        &lt;li&gt;메트릭 서버나 프로메테우스 등(거의 필수)&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-메트릭-서버-설치&quot;&gt;1.1. 메트릭 서버 설치&lt;/h2&gt;

&lt;p&gt;먼저 설치를 진행할 작업 디렉터리를 생성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;work/app
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;argocd  helm  metallb  nginx-ingress-controller
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;metric-server
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;metric-server/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;111-헬름helm-차트-준비&quot;&gt;1.1.1. 헬름(Helm) 차트 준비&lt;/h3&gt;

&lt;p&gt;헬름 리포지토리에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metrics-server&lt;/code&gt;를 추가하고 최신 정보를 업데이트한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metric-server&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server
&lt;span class=&quot;s2&quot;&gt;&quot;metrics-server&quot;&lt;/span&gt; has been added to your repositories

assu@myserver01:~/work/app/metric-server&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo update
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;헬름 리포지토리에서 설치 가능한 버전을 확인하고 차트를 다운로드한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metric-server&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm search repo metric
NAME                                        	CHART VERSION	APP VERSION	DESCRIPTION
...
metrics-server/metrics-server               	3.13.0       	0.8.0      	Metrics Server is a scalable, efficient &lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; ...
...

&lt;span class=&quot;c&quot;&gt;# 차트 다운로드 및 압축 해제&lt;/span&gt;
assu@myserver01:~/work/app/metric-server&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm pull metrics-server/metrics-server
assu@myserver01:~/work/app/metric-server&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;metrics-server-3.13.0.tgz

assu@myserver01:~/work/app/metric-server&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tar &lt;/span&gt;xvfz metrics-server-3.13.0.tgz
metrics-server/Chart.yaml
...
assu@myserver01:~/work/app/metric-server&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;metrics-server  metrics-server-3.13.0.tgz

assu@myserver01:~/work/app/metric-server&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mv &lt;/span&gt;metrics-server metrics-server-3.13.0
assu@myserver01:~/work/app/metric-server&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;metrics-server-3.13.0  metrics-server-3.13.0.tgz
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;112-valuesyaml-수정설정-커스터마이징&quot;&gt;1.1.2. values.yaml 수정(설정 커스터마이징)&lt;/h3&gt;

&lt;p&gt;기본 설정 파일인 values.yaml 을 복사하여 사용자 정의 설정 파일인 my-values.yaml 을 생성하고, 환경에 맞게 인자를 수정한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metric-server&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;metrics-server-3.13.0/

assu@myserver01:~/work/app/metric-server/metrics-server-3.13.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;CHANGELOG.md  Chart.yaml  ci  README.md  RELEASE.md  templates  values.yaml

assu@myserver01:~/work/app/metric-server/metrics-server-3.13.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp &lt;/span&gt;values.yaml my-values.yaml
assu@myserver01:~/work/app/metric-server/metrics-server-3.13.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;CHANGELOG.md  Chart.yaml  ci  my-values.yaml  README.md  RELEASE.md  templates  values.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;defaultArgs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;--cert-dir=/tmp&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;--kubelet-use-node-status-port&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;--metric-resolution=15s&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;--kubelet-insecure-tls&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 추가&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;--kubelet-preferred-address-types=InternalIP&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 추가&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--kubelet-insecure-tls&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;메트릭 서버가 &lt;a href=&quot;https://assu10.github.io/dev/2025/11/30/kubernetes-basic-concept-architecture/#23-%EB%85%B8%EB%93%9Cnode&quot;&gt;kubelet&lt;/a&gt;에 접근할 때 TLS 인증서 검증을 건너뛰도록 설정&lt;/li&gt;
      &lt;li&gt;테스트 환경이나 사설 인증서(self-signed certificate)를 사용하는 환경에서 인증서 오류를 방지하기 위해 사용됨&lt;/li&gt;
      &lt;li&gt;프로덕션 환경에서는 보안상 주의가 필요함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--kubelet-preferred-address-types=InternalIP&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;노드 간 통신 시 호스트 이름이나 외부 IP 대신 &lt;strong&gt;내부 IP(InternalIP)&lt;/strong&gt;를 우선적으로 사용하도록 강제함&lt;/li&gt;
      &lt;li&gt;이는 DNS 설정이 완벽하지 않거나 내부 네트워크 통신만 허용된 환경에서 연결 문제를 해결해 줌&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;113-설치-및-확인&quot;&gt;1.1.3. 설치 및 확인&lt;/h3&gt;

&lt;p&gt;작성한 my-values.yaml 파일을 적용하여 kube-system 네임 스페이스에 메트릭 서버를 설치한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 아직은 메트릭 서버와 관련된 오브젝트가 없음&lt;/span&gt;
assu@myserver01:~/work/app/metric-server/metrics-server-3.13.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; kube-system
NAME                                     READY   STATUS    RESTARTS      AGE
pod/coredns-76f75df574-lpvsm             1/1     Running   1 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;19d ago&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   20d
pod/coredns-76f75df574-m2mtp             1/1     Running   1 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;19d ago&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   20d
pod/etcd-myserver01                      1/1     Running   1 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;19d ago&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   20d
pod/kube-apiserver-myserver01            1/1     Running   1 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;19d ago&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   20d
pod/kube-controller-manager-myserver01   1/1     Running   1 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;19d ago&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   20d
pod/kube-proxy-dlhdm                     1/1     Running   1 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;19d ago&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   20d
pod/kube-proxy-jz4j8                     1/1     Running   1 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;19d ago&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   20d
pod/kube-proxy-rjltj                     1/1     Running   1 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;19d ago&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   20d
pod/kube-scheduler-myserver01            1/1     Running   1 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;19d ago&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   20d

NAME               TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                  AGE
service/kube-dns   ClusterIP   10.96.0.10   &amp;lt;none&amp;gt;        53/UDP,53/TCP,9153/TCP   20d

NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/kube-proxy   3         3         3       3            3           kubernetes.io/os&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux   20d

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coredns   2/2     2            2           20d

NAME                                 DESIRED   CURRENT   READY   AGE
replicaset.apps/coredns-76f75df574   2         2         2       20d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metric-server/metrics-server-3.13.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; kube-system &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;--generate-name&lt;/span&gt; metrics-server/metrics-server &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; my-values.yaml

NAME: metrics-server-1767493116
LAST DEPLOYED: Sun Jan  4 02:18:37 2026
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
DESCRIPTION: Install &lt;span class=&quot;nb&quot;&gt;complete
&lt;/span&gt;TEST SUITE: None
NOTES:
&lt;span class=&quot;k&quot;&gt;***********************************************************************&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; Metrics Server                                                      &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;***********************************************************************&lt;/span&gt;
  Chart version: 3.13.0
  App version:   0.8.0
  Image tag:     registry.k8s.io/metrics-server/metrics-server:v0.8.0
&lt;span class=&quot;k&quot;&gt;***********************************************************************&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;설치가 완료되면 kube-system 네임스페이스의 리소스를 확인하여 파드가 정상적으로 실행 중인지 확인한다.&lt;/p&gt;

&lt;p&gt;이제 다시 kube-system 네임스페이스의 오브젝트들을 확인해보자.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metric-server/metrics-server-3.13.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; kube-system
NAME                                             READY   STATUS    RESTARTS      AGE
...
pod/metrics-server-1767493116-59cc4c4cb4-sv49h   0/1     Running   0             27s  &lt;span class=&quot;c&quot;&gt;# 추가&lt;/span&gt;

NAME                                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                  AGE
...
service/metrics-server-1767493116   ClusterIP   10.102.162.208   &amp;lt;none&amp;gt;        443/TCP                  27s  &lt;span class=&quot;c&quot;&gt;# 추가&lt;/span&gt;

...

NAME                                        READY   UP-TO-DATE   AVAILABLE   AGE
...
deployment.apps/metrics-server-1767493116   0/1     1            0           27s  &lt;span class=&quot;c&quot;&gt;# 추가&lt;/span&gt;

NAME                                                   DESIRED   CURRENT   READY   AGE
...
replicaset.apps/metrics-server-1767493116-59cc4c4cb4   1         1         0       27s  &lt;span class=&quot;c&quot;&gt;# 추가&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;metrics-server와 관련된 오브젝트(파드, 서비스, 디플로이먼트, 레플리카셋)가 추가된 것을 확인할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;12-메트릭-서버를-통한-리소스-사용량-확인&quot;&gt;1.2. 메트릭 서버를 통한 리소스 사용량 확인&lt;/h2&gt;

&lt;p&gt;메트릭 서버 설치 후 1~2분 정도 후 수집된 데이터를 바탕으로 노드와 파드의 리소스 사용량을 조회할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;121-노드-및-파드-사용량-조회&quot;&gt;1.2.1. 노드 및 파드 사용량 조회&lt;/h3&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 노드의 CPU, 메모리 사용량 확인&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl top nodes
NAME         CPU&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;cores&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   CPU%   MEMORY&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;bytes&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   MEMORY%
myserver01   140m         3%     2637Mi          33%
myserver02   51m          1%     1720Mi          22%
myserver03   46m          1%     1817Mi          23%

&lt;span class=&quot;c&quot;&gt;# 파드 사용량 확인&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl top pod &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; kube-system
NAME                                         CPU&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;cores&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   MEMORY&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;bytes&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
coredns-76f75df574-lpvsm                     3m           15Mi
metrics-server-1767493116-59cc4c4cb4-sv49h   5m           20Mi
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;122-단위unit-상세-해석과-전체-용량-확인-방법&quot;&gt;1.2.2. 단위(Unit) 상세 해석과 전체 용량 확인 방법&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl top&lt;/code&gt; 명령어를 처음 접하면 140m, 2637Mi 와 같은 단위가 생소할 수 있다.&lt;br /&gt;
정확한 모니터링을 위해 이 단위들이 의미하는 바를 명확히 이해해야 한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) CPU 단위: m(millicores)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;오해&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;140m은 140MB가 아니다. CPU는 저장 공간이 아니므로 바이트 단위를 사용하지 않는다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;정의&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&lt;/code&gt;은 &lt;strong&gt;밀리코어(millicores)&lt;/strong&gt;를 의미한다.
        &lt;ul&gt;
          &lt;li&gt;1000m = 1 Core(vCPU)&lt;/li&gt;
          &lt;li&gt;즉, 140m은 &lt;strong&gt;0.14 Core&lt;/strong&gt;를 사용 중이라는 뜻이다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;파드 예시&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;coredns&lt;/code&gt; 파드가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3m&lt;/code&gt;을 사용한다는 것은 0.003 Core 만큼의 CPU 연산을 수행 중이라는 의미이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2) Memory 단위: Mi(Mebibytes)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;정의&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Mi&lt;/code&gt;는 &lt;strong&gt;메비바이트(Mebibytes)&lt;/strong&gt;를 의미한다.&lt;/li&gt;
      &lt;li&gt;우리가 흔히 사용하는 MB(Megabytes)는 \(10^{6}(1,000,000)\) 바이트 기준이다.&lt;/li&gt;
      &lt;li&gt;Mi는 \(2^{20}(1,048,576)\) 바이트 기준이다.&lt;/li&gt;
      &lt;li&gt;대략적으로 &lt;strong&gt;1 Mi ≈ 1.048 MB&lt;/strong&gt; 이므로, &lt;strong&gt;2637Mi&lt;/strong&gt;는 약 2,765MB 정도의 메모리를 사용 중임을 나타낸다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;3) 전체 용량(Capacity) 및 사용률(%) 계산 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl top nodes&lt;/code&gt;에서 보이는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CPU%&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MEMORY%&lt;/code&gt;는 해당 노드의 &lt;strong&gt;Allocatable(할당 가능) 리소스&lt;/strong&gt; 대비 현재 사용량을 나타낸다.&lt;/p&gt;

&lt;p&gt;전체 용량(Capacity)과 할당 가능 용량(Allocatable)을 정확히 확인하려면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl describe node&lt;/code&gt; 명령어를 사용한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 특정 노드의 상세 정보 확인&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl describe node myserver01
...
Capacity:
  cpu:                4             &lt;span class=&quot;c&quot;&gt;# 노드의 물리적 전체 CPU 코어 수&lt;/span&gt;
  ephemeral-storage:  61202244Ki
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             8086200Ki     &lt;span class=&quot;c&quot;&gt;# 노드의 물리적 전체 메모리&lt;/span&gt;
  pods:               110
Allocatable:
  cpu:                4             &lt;span class=&quot;c&quot;&gt;# 파드에 할당 가능한 CPU 코어 수 (시스템 예약분 제외)&lt;/span&gt;
  ephemeral-storage:  56391747653
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             7983800Ki     &lt;span class=&quot;c&quot;&gt;# 파드에 할당 가능한 메모리&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl top nodes&lt;/code&gt; 결과에서 myserver01의 CPU 사용량은 140m이고, 3%라고 출력되었었다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Allocatable CPU&lt;/strong&gt;: 4 Core = &lt;strong&gt;4000m&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Current Usage&lt;/strong&gt;: &lt;strong&gt;140m&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;계산: (140/4000) * 100 = 3.5% 인데 이를 반올림하여 약 3% 사용 중이다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 전체 리소스 중 얼마를 쓰고 있는지 정확한 수치를 보고 싶다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl describe node&lt;/code&gt;를 통해 분모(전체 용량)를 확인하고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl top&lt;/code&gt;을 통해 
분자(현재 사용량)를 확인하면 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-프로메테우스prometheus를-통한-모니터링-데이터-수집&quot;&gt;2. 프로메테우스(Prometheus)를 통한 모니터링 데이터 수집&lt;/h1&gt;

&lt;p&gt;앞서 메트릭 서버를 통해 실시간 리소스 현황을 확인했다면, 이제는 시계열 데이터를 저장하고 분석할 수 있는 &lt;strong&gt;프로메테우스&lt;/strong&gt;를 구축해본다.&lt;/p&gt;

&lt;p&gt;프로메테우스의 개념을 이해하고, 헬름을 사용하여 프로메테우스와 그라파나가 포함된 스택을 설치한 후, 실제로 데이터가 수집되는지 확인한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-프로메테우스-개념&quot;&gt;2.1. 프로메테우스 개념&lt;/h2&gt;

&lt;p&gt;모니터링 시스템의 핵심은 &lt;strong&gt;메트릭&lt;/strong&gt;이다.&lt;br /&gt;
메트릭이란 웹 서버 요청 횟수, CPU, 메모리 사용량 등 시스템 성능과 상태를 숫자로 나타낸 지표를 의미한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;프로메테우스&lt;/strong&gt;는 이러한 메트릭을 수집하는 도구로, 쿠버네티스 생태계에서 사실상의 표준으로 자리잡았다.&lt;br /&gt;
프로메테우스는 쿠버네티스 노드, 파드, 그리고 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/20/kubernetes-volume-basic-emptydir-hostpath-pv/#4-pvpersistentvolume&quot;&gt;Persistent Volume&lt;/a&gt; 등에서 
데이터를 주기적으로 긁어오는(pull) 구조를 가진다.&lt;/p&gt;

&lt;p&gt;여기서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-prometheus-stack&lt;/code&gt; 이라는 헬름 차트를 사용한다.&lt;br /&gt;
이 차트를 설치하면 단순히 프로메테우스만 설치되는 것이 아니라, 모니터링에 필요한 세트가 함께 설치된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Prometheus&lt;/strong&gt;: 메트릭 수집 및 저장&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Grafana&lt;/strong&gt;: 수집된 메트릭 시각화&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Alertmanager&lt;/strong&gt;: 경고 메시지 전송 관리&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Node Exporter&lt;/strong&gt;: 하드웨어 및 OS 레벨의 메트릭 노출&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-프로메테우스와-그라파나-설치&quot;&gt;2.2. 프로메테우스와 그라파나 설치&lt;/h2&gt;

&lt;p&gt;먼저 설치 작업을 진행할 디렉터리를 생성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;work/app
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;argocd  helm  metallb  metric-server  nginx-ingress-controller
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;prometheus
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;prometheus/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;221-헬름-차트-준비&quot;&gt;2.2.1. 헬름 차트 준비&lt;/h3&gt;

&lt;p&gt;프로메테우스 커뮤니티 리포지토리를 추가하고 차트를 다운로드한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
&lt;span class=&quot;s2&quot;&gt;&quot;prometheus-community&quot;&lt;/span&gt; has been added to your repositories

&lt;span class=&quot;c&quot;&gt;# 헬름 업데이트&lt;/span&gt;
assu@myserver01:~/work/app/prometheus&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo update

&lt;span class=&quot;c&quot;&gt;# 차트 검색&lt;/span&gt;
assu@myserver01:~/work/app/prometheus&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm search repo prometheus
NAME                                              	CHART VERSION	APP VERSION	DESCRIPTION
...
prometheus-community/kube-prometheus-stack        	80.10.0      	v0.87.1    	kube-prometheus-stack collects Kubernetes manif...

&lt;span class=&quot;c&quot;&gt;# 다운로드&lt;/span&gt;
assu@myserver01:~/work/app/prometheus&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm pull prometheus-community/kube-prometheus-stack

assu@myserver01:~/work/app/prometheus&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;kube-prometheus-stack-80.10.0.tgz

assu@myserver01:~/work/app/prometheus&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tar &lt;/span&gt;xvfz kube-prometheus-stack-80.10.0.tgz

assu@myserver01:~/work/app/prometheus&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;kube-prometheus-stack  kube-prometheus-stack-80.10.0.tgz

assu@myserver01:~/work/app/prometheus&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mv &lt;/span&gt;kube-prometheus-stack kube-prometheus-stack-80.10.0

assu@myserver01:~/work/app/prometheus&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;kube-prometheus-stack-80.10.0  kube-prometheus-stack-80.10.0.tgz

assu@myserver01:~/work/app/prometheus&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;kube-prometheus-stack-80.10.0/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;222-valuesyaml-수정프로메테우스-설정&quot;&gt;2.2.2. values.yaml 수정(프로메테우스 설정)&lt;/h3&gt;

&lt;p&gt;기본 설정 파일을 복사하여 my-values.yaml 을 만들고, 운영 환경에 맞게 수정한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Chart.lock  charts  Chart.yaml  README.md  templates  values.yaml

assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp &lt;/span&gt;values.yaml my-values.yaml
assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Chart.lock  charts  Chart.yaml  my-values.yaml  README.md  templates  values.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim my-values.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;주요 수정 사항 및 설명&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1) 서비스 타입 변경(NodePort)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prometheus.service.type&lt;/code&gt; 수정&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;service:
  ...
  nodePort: 30090
  &lt;span class=&quot;nb&quot;&gt;type&lt;/span&gt;: NodePort &lt;span class=&quot;c&quot;&gt;# 수정 (기존 ClusterIP)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;기본적으로 프로메테우스 서비스는 클러스터 내부에서만 접근 가능한 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/08/kubernetes-service-concept-and-types/#2-clusterip&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterIP&lt;/code&gt;&lt;/a&gt; 타입이다.&lt;br /&gt;
이를 외부(호스트 머신) 등에서 직접 웹 UI에 접속할 수 있도록 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/08/kubernetes-service-concept-and-types/#3-nodeport&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodePort&lt;/code&gt;&lt;/a&gt; 타입으로 변경하고, 30090 포트를 고정으로 할당하였다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2) Service Monitor Selector 설정&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;serviceMonitorSelectorNilUsesHelmValues&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 수정 (기존 true)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;프로메테우스는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServiceMonitor&lt;/code&gt; 라는 리소스를 통해 어떤 파드를 모니터링할지 결정한다.&lt;br /&gt;
이 값이 true 이면 헬름 차트가 관리하는 ServiceMonitor만 수집한다.&lt;br /&gt;
이를 &lt;strong&gt;false로 설정&lt;/strong&gt;해야 나중에 우리가 직접 배포할 애플리케이션의 ServiceMonitor도 프로메테우스가 감지하여 데이터를 수집할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;3) 데이터 보관 주기 및 용량 설정&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 데이터 유지기간 10일(기본값)&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;retention&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;10d&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;retentionSize&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;1GiB&quot;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 수정 (기존: &quot;&quot;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;수집한 메트릭 데이터를 10일간 보관하고, 저장된 데이터 용량이 1GB를 초과하면 보관기간(10일)이 지나지 않았더라도 가장 오래된 데이터부터 삭제한다.&lt;br /&gt;
디스크 용량 부족을 방지하기 위한 안전 장치이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;223-valuesyaml-수정그라파나-설정&quot;&gt;2.2.3. values.yaml 수정(그라파나 설정)&lt;/h3&gt;

&lt;p&gt;프로메테우스 뿐 아니라 그라파나의 서비스 타입도 변경해야 한다.&lt;br /&gt;
그라파나 차트는 charts/grafana 경로에 위치한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Chart.lock  charts  Chart.yaml  my-values.yaml  README.md  templates  values.yaml

assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;charts/

assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0/charts&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;crds  grafana  kube-state-metrics  prometheus-node-exporter  prometheus-windows-exporter

assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0/charts&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;grafana/

assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0/charts/grafana&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Chart.yaml  ci  dashboards  README.md  templates  values.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0/charts/grafana&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim values.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 수정 후&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;NodePort&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 수정 (기존 ClusterIP)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;224-설치-진행&quot;&gt;2.2.4. 설치 진행&lt;/h3&gt;

&lt;p&gt;모니터링 전용 네임스페이스 mymonitoring 을 생성하고 스택을 배포한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl create namespace mymonitoring
namespace/mymonitoring created

assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get namespace
NAME               STATUS   AGE
...
mymonitoring       Active   11s
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이제 프로메테우스를 설치한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mymonitoring &lt;span class=&quot;nt&quot;&gt;--generate-name&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
prometheus-community/kube-prometheus-stack &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; my-values.yaml

NAME: kube-prometheus-stack-1767502407
LAST DEPLOYED: Sun Jan  4 04:53:30 2026
NAMESPACE: mymonitoring
STATUS: deployed
REVISION: 1
DESCRIPTION: Install &lt;span class=&quot;nb&quot;&gt;complete
&lt;/span&gt;NOTES:
kube-prometheus-stack has been installed. Check its status by running:
  kubectl &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymonitoring get pods &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;release=kube-prometheus-stack-1767502407&quot;&lt;/span&gt;

Get Grafana &lt;span class=&quot;s1&quot;&gt;&apos;admin&apos;&lt;/span&gt; user password by running:

  kubectl &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymonitoring get secrets kube-prometheus-stack-1767502407-grafana &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;{.data.admin-password}&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;base64&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo

&lt;/span&gt;Access Grafana &lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;instance:

  &lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;POD_NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;kubectl &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymonitoring get pod &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;app.kubernetes.io/name=grafana,app.kubernetes.io/instance=kube-prometheus-stack-1767502407&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-oname&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
  kubectl &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymonitoring port-forward &lt;span class=&quot;nv&quot;&gt;$POD_NAME&lt;/span&gt; 3000

Get your grafana admin user password by running:

  kubectl get secret &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymonitoring &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; app.kubernetes.io/component&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;admin-secret &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;{.items[0].data.admin-password}&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;base64&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--decode&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;echo


&lt;/span&gt;Visit https://github.com/prometheus-operator/kube-prometheus &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;instructions on how to create &amp;amp; configure Alertmanager and Prometheus instances using the Operator.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-프로메테우스를-통한-데이터-확인&quot;&gt;2.3. 프로메테우스를 통한 데이터 확인&lt;/h2&gt;

&lt;p&gt;설치가 완료되면 파드와 서비스 상태를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mymonitoring
NAME                                                                  READY   STATUS    RESTARTS   AGE
pod/alertmanager-kube-prometheus-stack-1767-alertmanager-0            2/2     Running   0          16s
pod/kube-prometheus-stack-1767-operator-579c9cd49f-l7hs2              1/1     Running   0          21s
pod/kube-prometheus-stack-1767503046-grafana-655c88c77b-54gc4         3/3     Running   0          21s
pod/kube-prometheus-stack-1767503046-kube-state-metrics-67c945ndvpq   1/1     Running   0          21s
pod/kube-prometheus-stack-1767503046-prometheus-node-exporter-2k4mw   1/1     Running   0          21s
pod/kube-prometheus-stack-1767503046-prometheus-node-exporter-9bv2l   1/1     Running   0          21s
pod/kube-prometheus-stack-1767503046-prometheus-node-exporter-j2dw8   1/1     Running   0          21s
pod/prometheus-kube-prometheus-stack-1767-prometheus-0                2/2     Running   0          16s

NAME                                                                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                         AGE
service/alertmanager-operated                                       ClusterIP   None             &amp;lt;none&amp;gt;        9093/TCP,9094/TCP,9094/UDP      16s
service/kube-prometheus-stack-1767-alertmanager                     ClusterIP   10.111.7.79      &amp;lt;none&amp;gt;        9093/TCP,8080/TCP               22s
service/kube-prometheus-stack-1767-operator                         ClusterIP   10.106.8.158     &amp;lt;none&amp;gt;        443/TCP                         22s
service/kube-prometheus-stack-1767-prometheus                       NodePort    10.106.34.63     &amp;lt;none&amp;gt;        9090:30090/TCP,8080:32011/TCP   22s
service/kube-prometheus-stack-1767503046-grafana                    ClusterIP   10.101.160.47    &amp;lt;none&amp;gt;        80/TCP                          22s
service/kube-prometheus-stack-1767503046-kube-state-metrics         ClusterIP   10.108.142.71    &amp;lt;none&amp;gt;        8080/TCP                        22s
&lt;span class=&quot;c&quot;&gt;# node-exporter가 9100 포트를 사용하고 있음&lt;/span&gt;
service/kube-prometheus-stack-1767503046-prometheus-node-exporter   ClusterIP   10.102.144.228   &amp;lt;none&amp;gt;        9100/TCP                        22s
service/prometheus-operated                                         ClusterIP   None             &amp;lt;none&amp;gt;        9090/TCP                        16s

NAME                                                                       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/kube-prometheus-stack-1767503046-prometheus-node-exporter   3         3         3       3            3           kubernetes.io/os&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux   21s

NAME                                                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/kube-prometheus-stack-1767-operator                   1/1     1            1           21s
deployment.apps/kube-prometheus-stack-1767503046-grafana              1/1     1            1           21s
deployment.apps/kube-prometheus-stack-1767503046-kube-state-metrics   1/1     1            1           21s

NAME                                                                             DESIRED   CURRENT   READY   AGE
replicaset.apps/kube-prometheus-stack-1767-operator-579c9cd49f                   1         1         1       21s
replicaset.apps/kube-prometheus-stack-1767503046-grafana-655c88c77b              1         1         1       21s
replicaset.apps/kube-prometheus-stack-1767503046-kube-state-metrics-67c9457ccf   1         1         1       21s

NAME                                                                    READY   AGE
statefulset.apps/alertmanager-kube-prometheus-stack-1767-alertmanager   1/1     16s
statefulset.apps/prometheus-kube-prometheus-stack-1767-prometheus       1/1     16s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;231-node-exporter-확인&quot;&gt;2.3.1. Node Exporter 확인&lt;/h3&gt;

&lt;p&gt;목록을 보면 prometheus-node-exporter 라는 서비스와 데몬셋(DaemonSet)이 보인다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node Exporter&lt;/strong&gt;&lt;br /&gt;
쿠버네티스 노드(서버) 자체는 기본적으로 자신의 CPU, 메모리, 디스크 I/O, 네트워크 트래픽 등의 하드웨어 정보를 프로메테우스가 이해할 수 있는 메트릭 형태로 제공하지 않는다.&lt;br /&gt;
Node Exporter는 모든 노드에 하나씩 설치(DaemonSet)되어, 노드의 OS 레벨 메트릭을 수집하고, 이를 /metrics 엔드포인트로 노출하여 프로메테우스가 가져갈 수 있도록 
변환해주는 역할을 한다.&lt;/p&gt;

&lt;p&gt;Node Exporter가 정상 동작하는지 확인하기 위해 포트포워딩을 설정한다.&lt;br /&gt;
(로컬 호스트의 8080 포트를 9100 포트로 포트포워딩)&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl port-forward &lt;span class=&quot;nt&quot;&gt;--address&lt;/span&gt; 0.0.0.0 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
service/kube-prometheus-stack-1767503046-prometheus-node-exporter 8080:9100 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymonitoring

Forwarding from 0.0.0.0:8080 -&amp;gt; 9100
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;232-프로메테우스-웹-ui-접속&quot;&gt;2.3.2. 프로메테우스 웹 UI 접속&lt;/h3&gt;

&lt;p&gt;프로메테우스 대시보드에 접속하기 위해 위의 서비스 정보를 다시 확인해보자.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;service/kube-prometheus-stack-1767-prometheus                       NodePort    10.106.34.63     &amp;lt;none&amp;gt;        9090:30090/TCP,8080:32011/TCP   22s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여기서 포트 정보가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;9090:30090/TCP&lt;/code&gt;,&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8080:32011/TCP&lt;/code&gt; 두 가지가 보인다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;9090:30090/TCP&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;9090번은 프로메테우스 서버의 메인 포트이다.&lt;/li&gt;
      &lt;li&gt;이를 &lt;strong&gt;NodePort 30090으로 매핑&lt;/strong&gt;했다.&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;우리가 접속해야 할 포트&lt;/strong&gt;이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;8080:32011/TCP&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;8080번은 프로메테우스 파드 내에 있는 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/20/sidecar-pattern/&quot;&gt;사이드카(Config Reloader 등)&lt;/a&gt; 컨테이너용 포트이다.&lt;/li&gt;
      &lt;li&gt;이는 NodePort 32011로 매핑되어 있다.&lt;/li&gt;
      &lt;li&gt;메인 UI가 아니므로 이곳으로 접속하면 UI를 볼 수 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 외부에서 접속했을 때 Node Exporter에 접속할 수 있도록 호스트(VM) 포트포워딩 설정 시 30090 포트를 열어야 한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/port.png&quot; alt=&quot;포트포워딩 설정&quot; /&gt;&lt;/p&gt;

&lt;p&gt;웹 브라우저에서 &lt;a href=&quot;http://127.0.0.1:8080&quot;&gt;http://127.0.0.1:8080&lt;/a&gt; 으로 접속하면 프로메테우스 메인 화면을 확인할 수 있다. (Node Exporter)&lt;/p&gt;

&lt;p&gt;상단 검색창에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node_memory_MemTotal_bytes&lt;/code&gt;와 같은 쿼리를 입력하고 실행하면, Node Exporter가 수집한 데이터가 조회되는 것을 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/welcome.png&quot; alt=&quot;프로메테우스 접속&quot; /&gt;&lt;/p&gt;

&lt;p&gt;아래와 같이 다양한 정보를 검색할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/pro1.png&quot; alt=&quot;프로메테우스 검색 조회 1&quot; /&gt;
&lt;img src=&quot;/assets/img/dev/2026/0101/pro2.png&quot; alt=&quot;프로메테우스 검색 조회 2&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-그라파나grafana를-통한-모니터링-데이터-시각화&quot;&gt;3. 그라파나(Grafana)를 통한 모니터링 데이터 시각화&lt;/h1&gt;

&lt;p&gt;프로메테우스가 데이터를 수집하고 저장하는 역할을 한다면, &lt;strong&gt;그라파나&lt;/strong&gt;는 이 데이터를 시각화해주는 도구이다.&lt;br /&gt;
프로메테우스 자체 UI는 디버깅 용도로는 훌륭하지만, 운영자가 전체 시스템 현황을 한 눈에 파악하기에는 부족함이 있다.&lt;br /&gt;
따라서 프로메테우스와 그라파나는 바늘과 실처럼 항상 함께 사용된다.&lt;/p&gt;

&lt;p&gt;앞서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-prometheus-stack&lt;/code&gt;을 설치할 때 그라파나도 함께 설치되었으므로, 여기서는 그라파나의 외부 접속 설정을 변경하고 대시보드를 구성하는 방법을 다룬다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-그라파나-서비스-타입-변경loadbalancer&quot;&gt;3.1. 그라파나 서비스 타입 변경(LoadBalancer)&lt;/h2&gt;

&lt;p&gt;먼저 현재 그라파나 서비스의 상태를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get svc &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mymonitoring
NAME                                                        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                         AGE
alertmanager-operated                                       ClusterIP   None             &amp;lt;none&amp;gt;        9093/TCP,9094/TCP,9094/UDP      76m
kube-prometheus-stack-1767-alertmanager                     ClusterIP   10.111.7.79      &amp;lt;none&amp;gt;        9093/TCP,8080/TCP               76m
kube-prometheus-stack-1767-operator                         ClusterIP   10.106.8.158     &amp;lt;none&amp;gt;        443/TCP                         76m
kube-prometheus-stack-1767-prometheus                       NodePort    10.106.34.63     &amp;lt;none&amp;gt;        9090:30090/TCP,8080:32011/TCP   76m
&lt;span class=&quot;c&quot;&gt;# 그라파나 존재&lt;/span&gt;
kube-prometheus-stack-1767503046-grafana                    ClusterIP   10.101.160.47    &amp;lt;none&amp;gt;        80/TCP                          76m
kube-prometheus-stack-1767503046-kube-state-metrics         ClusterIP   10.108.142.71    &amp;lt;none&amp;gt;        8080/TCP                        76m
kube-prometheus-stack-1767503046-prometheus-node-exporter   ClusterIP   10.102.144.228   &amp;lt;none&amp;gt;        9100/TCP                        76m
prometheus-operated                                         ClusterIP   None             &amp;lt;none&amp;gt;        9090/TCP                        76m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;기본적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterIP&lt;/code&gt;로 설정되어 있어 외부에서 직접 접속이 불가능하다.&lt;br /&gt;
이를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadBalancer&lt;/code&gt; 타입으로 변경하여 외부 IP를 할당받도록 설정한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;311-서비스-패치patch&quot;&gt;3.1.1. 서비스 패치(Patch)&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl edit&lt;/code&gt; 을 사용할 수도 있지만, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl patch&lt;/code&gt; 명령어를 사용하면 CLI에서 즉시 설정을 변경할 수 있어 편리하다.&lt;/p&gt;

&lt;p&gt;서비스 타입 변경 전&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get svc kube-prometheus-stack-1767503046-grafana &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mymonitoring &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; yaml

apiVersion: v1
kind: Service
metadata:
  annotations:
    meta.helm.sh/release-name: kube-prometheus-stack-1767503046
    meta.helm.sh/release-namespace: mymonitoring
  creationTimestamp: &lt;span class=&quot;s2&quot;&gt;&quot;2026-01-04T05:04:12Z&quot;&lt;/span&gt;
  labels:
    app.kubernetes.io/instance: kube-prometheus-stack-1767503046
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: grafana
    app.kubernetes.io/version: 12.3.1
    helm.sh/chart: grafana-10.4.3
  name: kube-prometheus-stack-1767503046-grafana
  namespace: mymonitoring
  resourceVersion: &lt;span class=&quot;s2&quot;&gt;&quot;539399&quot;&lt;/span&gt;
  uid: d7c2426c-9f08-4396-92d9-18ff27dc705e
spec:
  clusterIP: 10.101.160.47
  clusterIPs:
  - 10.101.160.47
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - name: http-web
    port: 80
    protocol: TCP
    targetPort: grafana
  selector:
    app.kubernetes.io/instance: kube-prometheus-stack-1767503046
    app.kubernetes.io/name: grafana
  sessionAffinity: None
  &lt;span class=&quot;nb&quot;&gt;type&lt;/span&gt;: ClusterIP  &lt;span class=&quot;c&quot;&gt;# spec.type 이 ClusterIP로 되어있음&lt;/span&gt;
status:
  loadBalancer: &lt;span class=&quot;o&quot;&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patch&lt;/code&gt; 명령어를 통해 서비스 타입을 수정한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl patch svc kube-prometheus-stack-1767503046-grafana &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mymonitoring &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{&quot;spec&quot;: {&quot;type&quot;: &quot;LoadBalancer&quot;}}&apos;&lt;/span&gt;
service/kube-prometheus-stack-1767503046-grafana patched
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다시 서비스를 확인하면 그라파나의 서비스 타입이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadBalancer&lt;/code&gt;로 변경된 것을 볼 수 있다.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get svc &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mymonitoring
NAME                                                        TYPE           CLUSTER-IP       EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                         AGE
...
kube-prometheus-stack-1767503046-grafana                    LoadBalancer   10.101.160.47    10.0.2.22     80:31072/TCP                    83m
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;Type: LoadBalancer&lt;/li&gt;
  &lt;li&gt;External-IP: 10.0.2.22&lt;/li&gt;
  &lt;li&gt;Port: 80 (내부적으로 31082 NodePort와 매핑됨)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;다시 그라파나의 정보를 확인해보자.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get svc kube-prometheus-stack-1767503046-grafana &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mymonitoring &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; yaml

apiVersion: v1
kind: Service
metadata:
...
  &lt;span class=&quot;nb&quot;&gt;type&lt;/span&gt;: LoadBalancer
status:
  loadBalancer:
    ingress:
    - ip: 10.0.2.22
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;312-접속을-위한-포트포워딩-설정&quot;&gt;3.1.2. 접속을 위한 포트포워딩 설정&lt;/h3&gt;

&lt;p&gt;VM 환경에서 운영 중이므로, 호스트 OS(내 PC)에서 VM 내부의 LoadBalancer의 IP(10.0.2.22)에 접속하기 위해 VM 포트포워딩을 추가한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/grafana.png&quot; alt=&quot;그라파나 포트포워딩 설정&quot; /&gt;&lt;/p&gt;

&lt;p&gt;설정이 완료되면 &lt;a href=&quot;http://127.0.0.1:2002&quot;&gt;http://127.0.0.1:2002&lt;/a&gt; 을 통해 그라파나에 접근할 수 있게 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-그라파나-접속-및-초기-설정&quot;&gt;3.2. 그라파나 접속 및 초기 설정&lt;/h2&gt;

&lt;h3 id=&quot;321-관리자-비밀번호-확인&quot;&gt;3.2.1. 관리자 비밀번호 확인&lt;/h3&gt;

&lt;p&gt;그라파나의 초기 계정 정보는 쿠버네티스 &lt;strong&gt;Secret&lt;/strong&gt; 리소스에 안전하게 저장되어 있다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 그라파나와 관련된 시크릿 정보 확인&lt;/span&gt;
assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get secrets kube-prometheus-stack-1767503046-grafana &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mymonitoring &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; yaml

apiVersion: v1
data:
  &lt;span class=&quot;c&quot;&gt;# 접속 비밀번호가 암호화되어 있는 것을 확인할 수 있음&lt;/span&gt;
  admin-password: &lt;span class=&quot;nv&quot;&gt;SVBseTFtb0o1cnA3VWJTeHQ5RHBsZWozaDVMNVgzOUIyTkpxcTBwQw&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;==&lt;/span&gt;
  admin-user: &lt;span class=&quot;nv&quot;&gt;YWRtaW4&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
  ldap-toml: &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
kind: Secret
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;시크릿의 내용은 base64로 인코딩되어 있다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jsonpath&lt;/code&gt; 옵션과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;base64 -d&lt;/code&gt; 명령을 조합하여 디코딩된 실제 비밀번호를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 인코딩된 비밀번호를 확인하기 위해 `base64 -d`를 입력하여 인코딩된 비밀번호를 디코딩함&lt;/span&gt;
assu@myserver01:~/work/app/prometheus/kube-prometheus-stack-80.10.0&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get secrets kube-prometheus-stack-1767503046-grafana &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mymonitoring &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;{.data.admin-password}&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;base64&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 실제 비밀번호 확인&lt;/span&gt;
IPly1moJ5rp7UbSxt9Dplej3h5L5X39B2NJqq0pC
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;322-로그인-및-기본-대시보드-확인&quot;&gt;3.2.2. 로그인 및 기본 대시보드 확인&lt;/h3&gt;

&lt;p&gt;웹 브라우저에서 &lt;a href=&quot;http://127.0.0.1:2002&quot;&gt;http://127.0.0.1:2002&lt;/a&gt; 에 접속하면 로그인 화면이 뜬다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;User: admin&lt;/li&gt;
  &lt;li&gt;Password: 위에서 확인한 비밀번호&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;로그인 후 좌측 메뉴의 &lt;strong&gt;Dashboards&lt;/strong&gt;를 클릭하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-prometheus-stack&lt;/code&gt;이 기본적으로 제공하는 다양한 대시보드 목록을 볼 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/dashboards.png&quot; alt=&quot;대시보드 종류 선택&quot; /&gt;&lt;/p&gt;

&lt;p&gt;예를 들어 ‘Kubernetes / Compute Resources / Node (Pods)’ 등을 클릭하면 별도의 설정 없이도 노드별 리소스 사용량을 그래프로 확인할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;33-외부-대시보드-임포트&quot;&gt;3.3. 외부 대시보드 임포트&lt;/h2&gt;

&lt;p&gt;기본 대시보드 외에도 전 세계 사용자들이 만들어 공유한 대시보드를 쉽게 가져와 사용할 수 있다.&lt;br /&gt;
여기서는 ‘Kubernetes / Views / Global’ 뷰를 제공하는 ID 13332 대시보드를 추가해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1) 대시보드 ID 복사&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://grafana.com/grafana/dashboards/13332-kube-state-metrics-v2&quot;&gt;https://grafana.com/grafana/dashboards/13332-kube-state-metrics-v2&lt;/a&gt; 접속&lt;/li&gt;
  &lt;li&gt;우측 하단의 Copy ID to clipboard 클릭&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/import.png&quot; alt=&quot;대시보드 임포트 1&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2) 그라파나에서 임포트&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;그라파나 메뉴: Dashboards &amp;gt; New &amp;gt; Import&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;13332 입력 후 Load 클릭&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/import2.png&quot; alt=&quot;대시보드 임포트 2&quot; /&gt;
&lt;img src=&quot;/assets/img/dev/2026/0101/import3.png&quot; alt=&quot;대시보드 임포트 3&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3) 데이터 소스 연결&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;설정 화면 하단의 &lt;strong&gt;Prometheus&lt;/strong&gt; 드롭다운 메뉴에서 데이터 소스로 &lt;strong&gt;Prometheus&lt;/strong&gt;를 선택하고 Import 버튼을 클릭한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/import4.png&quot; alt=&quot;대시보드 임포트 4&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4) 결과 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;임포트가 완료되면 아래와 같이 클러스터 전체의 상태를 보여주는 새로운 대시보드가 생성된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/import5.png&quot; alt=&quot;대시보드 확인&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이처럼 그라파나는 강력한 커뮤니티 생태계를 가지고 있어, 필요한 거의 모든 형태의 모니터링 뷰를 손쉽게 구축할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-로키loki를-활용한-쿠버네티스-로그-확인&quot;&gt;4. 로키(Loki)를 활용한 쿠버네티스 로그 확인&lt;/h1&gt;

&lt;p&gt;리소스 사용량은 프로메테우스로 확인했다면, 애플리케이션이나 시스템에서 발생하는 &lt;strong&gt;로그&lt;/strong&gt;는 어떻게 관리해야 할까?&lt;br /&gt;
전통적인 리눅스 서버라면 각 노드에 접속하여 /var/log 를 뒤지겠지만, 파드가 수시로 생성되고 사라지는 쿠버네티스 환경에서는 불가능하다.&lt;/p&gt;

&lt;p&gt;이를 해결하기 위해 &lt;strong&gt;PLG 스택(Promtail + Loki + Grafana)&lt;/strong&gt;을 구축하여 로그를 중앙에서 통합 관리한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-로키-개념과-plgpromtaillokigrafana-개념&quot;&gt;4.1. 로키 개념과 PLG(Promtail+Loki+Grafana) 개념&lt;/h2&gt;

&lt;p&gt;로키는 ‘로그를 위한 프로메테우스’라고 불리는 오픈소스 로그 집계 시스템이다.&lt;br /&gt;
로그 데이터 전체를 인덱싱하는 대신, 로그에 붙은 라벨(Label)만 인덱싱하여 자원 소모가 적고 운영 비용이 저렴하다는 장점이 있다.&lt;/p&gt;

&lt;p&gt;로키는 단독으로 쓰이지 않고 보통 다음의 PLG 구조로 동작한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/plg.png&quot; alt=&quot;Promtail-로키-그라파나 구조&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Promtail&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;로그 수집기 역할&lt;/li&gt;
      &lt;li&gt;모든 노드에 데몬셋(DaemonSet)으로 설치되어 각 파드와 노드의 로그를 수집(Tail)하여 로키로 전송&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Loki&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;로그 저장소 역할&lt;/li&gt;
      &lt;li&gt;수집된 로그를 저장하고 LogQL이라는 쿼리 언어를 통해 조회할 수 있게 해줌&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Grafana&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시각화 역할&lt;/li&gt;
      &lt;li&gt;로키를 데이터 소스로 등록하여 로그를 검색하고 대시보드로 보여줌&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;42-로키-설치loki-stack&quot;&gt;4.2. 로키 설치(Loki Stack)&lt;/h2&gt;

&lt;h3 id=&quot;421-헬름-차트-준비&quot;&gt;4.2.1. 헬름 차트 준비&lt;/h3&gt;

&lt;p&gt;작업 디렉터리를 생성하고 그라파나 공식 헬름 리포지토리를 추가한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;
/home/assu/work/app
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ll
total 32
drwxrwxr-x  8 assu assu 4096 Jan  4 03:02 ./
drwxrwxr-x 10 assu assu 4096 Jan  3 07:54 ../
drwxrwxr-x  3 assu assu 4096 Jan  3 08:54 argocd/
drwxrwxr-x  2 assu assu 4096 Dec 27 04:40 helm/
drwxrwxr-x  3 assu assu 4096 Dec 27 11:18 metallb/
drwxrwxr-x  3 assu assu 4096 Jan  4 02:08 metric-server/
drwxrwxr-x  3 assu assu 4096 Dec 27 06:45 nginx-ingress-controller/
drwxrwxr-x  3 assu assu 4096 Jan  4 03:07 prometheus/
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;loki
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;loki/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/loki&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo add grafana https://grafana.github.io/helm-charts
&lt;span class=&quot;s2&quot;&gt;&quot;grafana&quot;&lt;/span&gt; has been added to your repositories

assu@myserver01:~/work/app/loki&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo update
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;우리는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loki-stack&lt;/code&gt; 차트를 사용할 것이다.&lt;br /&gt;
이 차트는 로키와 프롬테일을 한 번에 설치해준다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/loki&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm search repo loki
NAME                        	CHART VERSION	APP VERSION	DESCRIPTION
...
grafana/loki-stack          	2.10.3       	v2.9.3     	Loki: like Prometheus, but &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;logs.
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/loki&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm pull grafana/loki-stack

assu@myserver01:~/work/app/loki&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;loki-stack-2.10.3.tgz

assu@myserver01:~/work/app/loki&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tar &lt;/span&gt;xvfz loki-stack-2.10.3.tgz

assu@myserver01:~/work/app/loki&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;loki-stack  loki-stack-2.10.3.tgz

assu@myserver01:~/work/app/loki&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mv &lt;/span&gt;loki-stack loki-stack-2.10.3

assu@myserver01:~/work/app/loki&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;loki-stack-2.10.3  loki-stack-2.10.3.tgz
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;422-valuesyaml-수정&quot;&gt;4.2.2. values.yaml 수정&lt;/h3&gt;

&lt;p&gt;설정 파일을 복사하여 my-values.yaml 을 생성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/loki&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;loki-stack-2.10.3/

assu@myserver01:~/work/app/loki/loki-stack-2.10.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;charts  Chart.yaml  README.md  requirements.lock  requirements.yaml  templates  values.yaml

assu@myserver01:~/work/app/loki/loki-stack-2.10.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp &lt;/span&gt;values.yaml my-values.yaml

assu@myserver01:~/work/app/loki/loki-stack-2.10.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;charts  Chart.yaml  my-values.yaml  README.md  requirements.lock  requirements.yaml  templates  values.yaml
assu@myserver01:~/work/app/loki/loki-stack-2.10.3&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이미 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-prometheus-stack&lt;/code&gt;을 통해 &lt;strong&gt;프로메테우스와 그라파나를 설치&lt;/strong&gt;했다.&lt;br /&gt;
따라서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loki-stack&lt;/code&gt;에서는 그라파나와 프로메테우스를 중복 설치하지 않도록 비활성화하고, 로그 수집에 필요한 &lt;strong&gt;로키와 프롬테일만 활성화&lt;/strong&gt;한다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;test_pod&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;loki&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;promtail&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;423-설치-및-확인&quot;&gt;4.2.3. 설치 및 확인&lt;/h3&gt;

&lt;p&gt;myloki 네임스페이스를 생성하고 로키 스택을 배포한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/loki/loki-stack-2.10.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl create namespace myloki
namespace/myloki created

assu@myserver01:~/work/app/loki/loki-stack-2.10.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get namespace
NAME               STATUS   AGE
...
myloki             Active   13s  &lt;span class=&quot;c&quot;&gt;# 확인&lt;/span&gt;
...

assu@myserver01:~/work/app/loki/loki-stack-2.10.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; myloki &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--generate-name&lt;/span&gt; grafana/loki-stack &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; my-values.yaml

&lt;span class=&quot;nv&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;WARN &lt;span class=&quot;nv&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;this chart is deprecated&quot;&lt;/span&gt;
NAME: loki-stack-1768029200
LAST DEPLOYED: Sat Jan 10 07:13:21 2026
NAMESPACE: myloki
STATUS: deployed
REVISION: 1
DESCRIPTION: Install &lt;span class=&quot;nb&quot;&gt;complete
&lt;/span&gt;NOTES:
The Loki stack has been deployed to your cluster. Loki can now be added as a datasource &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;Grafana.

See http://docs.grafana.org/features/datasources/loki/ &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;more detail.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;설치 후 상태를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/loki/loki-stack-2.10.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; myloki &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME                                       READY   STATUS    RESTARTS   AGE     IP                NODE         NOMINATED NODE   READINESS GATES
pod/loki-stack-1768029200-0                1/1     Running   0          7m40s   192.168.131.122   myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
pod/loki-stack-1768029200-promtail-4lz8f   0/1     Running   0          7m40s   192.168.143.195   myserver01   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
pod/loki-stack-1768029200-promtail-fwf5m   1/1     Running   0          7m40s   192.168.131.121   myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
pod/loki-stack-1768029200-promtail-hpknz   0/1     Running   0          7m40s   192.168.149.249   myserver03   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

NAME                                       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;    AGE     SELECTOR
service/loki-stack-1768029200              ClusterIP   10.109.170.180   &amp;lt;none&amp;gt;        3100/TCP   7m40s   &lt;span class=&quot;nv&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;loki,release&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;loki-stack-1768029200
service/loki-stack-1768029200-headless     ClusterIP   None             &amp;lt;none&amp;gt;        3100/TCP   7m40s   &lt;span class=&quot;nv&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;loki,release&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;loki-stack-1768029200
service/loki-stack-1768029200-memberlist   ClusterIP   None             &amp;lt;none&amp;gt;        7946/TCP   7m40s   &lt;span class=&quot;nv&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;loki,release&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;loki-stack-1768029200

NAME                                            DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE     CONTAINERS   IMAGES                             SELECTOR
daemonset.apps/loki-stack-1768029200-promtail   3         3         1       3            1           &amp;lt;none&amp;gt;          7m40s   promtail     docker.io/grafana/promtail:3.5.1   app.kubernetes.io/instance&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;loki-stack-1768029200,app.kubernetes.io/name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;promtail
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Loki Pod&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;StatefulSet으로 관리되며 1개가 실행된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Promtail Pod&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;DaemonSet으로 관리되며 클러스터의 모든 노드(여기서는 3개)에 각각 하나씩 실행된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;특정 파드의 로그를 보고 싶다면 아래와 같이 보면 된다.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/loki/loki-stack-2.10.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl describe pod loki-stack-1768029200-promtail-4lz8f &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; myloki
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;43-로그-확인테스트-앱-배포&quot;&gt;4.3. 로그 확인(테스트 앱 배포)&lt;/h2&gt;

&lt;p&gt;로그가 잘 수집되는지 확인하기 위해, 접속 시 로그를 남기는 간단한 Flask 애플리케이션을 배포한다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://assu10.github.io/dev/2025/12/31/kubernetes-ci-cd-github-actions-argocd/#421-%EB%B0%B0%ED%8F%AC%EC%9A%A9-%EB%A7%A4%EB%8B%88%ED%8E%98%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1-%EB%B0%8F-push&quot;&gt;4.2.1. 배포용 매니페스트 작성 및 Push&lt;/a&gt; 에서 
진행했던 디렉터리로 이동하여 디플로이먼트, 서비스, 인그레스를 실행한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~/work/ch10
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01  ex02  ex03
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex02
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;flask-deploy.yml  flask-ingress.yml  flask-service.yml  myFlask02  myNginx02f
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-deploy.yml
deployment.apps/deploy-flask created
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-service.yml
service/flask-service created
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-ingress.yml
ingress.networking.k8s.io/flask-ingress created
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                               READY   STATUS    RESTARTS   AGE
pod/deploy-flask-b8ffb7c86-qm22s   2/2     Running   0          30s
pod/deploy-flask-b8ffb7c86-qzjwm   2/2     Running   0          30s
pod/deploy-flask-b8ffb7c86-w587h   2/2     Running   0          30s

NAME                    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/flask-service   ClusterIP   10.99.99.174   &amp;lt;none&amp;gt;        80/TCP    22s
service/kubernetes      ClusterIP   10.96.0.1      &amp;lt;none&amp;gt;        443/TCP   15d

NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/deploy-flask   3/3     3            3           30s

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/deploy-flask-b8ffb7c86   3         3         3       30s

assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get ingress
NAME            CLASS   HOSTS   ADDRESS   PORTS   AGE
flask-ingress   nginx   &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;                 80      35s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;배포가 완료되면 &lt;a href=&quot;http://127.0.0.1:2002/test02&quot;&gt;http://127.0.0.1:2002/test02&lt;/a&gt; 등으로 접속하여 트래픽을 발생시킨다.&lt;br /&gt;
화면에 hello world! 가 출력되면 로그가 생성된 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;44-그라파나와-로키-연동-및-로그-조회&quot;&gt;4.4. 그라파나와 로키 연동 및 로그 조회&lt;/h2&gt;

&lt;p&gt;이제 그라파나(화면)와 로키(데이터)를 연결해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;441-데이터-소스data-source-추가&quot;&gt;4.4.1. 데이터 소스(Data Source) 추가&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;그라파나(&lt;a href=&quot;http://127.0.0.1:2002/&quot;&gt;http://127.0.0.1:2002/&lt;/a&gt;)에 접속한다.&lt;/li&gt;
  &lt;li&gt;좌측 메뉴 Connections &amp;gt; Add new connection 으로 이동한다.&lt;/li&gt;
  &lt;li&gt;Loki를 검색하고 클릭한 뒤, 우측 상단의 Add new data source를 누른다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/loki.png&quot; alt=&quot;로키 선택&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;442-연결-정보-설정&quot;&gt;4.4.2. 연결 정보 설정&lt;/h3&gt;

&lt;p&gt;로키 서비스의 내부 도메인 주소를 입력해야 한다. 먼저 서비스 이름을 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get svc &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; myloki
NAME                               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;    AGE
loki-stack-1768029200              ClusterIP   10.109.170.180   &amp;lt;none&amp;gt;        3100/TCP   29m
loki-stack-1768029200-headless     ClusterIP   None             &amp;lt;none&amp;gt;        3100/TCP   29m
loki-stack-1768029200-memberlist   ClusterIP   None             &amp;lt;none&amp;gt;        7946/TCP   29m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;설정값:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;: Loki(임시 지정)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;URL&lt;/strong&gt;: http://loki-stack-1768029200.myloki:3100
    &lt;ul&gt;
      &lt;li&gt;형식: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;http://&amp;lt;로키서비스이름&amp;gt;.&amp;lt;네임스페이스&amp;gt;:3100&lt;/code&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/loki2.png&quot; alt=&quot;로키 정보 선택&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Save &amp;amp; test 버튼을 누른다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;443-로그-쿼리logql&quot;&gt;4.4.3. 로그 쿼리(LogQL)&lt;/h3&gt;

&lt;p&gt;이제 실제로 로그를 검색해보자.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;좌측 메뉴의 Explore를 클릭한다.&lt;/li&gt;
  &lt;li&gt;좌측 상단 데이터 소스 드롭다운에서 방금 추가한 Loki를 선택한다.&lt;/li&gt;
  &lt;li&gt;Label filters 혹은 Code 모드에서 쿼리를 입력한다.
    &lt;ul&gt;
      &lt;li&gt;쿼리 형식: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{pod=&quot;파드 이름&quot;}&lt;/code&gt;&lt;/li&gt;
      &lt;li&gt;예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{pod=&quot;deploy-flask-b8ffb7c86-qm22s&quot;}&lt;/code&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;우측 상단의 Run query를 클릭하면 아래와 같이 해당 파드에서 발생한 로그가 시간순으로 출력된다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{namespace=&quot;default&quot;}&lt;/code&gt; 처럼 네임스페이스 단위로 검색하거나, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;|= &quot;error&quot;&lt;/code&gt;와 같이 특정 문자열이 포함된 로그만 필터링할 수도 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2026/0101/loki3.png&quot; alt=&quot;로그 확인&quot; /&gt;&lt;/p&gt;

&lt;p&gt;파드 이름은 아래와 같이 확인한다.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod
NAME                           READY   STATUS    RESTARTS   AGE
deploy-flask-b8ffb7c86-qm22s   2/2     Running   0          33m
deploy-flask-b8ffb7c86-qzjwm   2/2     Running   0          33m
deploy-flask-b8ffb7c86-w587h   2/2     Running   0          33m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이제 리소스를 정리한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-ingress.yml
ingress.networking.k8s.io &lt;span class=&quot;s2&quot;&gt;&quot;flask-ingress&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-service.yml
service &lt;span class=&quot;s2&quot;&gt;&quot;flask-service&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-deploy.yml
deployment.apps &lt;span class=&quot;s2&quot;&gt;&quot;deploy-flask&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   15d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며..&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Metrics Server&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl top&lt;/code&gt;을 통해 노드와 파드의 즉각적인 리소스 사용량을 확인한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Prometheus&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;시계열 데이터 수집의 표준인 프로메테우스를 헬름으로 구축하고 Node Exporter 데이터를 확인한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Grafana&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;프로메테우스와 연동하여 리소스 사용량을 시각화하고, 외부 대시보드를 임포트하여 모니터링 환경을 완성할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Loki(PLG Stack)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;분산된 노드의 로그를 중앙으로 수집하고, 그라파나에서 통합 조회하는 환경을 구축할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 장철원 저자의 &lt;strong&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/126115324&quot;&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/losskatsu/DockerKubernetes&quot;&gt;예제 코드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Thu, 01 Jan 2026 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2026/01/01/kubernetes-monitoring-prometheus-grafana-loki/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2026/01/01/kubernetes-monitoring-prometheus-grafana-loki/</guid>
        
        <category>devops</category>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>monitoring</category>
        
        <category>observability</category>
        
        <category>metric-server</category>
        
        <category>prometheus</category>
        
        <category>grafana</category>
        
        <category>loki</category>
        
        <category>plg-stack</category>
        
        <category>helm</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Kubernetes - 깃허브 액션과 ArgoCD를 활용한 CI/CD 파이프라인 구축</title>
        <description>&lt;p&gt;클라우드 네이티브 환경에서 애플리케이션을 신속하고 안정적으로 배포하는 것은 선택이 아닌 필수이다.&lt;br /&gt;
수동으로 컨테이너를 빌드하고 배포하는 과정은 비효율적일 뿐만 아니라 휴먼 에러를 유발할 수 있기 때문이다.&lt;/p&gt;

&lt;p&gt;이번 포스트에서는 &lt;strong&gt;지속적 통합(Continuous Integration)&lt;/strong&gt;과 &lt;strong&gt;지속적 전달/배포(Continuous Delivery 혹은 Deployment)&lt;/strong&gt; 과정을 
자동화하는 방법에 대해 알아본다.&lt;/p&gt;

&lt;p&gt;특히 개발자들에게 익숙한 &lt;strong&gt;깃허브 액션&lt;/strong&gt;을 이용하여 CI를 구성하고, 쿠버네티스 환경의 배포 표준으로 자리 잡고 있는 GitOps 도구인 &lt;strong&gt;ArgoCD&lt;/strong&gt;를 활용하여 
CD를 구현해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-cicd의-이해&quot;&gt;1. CI/CD의 이해&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-사전-준비-사항&quot;&gt;2. 사전 준비 사항&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-metallb-설치-확인&quot;&gt;2.1. MetalLB 설치 확인&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-깃허브-가입&quot;&gt;2.2. 깃허브 가입&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-깃-설치&quot;&gt;2.3. 깃 설치&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-깃허브github-액션을-통한-소스코드-관리&quot;&gt;3. 깃허브(GitHub) 액션을 통한 소스코드 관리&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-깃허브-액션을-사용한-hello-world-출력&quot;&gt;3.1. 깃허브 액션을 사용한 Hello World! 출력&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-깃허브-액션을-통한-도커-컨테이너-실행&quot;&gt;3.2. 깃허브 액션을 통한 도커 컨테이너 실행&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#321-프로젝트-디렉터리-및-코드-준비&quot;&gt;3.2.1. 프로젝트 디렉터리 및 코드 준비&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#322-github-actions-워크플로-작성&quot;&gt;3.2.2. GitHub Actions 워크플로 작성&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#323-코드-푸시-및-결과-확인&quot;&gt;3.2.3. 코드 푸시 및 결과 확인&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-argocd를-활용한-cd&quot;&gt;4. ArgoCD를 활용한 CD&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-argocd-설치&quot;&gt;4.1. ArgoCD 설치&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#42-argocd를-활용한-깃허브-실습&quot;&gt;4.2. ArgoCD를 활용한 깃허브 실습&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#421-배포용-매니페스트-작성-및-push&quot;&gt;4.2.1. 배포용 매니페스트 작성 및 Push&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#422-argocd-애플리케이션-연동&quot;&gt;4.2.2. ArgoCD 애플리케이션 연동&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#423-배포-확인-및-상태-변경&quot;&gt;4.2.3. 배포 확인 및 상태 변경&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#정리하며&quot;&gt;정리하며..&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;개발 환경&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Guest OS: Ubuntu 24.04.2 LTS&lt;/li&gt;
  &lt;li&gt;Host OS: Mac Apple M3 Max&lt;/li&gt;
  &lt;li&gt;Memory: 48 GB&lt;/li&gt;
  &lt;li&gt;Kubernetes: v1.29.15&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-cicd의-이해&quot;&gt;1. CI/CD의 이해&lt;/h1&gt;

&lt;p&gt;애플리케이션 개발 프로젝트는 다수의 개발자가 동시에 참여하여 진행한다. 서로 다른 기능을 개발하는 개발자들이 각자의 코드를 하나로 통합하는 과정은 필수적이며, 
이 과정에서 충돌을 해결하고 정합성을 검증하는 일은 매우 중요하다.&lt;/p&gt;

&lt;p&gt;이러한 통합과 배포 작업이 하루에도 여러 번 발생할 수 있는데, 이를 수동으로 처리한다면 엄청난 리소스 낭비와 휴먼 에러를 유발하게 된다.&lt;br /&gt;
이를 해결하기 위해 등장한 개념이 &lt;strong&gt;CI/CD&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;CI(Continuous Integration, 지속적 통합)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CI는 개발자가 작성한 코드를 공유 리포지토리에 지속적으로 통합하고, 이 과정에서 빌드 및 테스트를 자동화하는 프로세스를 의미한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;프로세스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;개발자가 코드를 Git과 같은 버전 관리 시스템에 push하면, CI 서버(GitHub Actions, Jenkins 등)가 트리거되어 코드를 가져오고, 빌드하며, 단위 테스트 등을 자동으로 수행한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;버그를 조기에 발견할 수 있어 배포 위험을 획기적으로 줄이고, 소프트웨어의 품질을 일정 수준 이상으로 유지할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;CD(Continuous Delivery/Deployment, 지속적 전달/배포)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CD는 CI 과정을 통과한 코드를 실제 사용자에게 서비스하기 위해 배포하는 단계를 자동화하는 것이다.&lt;br /&gt;
용어는 비슷해 보이지만 범위에 따라 두 가지로 나뉜다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;지속적 전달(Continuous Delivery)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;코드를 운영 환경에 배포하기 전, 프로덕션과 유사한 스테이징 환경까지 배포하거나 배포 승인 단계까지 준비해두는 것을 의미한다.&lt;/li&gt;
      &lt;li&gt;실제 배포는 수동 트리거가 필요할 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;지속적 배포(Continuous Deployment)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;코드 변경 사항이 파이프라인의 모든 단계를 통과하면 사람의 개입 없이 자동으로 프로덕션 환경까지 배포되는 것을 의미한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이번 포스트에서는 &lt;strong&gt;GitHub Actions&lt;/strong&gt;를 활용하여 CI를 구축하고, 쿠버네티스 환경의 배포를 위해 &lt;strong&gt;ArgoCD&lt;/strong&gt;를 활용하여 CD를 실습해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-사전-준비-사항&quot;&gt;2. 사전 준비 사항&lt;/h1&gt;

&lt;p&gt;먼저 로드밸런서 구성과 Git 환경 설정이 필요하다.&lt;/p&gt;

&lt;p&gt;먼저 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-loadbalancer-for-bare-metal/&quot;&gt;MetalLB&lt;/a&gt;가 올바르게 설치되었는지 확인한 후 GitHub에 가입하고 Git을 설치한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-metallb-설치-확인&quot;&gt;2.1. MetalLB 설치 확인&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;MetalLB 는 &lt;a href=&quot;#41-argocd-설치&quot;&gt;4.1. ArgoCD 설치&lt;/a&gt; 에서 사용됩니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;이 포스트에서는 베어메탈(Bare-metal) 혹은 온프레미스 VM 환경을 가정하므로, LoadBalancer 타입의 서비스를 사용하기 위해 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-loadbalancer-for-bare-metal/&quot;&gt;&lt;strong&gt;MetalLB&lt;/strong&gt;&lt;/a&gt;가 필요하다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;MetalLB가 설치되어 있지 않다면 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-ingress-helm-metallb-baremetal-loadbalancer/#5-metallb%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%B2%A0%EC%96%B4%EB%A9%94%ED%83%88-loadbalancer-%EA%B5%AC%EC%84%B1&quot;&gt;5. MetalLB를 통한 베어메탈 LoadBalancer 구성&lt;/a&gt;을 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;아래 명령어로 설치 상태를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 헬름을 통한 설치 확인&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm &lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymetallb
NAME              	NAMESPACE	REVISION	UPDATED                                	STATUS  	CHART         	APP VERSION
metallb-1766834438	mymetallb	1       	2025-12-27 11:20:39.760663539 +0000 UTC	deployed	metallb-0.15.3	v0.15.3

&lt;span class=&quot;c&quot;&gt;# mymetallb 네임스페이스에서 작동 중인 오브젝트 확인&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymetallb
NAME                                               READY   STATUS    RESTARTS   AGE
pod/metallb-1766834438-controller-66c6c584-kbwdn   1/1     Running   0          6d19h
pod/metallb-1766834438-speaker-7sjlw               4/4     Running   0          6d19h
pod/metallb-1766834438-speaker-j99kv               4/4     Running   0          6d19h
pod/metallb-1766834438-speaker-zkktj               4/4     Running   0          6d19h

NAME                              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/metallb-webhook-service   ClusterIP   10.97.206.62   &amp;lt;none&amp;gt;        443/TCP   6d19h

NAME                                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/metallb-1766834438-speaker   3         3         3       3            3           kubernetes.io/os&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux   6d19h

NAME                                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/metallb-1766834438-controller   1/1     1            1           6d19h

NAME                                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/metallb-1766834438-controller-66c6c584   1         1         1       6d19h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Controller와 Speaker 파드가 모두 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Running&lt;/code&gt; 상태이고, Webhook 서비스가 정상적으로 떠 있다면 준비가 완료된 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-깃허브-가입&quot;&gt;2.2. 깃허브 가입&lt;/h2&gt;

&lt;p&gt;GitHub Actions를 사용하기 위해서는 GitHub 계정이 필수이다. 계정이 없다면 가입을 진행한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-깃-설치&quot;&gt;2.3. 깃 설치&lt;/h2&gt;

&lt;p&gt;로컬 환경에서 코드를 작성하고 깃허브로 전송하기 위해 Git 클라이언트를 설치하고 기본 설정을 진행한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Git 설치&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;git-all
Reading package lists... Done
Building dependency tree... Done
...

&lt;span class=&quot;c&quot;&gt;# 사용자 정보 설정(커밋 로그에 남을 정보)&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git config &lt;span class=&quot;nt&quot;&gt;--list&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.name &amp;lt;이름&amp;gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.email &amp;lt;이메일&amp;gt;

assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;app  ch04  ch05  ch06  ch09  ch10

&lt;span class=&quot;c&quot;&gt;# 작업 디렉터리 초기화&lt;/span&gt;
assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git init
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-깃허브github-액션을-통한-소스코드-관리&quot;&gt;3. 깃허브(GitHub) 액션을 통한 소스코드 관리&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt;는 깃허브 리포지토리 내에서 바로 소프트웨어 개발 워크플로를 자동화할 수 있게 해주는 강력한 CI/CD 도구이다.&lt;br /&gt;
별도의 CI 서버를 구축할 필요 없이 YAML 파일 설정만으로 빌드, 테스트, 배포 파이프라인을 정의할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-깃허브-액션을-사용한-hello-world-출력&quot;&gt;3.1. 깃허브 액션을 사용한 Hello World! 출력&lt;/h2&gt;

&lt;p&gt;가장 기본적인 워크플로를 작성하여 동작 방식을 이해해본다.&lt;br /&gt;
먼저 깃허브 웹사이트에서 &lt;em&gt;github-action-practice&lt;/em&gt; 라는 이름의 리포지토리를 생성한다.&lt;/p&gt;

&lt;p&gt;리포지토리 생성 후 &lt;strong&gt;Actions&lt;/strong&gt; 탭으로 이동하여 새로운 워크 플로를 생성한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/workflow.png&quot; alt=&quot;액션 워크플로 생성&quot; /&gt;&lt;/p&gt;

&lt;p&gt;그 다음에 뜨는 &lt;em&gt;github-action-practice/.github/workflows/main.yml&lt;/em&gt; 파일 편집기에 아래 내용을 작성한다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 워크플로 이름&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;HelloWorld&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# 트리거 조건: 코드가 push 될 때 실행&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# 수행할 작업 정의&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;echo&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 작업(Job) 식별자&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu-latest&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 실행될 환경(Runner) 지정 (Ubuntu 최신 버전)&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 순차적으로 실행될 단계들&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;hello&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 실행 단계의 이름&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;echo &quot;Hello, World!&quot;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 실행할 쉘 명령어&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/mainyml.png&quot; alt=&quot;main.yml 파일 생성&quot; /&gt;&lt;/p&gt;

&lt;p&gt;작성 후 커밋하면 GitHub Actions가 자동으로 main.yml을 감지하여 실행한다.&lt;br /&gt;
&lt;strong&gt;Actions&lt;/strong&gt; 탭에서 &lt;em&gt;Create main.yml&lt;/em&gt; 워크플로 실행 내역을 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/action1.png&quot; alt=&quot;액션 확인 1&quot; /&gt;
&lt;img src=&quot;/assets/img/dev/2025/1231/action2.png&quot; alt=&quot;액션 확인 2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;실행된 &lt;em&gt;echo&lt;/em&gt; 작업을 클릭하면 상세 로그를 볼 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/workflow2.png&quot; alt=&quot;워크플로 확인&quot; /&gt;&lt;/p&gt;

&lt;p&gt;로그를 자세히 살펴보면 다음과 같은 정보를 얻을 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Current runner version: &apos;2.330.0&apos;
Runner Image Provisioner
  Hosted Compute Agent
  Version: 20251211.462
  Commit: 6cbad8c2bb55d58165063d031ccabf57e2d2db61
  Build Date: 2025-12-11T16:28:49Z
  Worker ID: {4ff93d9d-3120-4526-890c-90592f60e50a}
Operating System
  Ubuntu
  24.04.3
  LTS
Runner Image
  Image: ubuntu-24.04
  Version: 20251215.174.1
  Included Software: https://github.com/actions/runner-images/blob/ubuntu24/20251215.174/images/ubuntu/Ubuntu2404-Readme.md
  Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu24%2F20251215.174
GITHUB_TOKEN Permissions
  Contents: read
  Metadata: read
  Packages: read
Secret source: Actions
Prepare workflow directory
Prepare all required actions
Complete job name: echo
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-깃허브-액션을-통한-도커-컨테이너-실행&quot;&gt;3.2. 깃허브 액션을 통한 도커 컨테이너 실행&lt;/h2&gt;

&lt;p&gt;이제 단순한 텍스트 출력을 넘어, 실제 애플리케이션을 도커 이미지로 빌드하고 컨테이너로 실행하는 CI 파이프라인을 구축해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;321-프로젝트-디렉터리-및-코드-준비&quot;&gt;3.2.1. 프로젝트 디렉터리 및 코드 준비&lt;/h3&gt;

&lt;p&gt;먼저 로컬 작업 환경을 구성하고 원격 리포지토리를 clone 한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;app  ch04  ch05  ch06  ch09  ch10
assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ch11
assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ch11
assu@myserver01:~/work/ch11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex01
assu@myserver01:~/work/ch11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex01
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git clone https://github.com/assu/github-action-practice.git
Cloning into &lt;span class=&quot;s1&quot;&gt;&apos;github-action-practice&apos;&lt;/span&gt;...
...
Receiving objects: 100% &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;5/5&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;

assu@myserver01:~/work/ch11/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;github-action-practice

assu@myserver01:~/work/ch11/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;github-action-practice/

assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls

&lt;/span&gt;assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-al&lt;/span&gt;
total 16
drwxrwxr-x 4 assu assu 4096 Jan  3 08:04 &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
drwxrwxr-x 3 assu assu 4096 Jan  3 08:04 ..
drwxrwxr-x 8 assu assu 4096 Jan  3 08:04 .git
drwxrwxr-x 3 assu assu 4096 Jan  3 08:04 .github

assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; .github/workflows/
assu@myserver01:~/work/ch11/ex01/github-action-practice/.github/workflows&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-al&lt;/span&gt;
total 12
drwxrwxr-x 2 assu assu 4096 Jan  3 08:04 &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
drwxrwxr-x 3 assu assu 4096 Jan  3 08:04 ..
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt; 1 assu assu  136 Jan  3 08:04 main.yml

assu@myserver01:~/work/ch11/ex01/github-action-practice/.github/workflows&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cat &lt;/span&gt;main.yml
name: HelloWorld

on: &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;push]

&lt;span class=&quot;nb&quot;&gt;jobs&lt;/span&gt;:
  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt;:
    runs-on: ubuntu-latest
    steps:
      - name: hello
        run: &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Hello, World!&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Dockerfile 작성&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;애플리케이션을 컨테이너화하기 위한 명세서이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim Dockerfile
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-dockerfile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 베이스 이미지로 Python 3.13.9 버전을 사용&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; python:3.13.9&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 컨테이너 내 작업 디렉터리를 /usr/src/app 으로 설정&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /usr/src/app&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 현재 디렉터리의 모든 파일을 컨테이너의 작업 디렉터리로 복사&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; .. .&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# pip를 최신 버전으로 업그레이드하고, 의존성 패키지를 설치&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;python &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; pip &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--upgrade&lt;/span&gt; pip
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;pip &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; requirements.txt

&lt;span class=&quot;c&quot;&gt;# 애플리케이션 소스 코드가 있는 디렉터리로 이동&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; ./myapp&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 컨테이너 실행 시 Gunicorn을 사용하여 Flask 앱을 실행&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 0.0.0.0:8001 주소로 바인딩하여 외부 접속을 허용&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CMD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; [&quot;gunicorn&quot;, &quot;main:app&quot;, &quot;--bind&quot;, &quot;0.0.0.0:8001&quot;]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;EXPOSE&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; 8001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;requirements.txt 작성&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;설치할 파이썬 패키지 목록이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim requirements.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;flask==3.1.2
gunicorn==23.0.0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;flask: 웹 프레임워크&lt;/li&gt;
  &lt;li&gt;gunicorn: 프로덕션 환경에서 사용할 WSGI HTTP 서버&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;애플리케이션 코드(myapp/main.py) 작성&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Flask 라이브러리를 활용하여 간단한 웹 애플리케이션 코드를 작성한다.&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;flask&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Flask&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Flask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# 루트 경로(&apos;/&apos;)로 접속 시 &apos;hello world!&apos;를 반환
&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;/&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hello_world&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;hello world!&apos;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# 스크립트가 직접 실행될 경우 8001 포트에서 앱을 실행
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;__main__&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;0.0.0.0&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;8001&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;322-github-actions-워크플로-작성&quot;&gt;3.2.2. GitHub Actions 워크플로 작성&lt;/h3&gt;

&lt;p&gt;이제 위에서 만든 애플리케이션을 테스트하는 워크플로 파일 &lt;em&gt;.github/workflows/flask-test.yml&lt;/em&gt; 을 작성한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;작업 디렉터리 정리&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-al&lt;/span&gt;
total 28
drwxrwxr-x 5 assu assu 4096 Jan  3 08:12 &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
drwxrwxr-x 3 assu assu 4096 Jan  3 08:04 ..
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt; 1 assu assu  214 Jan  3 08:10 Dockerfile
drwxrwxr-x 8 assu assu 4096 Jan  3 08:04 .git
drwxrwxr-x 3 assu assu 4096 Jan  3 08:04 .github
drwxrwxr-x 2 assu assu 4096 Jan  3 08:14 myapp
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt; 1 assu assu   30 Jan  3 08:11 requirements.txt

assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; .github/
assu@myserver01:~/work/ch11/ex01/github-action-practice/.github&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;workflows

assu@myserver01:~/work/ch11/ex01/github-action-practice/.github&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;workflows/
assu@myserver01:~/work/ch11/ex01/github-action-practice/.github/workflows&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;main.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;워크플로 작성&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex01/github-action-practice/.github/workflows&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim flask-test.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# 워크플로 이름 정의&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Docker Test&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# main 브랜치에 push 이벤트가 발생할 때만 이 워크플로를 실행&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;main&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# 1. GitHub 저장소의 코드를 러너 환경으로 체크아웃(내려받기)&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Checkout code&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/checkout@v3&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 깃허브 액션에서 제공하는 checkout 액션 의미&lt;/span&gt;

      &lt;span class=&quot;c1&quot;&gt;# 2. 파이썬 3.13 환경을 설정&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Setup Python&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/setup-python@v3&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;with&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;python-version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3.13&apos;&lt;/span&gt;
          
      &lt;span class=&quot;c1&quot;&gt;# 3. Dockerfile을 기반으로 이미지를 빌드. 태그는 myflask-test로 지정    &lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;build Flask Docker Image&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;docker image build -t myflask-test .&lt;/span&gt;
        
      &lt;span class=&quot;c1&quot;&gt;# 4. 빌드된 이미지로 컨테이너를 실행 (백그라운드 모드 -d, 포트 포워딩 -p)  &lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Run Flask Docker Container&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;docker container run -d --name myflask-ac -p 8001:8001 myflask-test&lt;/span&gt;
        
      &lt;span class=&quot;c1&quot;&gt;# 5. 컨테이너가 구동될 시간을 잠시 대기(sleep)한 후, curl 명령어로 응답을 테스트  &lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Test Flask App&lt;/span&gt; 
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;sleep 10&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;curl http://127.0.0.1:8001&lt;/span&gt;

      &lt;span class=&quot;c1&quot;&gt;# 6. 테스트가 끝나면 컨테이너를 정지하고 삭제하여 환경을 정리    &lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Stop and remove Docker Container&lt;/span&gt; 
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;docker container stop myflask-ac&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;docker container rm myflask-ac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;323-코드-푸시-및-결과-확인&quot;&gt;3.2.3. 코드 푸시 및 결과 확인&lt;/h3&gt;

&lt;p&gt;작성한 파일들을 Git에 추가하고 원격 저장소로 푸시한다.&lt;/p&gt;

&lt;p&gt;먼저 업로드할 깃 브랜치와 리모트 정보를 확인한다.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git branch
&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; main
assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git remote
origin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 파일을 스테이지 영역에 추가&lt;/span&gt;
assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git add &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;

assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git commit &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;flask docker test&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;main 6e44db2] flask docker &lt;span class=&quot;nb&quot;&gt;test
 &lt;/span&gt;4 files changed, 55 insertions&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;+&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
 create mode 100644 .github/workflows/flask-test.yml
 create mode 100644 Dockerfile
 create mode 100644 myapp/main.py
 create mode 100644 requirements.txt

assu@myserver01:~/work/ch11/ex01/github-action-practice&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git push
Username &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;https://github.com&apos;&lt;/span&gt;: &amp;lt;깃허브 ID&amp;gt;
Password &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;https://assu@github.com&apos;&lt;/span&gt;: &amp;lt;깃허브 토큰&amp;gt;
Enumerating objects: 12, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
Counting objects: 100% &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;12/12&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
Delta compression using up to 4 threads
Compressing objects: 100% &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;6/6&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
Writing objects: 100% &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;9/9&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, 1.15 KiB | 1.15 MiB/s, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
Total 9 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;delta 0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, reused 0 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;delta 0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, pack-reused 0
To https://github.com/assu/github-action-practice.git
   7ba9080..6e44db2  main -&amp;gt; main
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;푸시가 완료되면 깃허브 저장소의 &lt;strong&gt;Actions&lt;/strong&gt; 탭에서 워크플로가 자동으로 실행되는 것을 볼 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/result1.png&quot; alt=&quot;깃허브 액션 결과 확인 1&quot; /&gt;
&lt;img src=&quot;/assets/img/dev/2025/1231/result2.png&quot; alt=&quot;깃허브 액션 결과 확인 2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;build&lt;/strong&gt; 버튼을 눌러서 세부 작업 내역을 확인해 보면, 도커 이미지가 빌드되고 컨테이너가 실행된 후 curl 명령어를 통해 hello world! 응답을 정상적으로 수신했음을 
확인할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/result3.png&quot; alt=&quot;깃허브 액션 결과 확인 3&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이로써 코드 변경 사항이 발생했을 때 자동으로 빌드하고 테스트하는 CI 파이프라인을 구축해보았다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-argocd를-활용한-cd&quot;&gt;4. ArgoCD를 활용한 CD&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;#3-깃허브github-액션을-통한-소스코드-관리&quot;&gt;3. 깃허브(GitHub) 액션을 통한 소스코드 관리&lt;/a&gt;에서 깃허브 액션을 통해 코드를 빌드하고 도커 컨테이너가 정상적으로 
실행되는지 테스트하는 &lt;strong&gt;CI(지속적 통합)&lt;/strong&gt; 과정에 대해 알아보았다.&lt;br /&gt;
하지만 테스트가 끝난 컨테이너를 실제 쿠버네티스 클러스터에 배포하고 관리하는 것은 또 다른 영역이다.&lt;/p&gt;

&lt;p&gt;이를 해결하기 위해 &lt;strong&gt;ArgoCD&lt;/strong&gt;를 사용한다.&lt;br /&gt;
ArgoCD는 쿠버네티스를 위한 선언적 &lt;strong&gt;GitOps&lt;/strong&gt; 도구로, Git 리포지토리에 정의된 형상(Manifest)을 쿠버네티스 클러스터의 상태와 자동으로 동기화(Sync)해주는 
역할을 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-argocd-설치&quot;&gt;4.1. ArgoCD 설치&lt;/h2&gt;

&lt;p&gt;ArgoCD는 쿠버네티스 애플리케이션 형태(CRD, Controller, Server 등)로 설치된다.&lt;br /&gt;
가장 간편한 방법인 Helm을 사용하여 설치를 진행한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;헬름 리포지토리 추가 및 차트 다운로드&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# ArgoCD 헬름 리포지토리 추가&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo add argo https://argoproj.github.io/argo-helm
&lt;span class=&quot;s2&quot;&gt;&quot;argo&quot;&lt;/span&gt; has been added to your repositories

assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo update
Hang tight &lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;we grab the latest from your chart repositories...
...Successfully got an update from the &lt;span class=&quot;s2&quot;&gt;&quot;argo&quot;&lt;/span&gt; chart repository
...Successfully got an update from the &lt;span class=&quot;s2&quot;&gt;&quot;ingress-nginx&quot;&lt;/span&gt; chart repository
...Successfully got an update from the &lt;span class=&quot;s2&quot;&gt;&quot;metallb&quot;&lt;/span&gt; chart repository
...Successfully got an update from the &lt;span class=&quot;s2&quot;&gt;&quot;bitnami&quot;&lt;/span&gt; chart repository
Update Complete. ⎈Happy Helming!⎈

&lt;span class=&quot;c&quot;&gt;# 설치 가능한 ArgoCD 차트 버전 확인&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm search repo argo
NAME                      	CHART VERSION	APP VERSION  	DESCRIPTION
argo/argo                 	1.0.0        	v2.12.5      	A Helm chart &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;Argo Workflows
argo/argo-cd              	9.2.4        	v3.2.3       	A Helm chart &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;Argo CD, a declarative, GitOps...
argo/argo-ci              	1.0.0        	v1.0.0-alpha2	A Helm chart &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;Argo-CI
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;작업 디렉터리를 생성하고 차트를 다운로드하여 압축을 해제한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;app  ch04  ch05  ch06  ch09  ch10  ch11

assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;app
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;helm  metallb  nginx-ingress-controller

assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;argocd
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;argocd/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/argocd&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm pull argo/argo-cd
assu@myserver01:~/work/app/argocd&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;argo-cd-9.2.4.tgz

assu@myserver01:~/work/app/argocd&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tar &lt;/span&gt;xvfz argo-cd-9.2.4.tgz

assu@myserver01:~/work/app/argocd&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;argo-cd  argo-cd-9.2.4.tgz

assu@myserver01:~/work/app/argocd&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mv &lt;/span&gt;argo-cd argo-cd-9.2.4
assu@myserver01:~/work/app/argocd&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;argo-cd-9.2.4  argo-cd-9.2.4.tgz

assu@myserver01:~/work/app/argocd&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;argo-cd-9.2.4/
assu@myserver01:~/work/app/argocd/argo-cd-9.2.4&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Chart.lock  charts  Chart.yaml  README.md  templates  values.yaml

&lt;span class=&quot;c&quot;&gt;# 사용자 설정을 위한 values.yaml 복사&lt;/span&gt;
assu@myserver01:~/work/app/argocd/argo-cd-9.2.4&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp &lt;/span&gt;values.yaml my-values.yaml

assu@myserver01:~/work/app/argocd/argo-cd-9.2.4&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Chart.lock  charts  Chart.yaml  my-values.yaml  README.md  templates  values.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;ArgoCD 설치 및 서비스 노출&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ArgoCD 전용 네임스페이스를 생성하고 설치를 진행한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/argocd/argo-cd-9.2.4&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl create namespace myargocd
namespace/myargocd created

assu@myserver01:~/work/app/argocd/argo-cd-9.2.4&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get namespace
NAME               STATUS   AGE
calico-apiserver   Active   20d
calico-system      Active   20d
default            Active   20d
kube-node-lease    Active   20d
kube-public        Active   20d
kube-system        Active   20d
myargocd           Active   6s
mymetallb          Active   6d21h
mynginx            Active   7d1h
tigera-operator    Active   20d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/argocd/argo-cd-9.2.4&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; myargocd &lt;span class=&quot;nt&quot;&gt;--generate-name&lt;/span&gt; argo/argo-cd &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; my-values.yaml &lt;span class=&quot;nt&quot;&gt;--debug&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--v&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1

NOTES:
In order to access the server UI you have the following options:

1. kubectl port-forward service/argo-cd-1767430758-argocd-server &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; myargocd 8080:443

    and &lt;span class=&quot;k&quot;&gt;then &lt;/span&gt;open the browser on http://localhost:8080 and accept the certificate

2. &lt;span class=&quot;nb&quot;&gt;enable &lt;/span&gt;ingress &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;the values file &lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;server.ingress.enabled&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt; and either
      - Add the annotation &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;ssl passthrough: https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/#option-1-ssl-passthrough
      - Set the &lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt;configs.params.&lt;span class=&quot;s2&quot;&gt;&quot;server.insecure&quot;&lt;/span&gt;&lt;span class=&quot;sb&quot;&gt;`&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;the values file and terminate SSL at your ingress: https://argo-cd.readthedocs.io/en/stable/operator-manual/ingress/#option-2-multiple-ingress-objects-and-hosts


After reaching the UI the first &lt;span class=&quot;nb&quot;&gt;time &lt;/span&gt;you can login with username: admin and the random password generated during the installation. You can find the password by running:

kubectl &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; myargocd get secret argocd-initial-admin-secret &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;{.data.password}&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;base64&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;

&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;You should delete the initial secret afterwards as suggested by the Getting Started Guide: https://argo-cd.readthedocs.io/en/stable/getting_started/#4-login-using-the-cli&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;설치가 완료된 후 실행되고 있는 리소스들을 확인해본다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/argocd/argo-cd-9.2.4&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; myargocd
NAME                                                                  READY   STATUS    RESTARTS   AGE
pod/argo-cd-1767430758-argocd-application-controller-0                1/1     Running   0          6m37s
pod/argo-cd-1767430758-argocd-applicationset-controller-5bd64bxlvwb   1/1     Running   0          6m37s
pod/argo-cd-1767430758-argocd-dex-server-6d7db8885d-gl8dh             1/1     Running   0          6m37s
pod/argo-cd-1767430758-argocd-notifications-controller-5fd5d7fskd42   1/1     Running   0          6m37s
pod/argo-cd-1767430758-argocd-redis-59f6cfd96f-jn95x                  1/1     Running   0          6m37s
pod/argo-cd-1767430758-argocd-repo-server-d69d5f468-kwrrd             1/1     Running   0          6m37s
pod/argo-cd-1767430758-argocd-server-6b75d9fdcd-tnfpl                 1/1     Running   0          6m37s


NAME                                                          TYPE        CLUSTER-IP       EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;             AGE
service/argo-cd-1767430758-argocd-applicationset-controller   ClusterIP   10.102.4.107     &amp;lt;none&amp;gt;        7000/TCP            6m37s
service/argo-cd-1767430758-argocd-dex-server                  ClusterIP   10.111.156.0     &amp;lt;none&amp;gt;        5556/TCP,5557/TCP   6m37s
service/argo-cd-1767430758-argocd-redis                       ClusterIP   10.106.109.139   &amp;lt;none&amp;gt;        6379/TCP            6m37s
service/argo-cd-1767430758-argocd-repo-server                 ClusterIP   10.104.152.234   &amp;lt;none&amp;gt;        8081/TCP            6m37s
service/argo-cd-1767430758-argocd-server                      ClusterIP   10.102.54.141    &amp;lt;none&amp;gt;        80/TCP,443/TCP      6m37s

NAME                                                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/argo-cd-1767430758-argocd-applicationset-controller   1/1     1            1           6m37s
deployment.apps/argo-cd-1767430758-argocd-dex-server                  1/1     1            1           6m37s
deployment.apps/argo-cd-1767430758-argocd-notifications-controller    1/1     1            1           6m37s
deployment.apps/argo-cd-1767430758-argocd-redis                       1/1     1            1           6m37s
deployment.apps/argo-cd-1767430758-argocd-repo-server                 1/1     1            1           6m37s
deployment.apps/argo-cd-1767430758-argocd-server                      1/1     1            1           6m37s

NAME                                                                             DESIRED   CURRENT   READY   AGE
replicaset.apps/argo-cd-1767430758-argocd-applicationset-controller-5bd64bd8c8   1         1         1       6m37s
replicaset.apps/argo-cd-1767430758-argocd-dex-server-6d7db8885d                  1         1         1       6m37s
replicaset.apps/argo-cd-1767430758-argocd-notifications-controller-5fd5d7ffdf    1         1         1       6m37s
replicaset.apps/argo-cd-1767430758-argocd-redis-59f6cfd96f                       1         1         1       6m37s
replicaset.apps/argo-cd-1767430758-argocd-repo-server-d69d5f468                  1         1         1       6m37s
replicaset.apps/argo-cd-1767430758-argocd-server-6b75d9fdcd                      1         1         1       6m37s

NAME                                                                READY   AGE
statefulset.apps/argo-cd-1767430758-argocd-application-controller   1/1     6m37s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;파드와 서비스를 확인해보면 &lt;em&gt;argocd-server&lt;/em&gt; 서비스가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterIP&lt;/code&gt; 타입으로 생성된 것을 볼 수 있다.&lt;br /&gt;
외부 (웹 브라우저)에서 ArgoCD UI에 접속하기 위해 이를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadBalancer&lt;/code&gt; 타입으로 변경한다.&lt;/p&gt;

&lt;p&gt;이 때 &lt;strong&gt;MetalLB&lt;/strong&gt; 가 사용된다.&lt;br /&gt;
일반적인 베어메탈(온프레미스) 환경이나 VM에서는 서비스 타입을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadBalancer&lt;/code&gt;로 변경해도 외부 IP(External-IP)가 할당되지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;pending&amp;gt;&lt;/code&gt; 상태로 남는다.&lt;br /&gt;
하지만 사전에 설치해 둔 &lt;strong&gt;MetalLB&lt;/strong&gt;가 이 요청을 감지하고, 설정된 IP 풀에서 사용 가능한 IP(예: 10.0.2.21)를 할당해준다.&lt;br /&gt;
즉, MetalLB 덕분에 로드밸런서 IP를 통해 ArgoCD에 접속할 수 있게 되는 것이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 서비스 타입을 ClusterIP에서 LoadBalancer로 변경&lt;/span&gt;
assu@myserver01:~/work/app/argocd/argo-cd-9.2.4&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl patch svc argo-cd-1767430758-argocd-server &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; myargocd &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{&quot;spec&quot;: {&quot;type&quot;: &quot;LoadBalancer&quot;}}&apos;&lt;/span&gt;
service/argo-cd-1767430758-argocd-server patched

&lt;span class=&quot;c&quot;&gt;# 변경 결과 확인 (EXTERNAL-IP가 할당되었는지 확인)&lt;/span&gt;
assu@myserver01:~/work/app/argocd/argo-cd-9.2.4&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get svc &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; myargocd
NAME                                                  TYPE           CLUSTER-IP       EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                      AGE
argo-cd-1767430758-argocd-applicationset-controller   ClusterIP      10.102.4.107     &amp;lt;none&amp;gt;        7000/TCP                     23m
argo-cd-1767430758-argocd-dex-server                  ClusterIP      10.111.156.0     &amp;lt;none&amp;gt;        5556/TCP,5557/TCP            23m
argo-cd-1767430758-argocd-redis                       ClusterIP      10.106.109.139   &amp;lt;none&amp;gt;        6379/TCP                     23m
argo-cd-1767430758-argocd-repo-server                 ClusterIP      10.104.152.234   &amp;lt;none&amp;gt;        8081/TCP                     23m
argo-cd-1767430758-argocd-server                      LoadBalancer   10.102.54.141    10.0.2.21     80:32137/TCP,443:30346/TCP   23m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;ArgoCD 접속&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;초기 관리자(admin) 비밀번호를 확인한다.&lt;br /&gt;
출력된 결과를 이용하여 argocd에 접속할 예정이니 잘 메모해두어야 한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/argocd/argo-cd-9.2.4&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; myargocd get secret argocd-initial-admin-secret &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;{.data.password}&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;base64&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;
MLgJ3DVNB3JigJKc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;VM 환경인 경우 호스트 OS에서 접속하기 위해 포트포워딩 설정이 필요하다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/port.png&quot; alt=&quot;argocd 접속을 위한 포트포워딩&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이제 웹 브라우저에서 127.0.0.1:2001 로 접속하여 로그인한다.&lt;br /&gt;
처음에 신뢰할 수 없는 사이트라는 경고가 뜨는데 무시하고 접속한다.&lt;br /&gt;
아이디는 admin이고 비밀번호는 위에서 확인한 비밀번호이다.&lt;/p&gt;

&lt;p&gt;로그인이 성공하면 아래와 같은 argocd 첫 화면을 볼 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/argocd.png&quot; alt=&quot;ArgoCD 접속 화면&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;42-argocd를-활용한-깃허브-실습&quot;&gt;4.2. ArgoCD를 활용한 깃허브 실습&lt;/h2&gt;

&lt;p&gt;이제 GitOps 방식을 통해 실제 애플리케이션을 배포해본다. 깃허브에 &lt;em&gt;argocd-practice&lt;/em&gt; 리포지토리를 생성하고 매니페스트를 작성한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;421-배포용-매니페스트-작성-및-push&quot;&gt;4.2.1. 배포용 매니페스트 작성 및 Push&lt;/h3&gt;

&lt;p&gt;작업 디렉터리를 정리한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01
assu@myserver01:~/work/ch11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex02
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;디플로이먼트(Nginx 파드 3개를 띄우는 설정)와 서비스에 대한 매니페스트 작성 후 Git에 push 한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim deployment.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deployment&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deploy-test01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; 
        &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
          
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim service.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt; 
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Service&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-service&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ClusterIP&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;protocol&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;TCP&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git init
Initialized empty Git repository &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; /home/assu/work/ch11/ex02/.git/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;깃허브에 업로드하기 위해 리포지토리에 추가하고 업로드한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git remote add origin https://github.com/assu10/argocd-practice.git

assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git add &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;

assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git commit &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;argocd-practice&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;main &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;root-commit&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; 42c6730] argocd-practice
 2 files changed, 29 insertions&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;+&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
 create mode 100644 deployment.yml
 create mode 100644 service.yml
 
assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;git push &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; origin main
Enumerating objects: 4, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
Counting objects: 100% &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;4/4&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
Delta compression using up to 4 threads
Compressing objects: 100% &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;4/4&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
Writing objects: 100% &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;4/4&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, 564 bytes | 564.00 KiB/s, &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
Total 4 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;delta 0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, reused 0 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;delta 0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;, pack-reused 0
To https://github.com/assu10/argocd-practice.git
 &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;new branch]      main -&amp;gt; main
branch &lt;span class=&quot;s1&quot;&gt;&apos;main&apos;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;up to track &lt;span class=&quot;s1&quot;&gt;&apos;origin/main&apos;&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;깃허브 사이트에 들어가보면 해당 리포지토리에 해당 파일이 업로드된 것을 확인할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;422-argocd-애플리케이션-연동&quot;&gt;4.2.2. ArgoCD 애플리케이션 연동&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;리포지토리 연결&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;https://127.0.0.1:2001/ ArgoCD UI의 &lt;strong&gt;Settings &amp;gt; Repositories&lt;/strong&gt; 에서 &lt;strong&gt;CONNECT REPO&lt;/strong&gt; 버튼을 클릭한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/repo.png&quot; alt=&quot;리포지토리 정보 입력&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;애플리케이션 생성&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Applications &amp;gt; NEW APP&lt;/strong&gt; 를 클릭하여 아래 정보를 입력한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/application1.png&quot; alt=&quot;애플리케이션 정보 입력 1&quot; /&gt;
&lt;img src=&quot;/assets/img/dev/2025/1231/application2.png&quot; alt=&quot;애플리케이션 정보 입력 2&quot; /&gt;
&lt;img src=&quot;/assets/img/dev/2025/1231/application3.png&quot; alt=&quot;애플리케이션 정보 입력 3&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Application Name: nginx-test02&lt;/li&gt;
  &lt;li&gt;Project: default&lt;/li&gt;
  &lt;li&gt;Sync Policy: Manual(수동 동기화)&lt;/li&gt;
  &lt;li&gt;Source: 연결할 리포지토리 URL, Path는 루트 경로인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.&lt;/code&gt;을 입력(원하는 디렉터리가 있으면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/디렉터리 이름&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;Destination: Cluster URL(https://kubernetes.default.svc), Namespace(nginx-argocd-test02)&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;배포(Sync)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;생성된 애플리케이션에서 &lt;strong&gt;SYNC&lt;/strong&gt; 버튼을 누르면 Git에 있는 매니페스트 내용을 바탕으로 쿠버네티스 리소스를 생성한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/sync1.png&quot; alt=&quot;싱크로나이즈 작업&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/result.png&quot; alt=&quot;배포 확인&quot; /&gt;&lt;/p&gt;

&lt;p&gt;화면 좌측부터 보자.&lt;/p&gt;

&lt;p&gt;첫 번째의 nginx-test02 는 argoCD 에서의 애플리케이션 이름이다.&lt;br /&gt;
두 번째의 web-service는 Service 의 이름이고, deploy-test01은 디플로이먼트의 이름이다.&lt;br /&gt;
세 번째 디플로이먼트와 연결된 deploy-test01-5b8c666f54 은 ReplicaSet이다.&lt;br /&gt;
네 번째 ReplicaSet에 연결된 3개의 파드가 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;423-배포-확인-및-상태-변경&quot;&gt;4.2.3. 배포 확인 및 상태 변경&lt;/h3&gt;

&lt;p&gt;터미널에서 실제로 리소스가 생성되었는지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get namespace
NAME                  STATUS   AGE
...
nginx-argocd-test02   Active   6m13s
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;해당 애플리케이션 설치를 위한 네임스페이스가 추가된 것을 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;그리고 해당 애플리케이션이 설치된 네임스페이스의 리소스를 검색하면 앞서 ArgoCD 화면에서 본 것과 동일하게 원활히 실행 중인 것을 알 수 있다.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; nginx-argocd-test02
NAME                                 READY   STATUS    RESTARTS   AGE
pod/deploy-test01-5b8c666f54-4jhb9   1/1     Running   0          7m14s
pod/deploy-test01-5b8c666f54-d7w42   1/1     Running   0          7m14s
pod/deploy-test01-5b8c666f54-tbchm   1/1     Running   0          7m14s

NAME                  TYPE        CLUSTER-IP      EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/web-service   ClusterIP   10.107.141.23   &amp;lt;none&amp;gt;        80/TCP    7m14s

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/deploy-test01   3/3     3            3           7m14s

NAME                                       DESIRED   CURRENT   READY   AGE
replicaset.apps/deploy-test01-5b8c666f54   3         3         3       7m14s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;GitOps의 강력함은 변경 사항 관리에서 드러난다.&lt;br /&gt;
ArgoCD UI 의 디플로이먼트인 deploy-test01을 클릭하면 LIVE MANIFEST 가 나오는데 여기서 EDIT 버튼을 클릭한다.&lt;/p&gt;

&lt;p&gt;그리고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.replicas&lt;/code&gt;를 3에서 1로 수정하고 저장하면, 즉시 클러스터에 반영되어 파드 개수가 줄어드는 것을 시각적으로 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;수정 전&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;progressDeadlineSeconds&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;600&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;수정 후&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;progressDeadlineSeconds&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;600&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그럼 아래와 같이 실행 중인 파드 개수가 한 개로 변한 것을 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/pod.png&quot; alt=&quot;파드 개수 변동 확인&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;이제 애플리케이션을 삭제한다.&lt;br /&gt;
ArgoCD는 기본적으로 Cascading Delete를 수행하므로, 생성되었던 파드와 서비스도 함께 정리된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1231/delete.png&quot; alt=&quot;애플리케이션 삭제&quot; /&gt;&lt;/p&gt;

&lt;p&gt;네임스페이스를 삭제하며 실습을 종료한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; nginx-argocd-test02
No resources found &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;nginx-argocd-test02 namespace.

assu@myserver01:~/work/ch11/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete namespace nginx-argocd-test02
namespace &lt;span class=&quot;s2&quot;&gt;&quot;nginx-argocd-test02&quot;&lt;/span&gt; deleted
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며..&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;GitHub Actions(CI)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;코드가 리포지토리에 푸시될 때 자동으로 도커 이미지를 빌드하고 테스트 컨테이너를 실행하여 코드의 정합성을 검증했다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;MetalLB&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;베어메탈 환경에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadBalancer&lt;/code&gt; 타입의 서비스를 사용할 수 있도록 IP를 할당해주어, ArgoCD 서버에 외부 접속을 가능하게 했다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ArgoCD(CD)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Git 리포지토리의 매니페스트를 쿠버네티스 클러스터와 동기화하여, 복잡한 배포 과정을 자동화하고 시각화했다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 장철원 저자의 &lt;strong&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/126115324&quot;&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/losskatsu/DockerKubernetes&quot;&gt;예제 코드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://argo-cd.readthedocs.io/en/stable/&quot;&gt;Doc:: ArgoCD&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/ko/actions&quot;&gt;Doc:: GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 31 Dec 2025 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/31/kubernetes-ci-cd-github-actions-argocd/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/31/kubernetes-ci-cd-github-actions-argocd/</guid>
        
        <category>devops</category>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>ci-cd</category>
        
        <category>github-actions</category>
        
        <category>argocd</category>
        
        <category>gitops</category>
        
        <category>docker</category>
        
        <category>metallb</category>
        
        <category>helm</category>
        
        <category>automation</category>
        
        <category>continuous-integration</category>
        
        <category>continuous-delivery</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Kubernetes - 쿠버네티스로 웹 서비스 배포(3): 인그레스를 활용한 Flask &amp; Django 동시 배포</title>
        <description>&lt;p&gt;&lt;a href=&quot;https://assu10.github.io/dev/2025/12/28/kubernetes-django-nginx-ingress-practice/&quot;&gt;Kubernetes - 쿠버네티스로 웹 서비스 배포(1): 인그레스를 활용한 Django &amp;amp; Nginx 배포&lt;/a&gt;와 
&lt;a href=&quot;https://assu10.github.io/dev/2025/12/29/kubernetes-flask-nginx-ingress-practice/&quot;&gt;Kubernetes - 쿠버네티스로 웹 서비스 배포(2): 인그레스를 활용한 Flask &amp;amp; Nginx 배포&lt;/a&gt; 
를 통해 각 애플리케이션을 쿠버네티스 클러스터 위에서 인그레스를 통해 외부로 노출하는 과정에 대해 알아보았다.&lt;/p&gt;

&lt;p&gt;이번 포스트에서는 쿠버네티스 인그레스의 가장 강력한 기능 중 하나인 &lt;strong&gt;단일 진입점을 통한 다중 서비스 라우팅(Path-based Routing)&lt;/strong&gt;을 구현해본다.&lt;br /&gt;
(두 개의 개별 서비스를 하나의 인그레스 리소스를 통해 통합 관리하고 라우팅하는 &lt;a href=&quot;https://assu10.github.io/dev/2025/05/27/fanout/&quot;&gt;Fan-out&lt;/a&gt; 패턴)&lt;br /&gt;
즉, 하나의 도메인(IP)에서 경로(path)에 따라 Django 서비스와 Flask 서비스로 분기 처리하는 아키텍처를 구성한다.&lt;br /&gt;
이는 MSA에서 매우 흔하게 사용되는 패턴이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-인그레스-팬아웃fan-out이란&quot;&gt;1. 인그레스 팬아웃(Fan-out)이란?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-인그레스-파일-생성&quot;&gt;2. 인그레스 파일 생성&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-배포-및-검증&quot;&gt;3. 배포 및 검증&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-전체-아키텍처-및-리소스-정리&quot;&gt;4. 전체 아키텍처 및 리소스 정리&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;개발 환경&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Guest OS: Ubuntu 24.04.2 LTS&lt;/li&gt;
  &lt;li&gt;Host OS: Mac Apple M3 Max&lt;/li&gt;
  &lt;li&gt;Memory: 48 GB&lt;/li&gt;
  &lt;li&gt;Kubernetes: v1.29.15&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-인그레스-팬아웃fan-out이란&quot;&gt;1. 인그레스 팬아웃(Fan-out)이란?&lt;/h1&gt;

&lt;p&gt;쿠버네티스 인그레스의 가장 강력한 기능 중 하나는 단일 IP 주소와 포트를 통해 여러 서비스로 트래픽을 분산시키는 &lt;strong&gt;팬아웃(Fan-out)&lt;/strong&gt; 구성이다.&lt;/p&gt;

&lt;p&gt;이전엔 Django와 Flask를 각각 별도의 인그레스로 배포했지만, 이번에는 하나의 인그레스 리소스에서 &lt;strong&gt;경로(path)&lt;/strong&gt;에 따라 &lt;em&gt;/test01&lt;/em&gt;은 Django로, 
&lt;em&gt;/test02&lt;/em&gt;는 Flask로 연결하는 &lt;strong&gt;경로 기반 라우팅(Path-based Routing)&lt;/strong&gt;을 구현한다.&lt;/p&gt;

&lt;p&gt;이는 MSA에서 게이트웨이를 구성하는 핵심 원리이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-인그레스-파일-생성&quot;&gt;2. 인그레스 파일 생성&lt;/h1&gt;

&lt;p&gt;가장 먼저 작업 디렉터리 구조를 정의하고 필요한 매니페스트 파일들을 준비한다.&lt;br /&gt;
기존에 작성했던 Django와 Flask의 디플로이먼트 및 서비스(Service) 파일은 그대로 재사용하되, 트래픽을 분기한 새로운 인그레스 파일을 작성한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;작업 디렉터리 준비 및 파일 복사&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;
/home/assu/work/ch10

&lt;span class=&quot;c&quot;&gt;# 작업 디렉터리 생성&lt;/span&gt;
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex03
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex03
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Django 디플로이먼트와 서비스 YAML 파일을 복사한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ..
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01  ex02  ex03
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex01
assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;django-deploy.yml  django-ingress.yml  django-service.yml  myDjango04  myNginx04
assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp &lt;/span&gt;django-deploy.yml django-service.yml  ../ex03
assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ../ex03
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;django-deploy.yml  django-service.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;비슷하게 Flask의 디플로이먼트와 서비스 파일을 ex03 디렉터리로 복사한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ../ex02
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;myFlask02  myNginx02f
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;myNginx02f/
assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;1  default.conf  Dockerfile  flask-deploy.yml  flask-ingress.yml  flask-service.yml
assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp &lt;/span&gt;flask-deploy.yml flask-service.yml ../../ex03
assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ../../ex03
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;django-deploy.yml  django-service.yml  flask-deploy.yml  flask-service.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;통합 인그레스 매니페스트 작성(django-flask-ingress.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;두 서비스를 동시에 제어할 인그레스 설정이다.&lt;br /&gt;
여기서 핵심은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;paths&lt;/code&gt; 설정과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rewrite-target&lt;/code&gt; 애너테이션이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim django-flask-ingress.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Ingress&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;django-flask-ingress&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# URL 재작성 설정: 캡처 그룹($2)을 사용하여 경로의 뒷부분만 서비스로 전달&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/$2&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# Django 서비스 라우팅 (/test01 로 시작하는 모든 요청)&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/test01(/|$)(.*)&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ImplementationSpecific&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;django-service&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# django 웹 서비스 이름&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# Flask 서비스 라우팅 (/test02 로 시작하는 모든 요청)&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/test02(/|$)(.*)&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ImplementationSpecific&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;flask-service&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# flask 웹 서비스 이름&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;Rewrite Target에 대한 내용은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/29/kubernetes-flask-nginx-ingress-practice/#61-%EB%A7%A4%EB%8B%88%ED%8E%98%EC%8A%A4%ED%8A%B8-%EC%9E%91%EC%84%B1url-rewriting&quot;&gt;6.1. 매니페스트 작성(URL Rewriting)&lt;/a&gt;을 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;인그레스 설정값에 대한 좀 더 상세한 설명은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-ingress-helm-metallb-baremetal-loadbalancer/#6-%EC%9D%B8%EA%B7%B8%EB%A0%88%EC%8A%A4%EB%A1%9C-%ED%95%98%EB%82%98%EC%9D%98-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%B0%B0%ED%8F%AC&quot;&gt;6. 인그레스로 하나의 서비스 배포&lt;/a&gt;
를 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-배포-및-검증&quot;&gt;3. 배포 및 검증&lt;/h1&gt;

&lt;p&gt;이제 준비된 리소스들을 쿠버네티스 클러스터에 배포한다.&lt;br /&gt;
순서는 일반적으로 &lt;strong&gt;디플로이먼트(파드 생성) → Service(네트워크 노출) → 인그레스(외부 라우팅)&lt;/strong&gt; 순으로 진행하는 것이 좋다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;디플로이먼트 및 서비스 배포&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ll
total 28
drwxrwxr-x 2 assu assu 4096 Jan  3 02:34 ./
drwxrwxr-x 5 assu assu 4096 Jan  3 02:20 ../
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt; 1 assu assu  510 Jan  3 02:22 django-deploy.yml
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt; 1 assu assu  644 Jan  3 02:34 django-flask-ingress.yml
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt; 1 assu assu  202 Jan  3 02:22 django-service.yml
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt; 1 assu assu  520 Jan  3 02:24 flask-deploy.yml
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt; 1 assu assu  207 Jan  3 02:24 flask-service.yml

&lt;span class=&quot;c&quot;&gt;# 디플로이먼트 배포&lt;/span&gt;
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-deploy.yml
deployment.apps/deploy-django created
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-deploy.yml
deployment.apps/deploy-flask created

assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
&lt;span class=&quot;c&quot;&gt;# django 파드와 flask 파드가 실행됨&lt;/span&gt;
NAME                                 READY   STATUS    RESTARTS   AGE
pod/deploy-django-77675fcbcf-g6dpb   2/2     Running   0          13s
pod/deploy-django-77675fcbcf-lh9gk   2/2     Running   0          13s
pod/deploy-django-77675fcbcf-lp2kc   2/2     Running   0          13s
pod/deploy-flask-b8ffb7c86-gz4mv     2/2     Running   0          6s
pod/deploy-flask-b8ffb7c86-lrrdw     2/2     Running   0          6s
pod/deploy-flask-b8ffb7c86-rrxdq     2/2     Running   0          6s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   7d21h

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/deploy-django   3/3     3            3           13s
deployment.apps/deploy-flask    3/3     3            3           6s

NAME                                       DESIRED   CURRENT   READY   AGE
replicaset.apps/deploy-django-77675fcbcf   3         3         3       13s
replicaset.apps/deploy-flask-b8ffb7c86     3         3         3       6s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 서비스 배포&lt;/span&gt;
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-service.yml
service/django-service created
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-service.yml
service/flask-service created

&lt;span class=&quot;c&quot;&gt;# 서비스들은 ClusterIP를 부여받음&lt;/span&gt;
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get svc
NAME             TYPE        CLUSTER-IP     EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
django-service   ClusterIP   10.98.235.44   &amp;lt;none&amp;gt;        80/TCP    10s
flask-service    ClusterIP   10.97.10.64    &amp;lt;none&amp;gt;        80/TCP    5s
kubernetes       ClusterIP   10.96.0.1      &amp;lt;none&amp;gt;        443/TCP   7d21h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 인그레스 배포&lt;/span&gt;
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-flask-ingress.yml
ingress.networking.k8s.io/django-flask-ingress created
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;배포 상태 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;모든 리소스가 정상적으로 생성되었는지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 실행 중인 리소스들 확인&lt;/span&gt;
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                                 READY   STATUS    RESTARTS   AGE
pod/deploy-django-77675fcbcf-g6dpb   2/2     Running   0          2m29s
pod/deploy-django-77675fcbcf-lh9gk   2/2     Running   0          2m29s
pod/deploy-django-77675fcbcf-lp2kc   2/2     Running   0          2m29s
pod/deploy-flask-b8ffb7c86-gz4mv     2/2     Running   0          2m22s
pod/deploy-flask-b8ffb7c86-lrrdw     2/2     Running   0          2m22s
pod/deploy-flask-b8ffb7c86-rrxdq     2/2     Running   0          2m22s

NAME                     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/django-service   ClusterIP   10.98.235.44   &amp;lt;none&amp;gt;        80/TCP    61s
service/flask-service    ClusterIP   10.97.10.64    &amp;lt;none&amp;gt;        80/TCP    56s
service/kubernetes       ClusterIP   10.96.0.1      &amp;lt;none&amp;gt;        443/TCP   7d21h

NAME                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/deploy-django   3/3     3            3           2m29s
deployment.apps/deploy-flask    3/3     3            3           2m22s

NAME                                       DESIRED   CURRENT   READY   AGE
replicaset.apps/deploy-django-77675fcbcf   3         3         3       2m29s
replicaset.apps/deploy-flask-b8ffb7c86     3         3         3       2m22s

&lt;span class=&quot;c&quot;&gt;# 실행 중인 인그레스 확인&lt;/span&gt;
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get ingress
NAME                   CLASS   HOSTS   ADDRESS   PORTS   AGE
django-flask-ingress   nginx   &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;                 80      25s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;접속 테스트&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;로컬 환경에서 브라우저를 통해 접속을 시도한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://127.0.0.1:2000/test01&quot;&gt;http://127.0.0.1:2000/test01&lt;/a&gt;: Django 환영 페이지 노출&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://127.0.0.1:2000/test02&quot;&gt;http://127.0.0.1:2000/test02&lt;/a&gt;: hello world!(Flask) 노출&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;하나의 도메인(여기서는 localhost)에서 경로만 바꾸어 서로 다른 기술 스택(Django, Flask)으로 만든 애플리케이션에 각각 접속되는 것을 확인할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-전체-아키텍처-및-리소스-정리&quot;&gt;4. 전체 아키텍처 및 리소스 정리&lt;/h1&gt;

&lt;p&gt;지금까지 진행한 내용의 전체 네트워크 흐름은 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1230/flow.png&quot; alt=&quot;전체적인 네트워크 흐름&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Client Request&lt;/strong&gt;: 사용자가 &lt;em&gt;/test01&lt;/em&gt; 또는 &lt;em&gt;/test02&lt;/em&gt; 경로로 요청을 보낸다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ingress Controller&lt;/strong&gt;: 요청 URL을 분석하여 규칙(Rule)에 정의된 서비스로 라우팅을 결정한다. 이 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Rewrite&lt;/code&gt; 규칙이 적용되어 URL 접두사가 제거된다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Service&lt;/strong&gt;: 선택된 서비스(Django service 또는 Flask service)는 트래픽을 적절한 파드로 부하 분산한다.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pod&lt;/strong&gt;: 최종적으로 컨테이너 내부의 애플리케이션이 요청을 처리하고 응답을 반환한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;리소스 정리&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;사용한 리소스를 정리하여 클러스터 자원을 확보한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-flask-ingress.yml
ingress.networking.k8s.io &lt;span class=&quot;s2&quot;&gt;&quot;django-flask-ingress&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-service.yml
service &lt;span class=&quot;s2&quot;&gt;&quot;django-service&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-service.yml
service &lt;span class=&quot;s2&quot;&gt;&quot;flask-service&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-deploy.yml
deployment.apps &lt;span class=&quot;s2&quot;&gt;&quot;deploy-django&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-deploy.yml
deployment.apps &lt;span class=&quot;s2&quot;&gt;&quot;deploy-flask&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get ingress
No resources found &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;default namespace.

assu@myserver01:~/work/ch10/ex03&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   7d21h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 장철원 저자의 &lt;strong&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/126115324&quot;&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/losskatsu/DockerKubernetes&quot;&gt;예제 코드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/ingress/&quot;&gt;Doc:: Ingress&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Tue, 30 Dec 2025 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/30/kubernetes-django-flask-ingress-integration/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/30/kubernetes-django-flask-ingress-integration/</guid>
        
        <category>devops</category>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>ingress</category>
        
        <category>django</category>
        
        <category>flask</category>
        
        <category>nginx</category>
        
        <category>microservices</category>
        
        <category>path-based-routing</category>
        
        <category>fanout</category>
        
        <category>load-balancing</category>
        
        <category>service-discovery</category>
        
        <category>url-rewriting</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Kubernetes - 쿠버네티스로 웹 서비스 배포(2): 인그레스를 활용한 Flask &amp; Nginx 배포</title>
        <description>&lt;p&gt;이번 포스트에서는 &lt;strong&gt;Flask&lt;/strong&gt;와 &lt;strong&gt;Nginx(웹 서버)&lt;/strong&gt;를 쿠버네티스 환경에 배포해보며, 인그레스를 활용하여 외부 트래픽을 안정적으로 처리하는
전체 과정을 실습해본다.&lt;/p&gt;

&lt;p&gt;도커 이미지를 빌드하는 것부터 디플로이먼트, 서비스, 그리고 최종적으로 인그레스를 설정하는 단계까지 포함한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-디렉터리-정리&quot;&gt;1. 디렉터리 정리&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-flask-이미지-빌드&quot;&gt;2. Flask 이미지 빌드&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-소스-코드-및-설정-확인&quot;&gt;2.1. 소스 코드 및 설정 확인&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-이미지-빌드-및-docker-hub-push&quot;&gt;2.2. 이미지 빌드 및 Docker Hub Push&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-nginx-이미지-빌드&quot;&gt;3. Nginx 이미지 빌드&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-nginx-설정-파일-변경&quot;&gt;3.1. Nginx 설정 파일 변경&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#32-이미지-빌드-및-docker-hub-push&quot;&gt;3.2. 이미지 빌드 및 Docker Hub Push&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-디플로이먼트deployment-실행&quot;&gt;4. 디플로이먼트(Deployment) 실행&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-매니페스트-작성sidecar-패턴-적용&quot;&gt;4.1. 매니페스트 작성(Sidecar 패턴 적용)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#42-배포-및-확인&quot;&gt;4.2. 배포 및 확인&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-서비스service-실행&quot;&gt;5. 서비스(Service) 실행&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#51-매니페스트-작성-및-포트-개념-정리&quot;&gt;5.1. 매니페스트 작성 및 포트 개념 정리&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#52-service-생성-및-확인&quot;&gt;5.2. Service 생성 및 확인&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-인그레스ingress-실행&quot;&gt;6. 인그레스(Ingress) 실행&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#61-매니페스트-작성url-rewriting&quot;&gt;6.1. 매니페스트 작성(URL Rewriting)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#62-인그레스-적용-및-접속-테스트&quot;&gt;6.2. 인그레스 적용 및 접속 테스트&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#63-전체-아키텍처-및-리소스-정리&quot;&gt;6.3. 전체 아키텍처 및 리소스 정리&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#정리하며&quot;&gt;정리하며..&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;개발 환경&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Guest OS: Ubuntu 24.04.2 LTS&lt;/li&gt;
  &lt;li&gt;Host OS: Mac Apple M3 Max&lt;/li&gt;
  &lt;li&gt;Memory: 48 GB&lt;/li&gt;
  &lt;li&gt;Kubernetes: v1.29.15&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-디렉터리-정리&quot;&gt;1. 디렉터리 정리&lt;/h1&gt;

&lt;p&gt;먼저 실습을 위한 작업 디렉터리를 구성한다. 기존에 작성했던 &lt;a href=&quot;https://assu10.github.io/dev/2025/11/29/docker-compose-flask-nginx-integration/#22-flask-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9C&quot;&gt;2.2. Flask 이미지 빌드&lt;/a&gt; 
를 재사용한다.&lt;/p&gt;

&lt;p&gt;기존 ch06/ex02 에 있던 Flask와 Nginx 관련 파일들을 이번 작업 디렉터리 경로인 ch10/ex02 로 복사한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 작업 디렉터리 생성&lt;/span&gt;
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex02
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01  ex02
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex02
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;
/home/assu/work/ch10/ex02

assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd
&lt;/span&gt;assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;work/ch06/ex02
assu@myserver01:~/work/ch06/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Flask02  myNginx02f

&lt;span class=&quot;c&quot;&gt;# 기존 소스 복사&lt;/span&gt;
assu@myserver01:~/work/ch06/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; ~/work/ch06/ex02/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; ~/work/ch10/ex02/

assu@myserver01:~/work/ch06/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd
&lt;/span&gt;assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;work/ch10/ex02
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Flask02  myNginx02f

&lt;span class=&quot;c&quot;&gt;# 디렉터리명 변경 (Flask02 -&amp;gt; myFlask02)&lt;/span&gt;
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mv &lt;/span&gt;Flask02 myFlask02
assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;myFlask02  myNginx02f
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-flask-이미지-빌드&quot;&gt;2. Flask 이미지 빌드&lt;/h1&gt;

&lt;p&gt;애플리케이션 계층을 담당할 Flask 이미지를 빌드한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-소스-코드-및-설정-확인&quot;&gt;2.1. 소스 코드 및 설정 확인&lt;/h2&gt;

&lt;p&gt;myFlask02 디렉터리는 아래와 같은 구조를 가지고 있다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;main.py&lt;/strong&gt;: Flask 애플리케이션 소스 코드&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;: 컨테이너 빌드 명세&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;requirements.txt&lt;/strong&gt;: 의존성 패키지 목록&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;myFlask02/
assu@myserver01:~/work/ch10/ex02/myFlask02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Dockerfile  myapp  requirements.txt

assu@myserver01:~/work/ch10/ex02/myFlask02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;myapp
assu@myserver01:~/work/ch10/ex02/myFlask02/myapp&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;main.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;main.py&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;flask&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Flask&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# flask 웹 애플리케이션 객체를 app이라고 지정
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Flask&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# 루트 경로에 접근
&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;@&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;/&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;hello_world&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;hello world!&apos;&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# 파이썬 스크립트가 실행되면 app.run을 통해 웹 애플리케이션 실행
# host=0.0.0.0은 모든 IP주소로부터 요청을 수락한다는 의미이고, port=8001은 8001번 포트를 사용한다는 의미
&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__name__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;__main__&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;0.0.0.0&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;8001&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;main.py 파일은 수정할 필요 없이 그대로 사용한다.&lt;/p&gt;

&lt;p&gt;이번엔 도커 파일을 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02/myFlask02/myapp&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ..
assu@myserver01:~/work/ch10/ex02/myFlask02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Dockerfile  myapp  requirements.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-dockerfile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; python:3.13.9&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /usr/src/app&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;COPY&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; .. .&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;python &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; pip &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--upgrade&lt;/span&gt; pip
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;pip &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; requirements.txt

&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; ./myapp&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Gunicorn을 사용하여 프로덕션 레벨의 WSGI 서버 실행&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;CMD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; gunicorn --bind 0.0.0.0:8001 main:app&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;EXPOSE&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; 8001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;도커 파일도 수정하지 않고 그대로 사용한다.&lt;/p&gt;

&lt;p&gt;마지막으로 requirements.txt 파일을 확인한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;requirements.txt&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;flask==3.1.2
gunicorn==23.0.0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;해당 파일 역시 수정할 부분은 따로 없다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-이미지-빌드-및-docker-hub-push&quot;&gt;2.2. 이미지 빌드 및 Docker Hub Push&lt;/h2&gt;

&lt;p&gt;이제 도커 이미지를 빌드한다. 태그는 0.2로 지정한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02/myFlask02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker image build &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; myflask_ch10:0.2
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;+] Building 2.4s &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;12/12&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; FINISHED                                                                                                           docker:default
...
 1 warning found &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;use docker &lt;span class=&quot;nt&quot;&gt;--debug&lt;/span&gt; to &lt;span class=&quot;nb&quot;&gt;expand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;:
 - JSONArgsRecommended: JSON arguments recommended &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;CMD to prevent unintended behavior related to OS signals &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;line 12&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
 
assu@myserver01:~/work/ch10/ex02/myFlask02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker image &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;REPOSITORY               TAG       IMAGE ID       CREATED       SIZE
myflask_ch10             0.2       2e8232aedc7f   3 weeks ago   1.15GB
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이미지 빌드가 완료되었다면, Kubernetes 클러스터가 이미지를 가져갈 수 있도록 원격 저장소인 Docker Hub에 업로드해야 한다.&lt;br /&gt;
이를 위해 리포지토리를 생성한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1229/repo.png&quot; alt=&quot;도커 허브 리포지토리 생성&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;로그인 및 푸시&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Docker Hub 로그인&lt;/span&gt;
assu@myserver01:~/work/ch10/ex02/myFlask02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker login
...
Login Succeeded
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;도커 이미지 업로드를 위해 tag 명령어를 활용하여 업로드용 이미지를 생성하고 해당 이미지를 도커 허브에 업로드한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 원격 저장소용 태그 생성 (사용자 ID/이미지명:태그)&lt;/span&gt;
assu@myserver01:~/work/ch10/ex02/myFlask02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker tag myflask_ch10:0.2 assu/myflask_ch10:0.2

&lt;span class=&quot;c&quot;&gt;# 이미지 푸시&lt;/span&gt;
assu@myserver01:~/work/ch10/ex02/myFlask02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker push assu/myflask_ch10:0.2
5f70bf18a086: Pushed
...
0.2: digest: sha256:94e9bba2a7b9358bf7f4bf00c8b864aabe050ebeac680b4c587cd6ff4b890194 size: 2838
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;도커 허브에 접속한 후 해당 리포지토리를 확인하면 이미지가 업로드된 것을 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1229/tag.png&quot; alt=&quot;업로드 확인&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-nginx-이미지-빌드&quot;&gt;3. Nginx 이미지 빌드&lt;/h1&gt;

&lt;p&gt;다음은 웹 서버 역할을 할 Nginx 이미지를 빌드한다.&lt;br /&gt;
이 단계에서 &lt;strong&gt;설정 파일의 변경&lt;/strong&gt;이 매우 중요하다.&lt;/p&gt;

&lt;p&gt;먼저 디렉터리를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;myFlask02  myNginx02f

assu@myserver01:~/work/ch10/ex02&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;myNginx02f/

assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;default.conf  Dockerfile
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-nginx-설정-파일-변경&quot;&gt;3.1. Nginx 설정 파일 변경&lt;/h2&gt;

&lt;p&gt;기존 설정 파일인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;default.conf&lt;/code&gt; 파일을 수정한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;수정 전(Docker 환경)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;upstream myweb{
    server flasktest:8001;
}

server{
    listen 81; # Nginx는 81번 포트로 요청을 받음
    server_name localhost;

    location /{
        proxy_pass http://myweb;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;기존에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;upstream&lt;/code&gt;을 사용하여 별도의 호스트(flasktest)로 트래픽을 보냈다.&lt;br /&gt;
이는 Nginx와 Flask가 서로 다른 컨테이너(또는 호스트)로 네트워크가 분리된 상태를 가정한 것이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;수정 후(Kubernetes 환경)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;server{
    listen 80; # Nginx는 80번 포트로 요청을 받음
    server_name localhost;

    location /{
        # 127.0.0.1 (Localhost)로 포워딩
        proxy_pass http://127.0.0.1:8001;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;왜 127.0.0.1 일까?&lt;/strong&gt;&lt;br /&gt;
여기서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;proxy_pass&lt;/code&gt; 주소를 &lt;em&gt;127.0.0.1:8001&lt;/em&gt;로 변경했다. 이는 &lt;strong&gt;Nginx 컨테이너와 Flask 컨테이너가 동일한 네트워크 네임스페이스를 공유&lt;/strong&gt;한다는 것을 의미한다.&lt;/p&gt;

&lt;p&gt;Kubernetes에서 &lt;strong&gt;파드&lt;/strong&gt;는 하나의 논리적인 호스트와 같아서, 파드 내부의 컨테이너들은 localhost를 통해 서로 통신할 수 있다.&lt;br /&gt;
즉, 우리는 나중에 배포 단계에서 &lt;strong&gt;하나의 파드 안에 Nginx와 Flask 컨테이너를 함께 배치(멀티 컨테이너 파드 패턴 혹은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/20/sidecar-pattern/&quot;&gt;Sidecar 패턴&lt;/a&gt;)&lt;/strong&gt;할 것임을 알 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;32-이미지-빌드-및-docker-hub-push&quot;&gt;3.2. 이미지 빌드 및 Docker Hub Push&lt;/h2&gt;

&lt;p&gt;변경된 설정을 포함하여 Nginx 이미지를 빌드하고 업로드한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 이미지 빌드&lt;/span&gt;
assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker image build &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; mynginxf_ch10:0.3
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;+] Building 2.2s &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;9/9&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; FINISHED                                                                                                             docker:default
... 

&lt;span class=&quot;c&quot;&gt;# 이미지 확인&lt;/span&gt;
assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker image &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;REPOSITORY               TAG       IMAGE ID       CREATED          SIZE
mynginxf_ch10            0.3       2a658d12231a   26 seconds ago   192MB
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이미지 업로드를 위해 도커 허브에 리포지토리를 추가한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1229/repo2.png&quot; alt=&quot;도커 허브 리포지토리 생성&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이제 도커 이미지를 도커 허브로 업로드하기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker tag&lt;/code&gt; 명령어로 업로드 이미지를 생성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 태그 생성 및 푸시&lt;/span&gt;
assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker tag mynginxf_ch10:0.3 assu/mynginxf_ch10:0.3

assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker push assu/mynginxf_ch10:0.3
5725d8228845: Pushed
...
0.3: digest: sha256:3ee610cb3e166f6cbbff90acd4d8a88a4d6906c2cf2c44603e100baf6409e3c3 size: 2192
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;도커 허브 페이지에 들어가면 이미지가 업로드된 것을 확인할 수 있다.&lt;br /&gt;
이제 Kubernetes 클러스터에 배포할 준비가 끝났다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1229/tag2.png&quot; alt=&quot;업로드 확인&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-디플로이먼트deployment-실행&quot;&gt;4. 디플로이먼트(Deployment) 실행&lt;/h1&gt;

&lt;p&gt;가장 먼저 애플리케이션의 Replica를 관리하고 배포를 담당하는 디플로이먼트를 생성한다.&lt;/p&gt;

&lt;p&gt;앞서 생성한 이미지를 활용해 디플로이먼트를 생성한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-매니페스트-작성sidecar-패턴-적용&quot;&gt;4.1. 매니페스트 작성(Sidecar 패턴 적용)&lt;/h2&gt;

&lt;p&gt;아래 파일에서 중요한 부분은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.containers&lt;/code&gt; 항목이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim flask-deploy.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deployment&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deploy-flask&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 파드를 3개로 유지하여 가용성 확보&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;flask-web-deploy&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 디플로이먼트가 관리할 파드 지정&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; 
        &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;flask-web-deploy&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 파드 라벨도 동일하게 지정&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# 첫 번째 컨테이너: Nginx (Reverse Proxy)&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-f&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;assu/mynginxf_ch10:0.3&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 도커 허브에서 가져올 이미지&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;containerPort&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# 두 번째 컨테이너: Flask (Application)&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;flask-web&lt;/span&gt; 
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;assu/myflask_ch10:0.2&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;containerPort&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Multi-container Pod 혹은 Sidecar 패턴이란?&lt;/strong&gt;&lt;br /&gt;
위 설정처럼 하나의 파드 안에 두 개 이상의 컨테이너를 정의한다. 이들은 &lt;strong&gt;동일한 네트워크 네임스페이스&lt;/strong&gt;를 공유한다.&lt;br /&gt;
따라서 Nginx가 localhost:8001 로 요청을 보내면, 같은 파드 내의 Flask 컨테이너가 이를 수신할 수 있게 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;42-배포-및-확인&quot;&gt;4.2. 배포 및 확인&lt;/h2&gt;

&lt;p&gt;작성한 YAML 파일을 클러스터에 적용한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-deploy.yml
deployment.apps/deploy-flask created
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;정상적으로 실행되었는지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                               READY   STATUS    RESTARTS   AGE
pod/deploy-flask-b8ffb7c86-bg6q5   2/2     Running   0          23s
pod/deploy-flask-b8ffb7c86-gj7tt   2/2     Running   0          23s
pod/deploy-flask-b8ffb7c86-zx6kq   2/2     Running   0          23s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   7d5h

NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/deploy-flask   3/3     3            3           23s

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/deploy-flask-b8ffb7c86   3         3         3       23s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;READY 2/2&lt;/code&gt; 표시는 파드 내의 두 컨테이너(Flask, Nginx)가 모두 정상적으로 실행 중임을 의미한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-서비스service-실행&quot;&gt;5. 서비스(Service) 실행&lt;/h1&gt;

&lt;p&gt;파드들은 동적으로 생성되고 IP가 변경될 수 있다. 따라서 파드 집합에 대한 단일 진입점을 제공하기 위해 &lt;strong&gt;Service&lt;/strong&gt;를 생성한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;51-매니페스트-작성-및-포트-개념-정리&quot;&gt;5.1. 매니페스트 작성 및 포트 개념 정리&lt;/h2&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim flask-service.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Service&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;flask-service&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;flask-web-deploy&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Deployment의 라벨과 일치해야 함&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ClusterIP&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 클러스터 내부에서만 접근 가능&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;protocol&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;TCP&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 서비스가 노출하는 포트&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;targetPort&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 파드(컨테이너)로 전달될 포트&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;port vs targetPort&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;port&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Service 자체의 포트&lt;/strong&gt;이다.&lt;/li&gt;
      &lt;li&gt;클러스터 내부의 다른 리소스들이 이 서비스에 접근할 때 사용하는 포트이다.&lt;/li&gt;
      &lt;li&gt;예: curl http://flask-service:80&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;targetPort&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;파드 내부 컨테이너의 포트&lt;/strong&gt;이다.&lt;/li&gt;
      &lt;li&gt;서비스가 요청을 받으면 이 포트로 트래픽을 토스한다.&lt;/li&gt;
      &lt;li&gt;여기서는 Nginx 컨테이너가 80번 포트를 열고 있으므로 80으로 설정했다.&lt;/li&gt;
      &lt;li&gt;만일 Nginx 없이 Flask(8001)로 직접 연결했다면 targetPort는 8001이 되어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;52-service-생성-및-확인&quot;&gt;5.2. Service 생성 및 확인&lt;/h2&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-service.yml
service/flask-service created

assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                               READY   STATUS    RESTARTS   AGE
pod/deploy-flask-b8ffb7c86-bg6q5   2/2     Running   0          3m55s
pod/deploy-flask-b8ffb7c86-gj7tt   2/2     Running   0          3m55s
pod/deploy-flask-b8ffb7c86-zx6kq   2/2     Running   0          3m55s

NAME                    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/flask-service   ClusterIP   10.101.8.233   &amp;lt;none&amp;gt;        80/TCP    4s
service/kubernetes      ClusterIP   10.96.0.1      &amp;lt;none&amp;gt;        443/TCP   7d5h

NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/deploy-flask   3/3     3            3           3m55s

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/deploy-flask-b8ffb7c86   3         3         3       3m55s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterIP&lt;/code&gt; 타입으로 생성되었으므로, 클러스터 내부 통신망이 구축되었다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-인그레스ingress-실행&quot;&gt;6. 인그레스(Ingress) 실행&lt;/h1&gt;

&lt;p&gt;이제 외부 사용자(Browser)가 접근할 수 있도록 L7 로드밸런서 역할을 하는 인그레스를 설정한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;61-매니페스트-작성url-rewriting&quot;&gt;6.1. 매니페스트 작성(URL Rewriting)&lt;/h2&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim flask-ingress.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Ingress&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;flask-ingress&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# [중요] 캡처 그룹($2)을 이용하여 경로 재작성&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/$2&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# 정규표현식을 사용하여 경로 매칭&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/test02(/|$)(.*)&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ImplementationSpecific&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;flask-service&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 연동할 서비스 지정&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rewrite Target&lt;/strong&gt;&lt;br /&gt;
사용자가 &lt;em&gt;http://domain/test02/hello&lt;/em&gt; 로 접근한다고 가정해보자.&lt;br /&gt;
Flask 애플리케이션은 보통 루트(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;)나 &lt;em&gt;/hello&lt;/em&gt; 경로를 기다린다. &lt;em&gt;/test02&lt;/em&gt; 라는 접두사는 인그레스 라우팅용이지 앱용이 아니다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;path: /test02(/|$)(.*)&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;URL을 분석하여 &lt;em&gt;/test02&lt;/em&gt; 뒤의 나머지 부분(&lt;em&gt;hello&lt;/em&gt;)을 두 번째 그룹 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(.*)&lt;/code&gt;으로 캡처한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rewrite-target: /$2&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;캡처한 그룹 (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$2&lt;/code&gt;)을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt; 뒤에 붙여서 서비스로 전달한다.&lt;/li&gt;
      &lt;li&gt;즉, &lt;em&gt;/test02/hello&lt;/em&gt; 요청은 &lt;em&gt;/hello&lt;/em&gt;로 바뀌어 파드로 전달된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;인그레스 설정값에 대한 좀 더 상세한 설명은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-ingress-helm-metallb-baremetal-loadbalancer/#6-%EC%9D%B8%EA%B7%B8%EB%A0%88%EC%8A%A4%EB%A1%9C-%ED%95%98%EB%82%98%EC%9D%98-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%B0%B0%ED%8F%AC&quot;&gt;6. 인그레스로 하나의 서비스 배포&lt;/a&gt;
를 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;62-인그레스-적용-및-접속-테스트&quot;&gt;6.2. 인그레스 적용 및 접속 테스트&lt;/h2&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-ingress.yml
ingress.networking.k8s.io/flask-ingress created

assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get ingress
NAME            CLASS   HOSTS   ADDRESS     PORTS   AGE
flask-ingress   nginx   &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;       10.0.2.20   80      4m47s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;웹 브라우저에서 &lt;a href=&quot;http://127.0.0.1:2000/test02&quot;&gt;http://127.0.0.1:2000/test02&lt;/a&gt;로 접속하면, Flask 앱이 반환하는 hello world! 메시지를 확인할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;63-전체-아키텍처-및-리소스-정리&quot;&gt;6.3. 전체 아키텍처 및 리소스 정리&lt;/h2&gt;

&lt;p&gt;지금까지 구성한 전체 네트워크 흐름을 시각화하면 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1229/flow.png&quot; alt=&quot;전체적인 네트워크 흐름&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;User&lt;/strong&gt;: 브라우저에서 &lt;em&gt;/test02&lt;/em&gt; 로 요청&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ingress&lt;/strong&gt;: &lt;em&gt;/test02&lt;/em&gt; 경로를 확인하고, Rewrite 규칙에 따라 경로를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;로 변경하여 flask-service로 전달&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Service&lt;/strong&gt;: flask-service는 연결된 파드 중 하나를 선택하여 트래픽 전달(LoadBalancing)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pod&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;strong&gt;Nginx(Container 1)&lt;/strong&gt;: 80번 포트로 요청을 받음. 설정(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;proxy_pass&lt;/code&gt;)에 따라 localhost:8001로 전달&lt;/li&gt;
      &lt;li&gt;&lt;strong&gt;Flask(Container 2)&lt;/strong&gt;: 8001번 포트에서 요청을 받아 hello world! 응답&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이제 리소스를 정리한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-ingress.yml
ingress.networking.k8s.io &lt;span class=&quot;s2&quot;&gt;&quot;flask-ingress&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-service.yml
service &lt;span class=&quot;s2&quot;&gt;&quot;flask-service&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; flask-deploy.yml
deployment.apps &lt;span class=&quot;s2&quot;&gt;&quot;deploy-flask&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch10/ex02/myNginx02f&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod,svc,ingress
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   7d5h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며..&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Sidecar 패턴&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;한 파드 내에 Nginx와 Flask를 두어 localhost 통신을 구현했다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Service Port&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;port&lt;/code&gt;(서비스 노출)와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;targetPort&lt;/code&gt;(컨테이너 수신)의 차이를 명확히 했다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ingress Rewrite&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;외부 노출 경로(/test02)와 내부 애플리케이션 경로(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;)의 차이를 극복하기 위해 Annotations를 활용했다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 장철원 저자의 &lt;strong&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/126115324&quot;&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/losskatsu/DockerKubernetes&quot;&gt;예제 코드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 29 Dec 2025 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/29/kubernetes-flask-nginx-ingress-practice/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/29/kubernetes-flask-nginx-ingress-practice/</guid>
        
        <category>devops</category>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>deployment</category>
        
        <category>service</category>
        
        <category>ingress</category>
        
        <category>sidecar-pattern</category>
        
        <category>clusterip</category>
        
        <category>port-forwarding</category>
        
        <category>nginx-ingress-controller</category>
        
        <category>path-rewrite</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Kubernetes - 쿠버네티스로 웹 서비스 배포(1): 인그레스를 활용한 Django &amp; Nginx 배포</title>
        <description>&lt;p&gt;여러 도메인을 라우팅해야 하거나, SSL/TLS 인증서를 통합 관리하고, 로드 밸런싱을 더 효율적으로 처리해야 할 때 &lt;strong&gt;인그레스(Ingress)&lt;/strong&gt;를 사용한다.&lt;/p&gt;

&lt;p&gt;이번 포스트에서는 &lt;strong&gt;Django(웹 프레임워크)&lt;/strong&gt;와 &lt;strong&gt;Nginx(웹 서버)&lt;/strong&gt;를 쿠버네티스 환경에 배포해보며, 인그레스를 활용하여 외부 트래픽을 안정적으로 처리하는 
전체 과정을 실습해본다.&lt;/p&gt;

&lt;p&gt;도커 이미지를 빌드하는 것부터 디플로이먼트, 서비스, 그리고 최종적으로 인그레스를 설정하는 단계까지 포함한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-사전-준비-사항&quot;&gt;1. 사전 준비 사항&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-인그레스ingress를-활용한-django-실행&quot;&gt;2. 인그레스(Ingress)를 활용한 django 실행&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-디렉터리-정리&quot;&gt;2.1. 디렉터리 정리&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#22-django-이미지-빌드&quot;&gt;2.2. Django 이미지 빌드&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#23-nginx-이미지-빌드&quot;&gt;2.3. Nginx 이미지 빌드&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#24-디플로이먼트deployment-실행&quot;&gt;2.4. 디플로이먼트(Deployment) 실행&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#25-서비스service-실행&quot;&gt;2.5. 서비스(Service) 실행&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#26-인그레스ingress-실행&quot;&gt;2.6. 인그레스(Ingress) 실행&lt;/a&gt;
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#261-파드-내부-접속-및-상태-확인&quot;&gt;2.6.1. 파드 내부 접속 및 상태 확인&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#262-전체-아키텍처-및-리소스-정리&quot;&gt;2.6.2. 전체 아키텍처 및 리소스 정리&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#정리하며&quot;&gt;정리하며..&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;개발 환경&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Guest OS: Ubuntu 24.04.2 LTS&lt;/li&gt;
  &lt;li&gt;Host OS: Mac Apple M3 Max&lt;/li&gt;
  &lt;li&gt;Memory: 48 GB&lt;/li&gt;
  &lt;li&gt;Kubernetes: v1.29.15&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-사전-준비-사항&quot;&gt;1. 사전 준비 사항&lt;/h1&gt;

&lt;p&gt;본격적인 실습에 앞서 몇 가지 준비가 필요하다.&lt;br /&gt;
이번 포스트에서는 우리가 만든 컨테이너 이미지를 &lt;strong&gt;Docker Hub&lt;/strong&gt;에 직접 업로드하고, 쿠버네티스 클러스터가 이를 내려받아 실행하는 구조를 가진다.&lt;/p&gt;

&lt;p&gt;따라서 &lt;a href=&quot;https://hub.docker.com&quot;&gt;Docker Hub&lt;/a&gt; 계정이 없다면 먼저 가입을 진행해야 한다.&lt;/p&gt;

&lt;p&gt;또한, Django 애플리케이션이 데이터를 저장할 데이터베이스인 PostgreSQL이 로컬 서버(Host)에서 정상적으로 실행 중인지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl status postgresql.service
● postgresql.service - PostgreSQL RDBMS
     Loaded: loaded &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;/usr/lib/systemd/system/postgresql.service&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; enabled&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; preset: enabled&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
     Active: active &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;exited&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; since Mon 2025-12-15 06:27:35 UTC&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; 2 weeks 3 days ago
   Main PID: 1459 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;exited, &lt;span class=&quot;nv&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;0/SUCCESS&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        CPU: 1ms
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이와 같이 상태가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;active&lt;/code&gt;인 것을 확인했다면 준비가 완료된 것이다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;PostgreSQL이 설치되어 있지 않다면, &lt;a href=&quot;https://assu10.github.io/dev/2025/11/16/docker-basics-and-commands-guide-2/#22-%EB%8F%84%EC%BB%A4-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1&quot;&gt;2.2. 도커 스토리지의 필요성&lt;/a&gt;을 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-인그레스ingress를-활용한-django-실행&quot;&gt;2. 인그레스(Ingress)를 활용한 django 실행&lt;/h1&gt;

&lt;p&gt;이제 인그레스를 활용하여 Django 애플리케이션을 외부로 서비스해본다.  전체적인 흐름은 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Django &amp;amp; Nginx 이미지 빌드&lt;/strong&gt;(Docker Hub에 업로드)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;디플로이먼트(Deployment) 생성&lt;/strong&gt;(Pod 실행)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;서비스(Service) 생성&lt;/strong&gt;(내부 네트워크 구성)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;인그레스(Ingress) 생성&lt;/strong&gt;(외부 트래픽 라우팅)&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-디렉터리-정리&quot;&gt;2.1. 디렉터리 정리&lt;/h2&gt;

&lt;p&gt;먼저 디렉터리를 생성하고 정리한다.&lt;br /&gt;
기존에 사용했던 Django 관련 파일을 재사용할 것이므로, &lt;a href=&quot;https://assu10.github.io/dev/2025/11/28/docker-compose-nginx-django-postgresql-integration/#22-django-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9Chost-ip-%EC%97%B0%EA%B2%B0&quot;&gt;2.2. Django 이미지 빌드(Host IP 연결)&lt;/a&gt; 
에서 사용했던 ch05/ex08 디렉터리를 복사하여 가져온다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;app  ch04  ch05  ch06  ch09

&lt;span class=&quot;c&quot;&gt;# 이번 실습을 위한 디렉터리 생성&lt;/span&gt;
assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ch10
assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ch10
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex01
assu@myserver01:~/work/ch10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex01
assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;
/home/assu/work/ch10/ex01
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch05/ex08&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;
/home/assu/work/ch05/ex08
assu@myserver01:~/work/ch05/ex08&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;myDjango04  myNginx04

&lt;span class=&quot;c&quot;&gt;# 기존 소스 복사&lt;/span&gt;
assu@myserver01:~/work/ch05/ex08&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; ./ /home/assu/work/ch10/ex01

assu@myserver01:~/work/ch05/ex08&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /home/assu/work/ch10/ex01
assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;myDjango04  myNginx04
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;22-django-이미지-빌드&quot;&gt;2.2. Django 이미지 빌드&lt;/h2&gt;

&lt;p&gt;Django 컨테이너가 로컬 호스트(Node)에 설치된 PostgreSQL 데이터베이스를 바라볼 수 있도록 설정을 변경하고, 새로운 이미지를 빌드한다.&lt;/p&gt;

&lt;p&gt;먼저 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;settings.py&lt;/code&gt; 파일이 있는 위치로 이동한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;myDjango04/myapp/myapp
assu@myserver01:~/work/ch10/ex01/myDjango04/myapp/myapp&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;asgi.py  __init__.py  __pycache__  settings.py  urls.py  wsgi.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01/myDjango04/myapp/myapp&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim settings.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DATABASES&lt;/code&gt; 항목의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HOST&lt;/code&gt; 정보를 수정한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;수정 전(Docker 환경)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;DATABASES&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;default&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;ENGINE&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;django.db.backends.postgresql&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;NAME&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;postgres&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;USER&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;postgres&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;PASSWORD&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;mysecretpassword&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;HOST&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;172.17.0.1&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# Docker Bridge Gateway IP
&lt;/span&gt;        &lt;span class=&quot;s&quot;&gt;&apos;PORT&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5432&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;수정 후(Kubernetes 환경)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;DATABASES&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;default&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;ENGINE&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;django.db.backends.postgresql&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;NAME&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;postgres&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;USER&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;postgres&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;PASSWORD&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;mysecretpassword&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;HOST&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;10.0.2.4&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# PostgreSQL이 설치된 Node의 실제 IP
&lt;/span&gt;        &lt;span class=&quot;s&quot;&gt;&apos;PORT&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5432&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;기존 Docker 환경에서는 172.17.0.1을 사용했지만, 쿠버네티스 환경(특히 Minikube나 VM 환경)에서는 노드의 실제 IP를 명시해주어야 파드에서 호스트의 DB로 
접근이 원활하다. 여기서는 myserver01의 IP인 10.0.2.4를 입력했다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Docker 환경과 k8s 환경에서의 네트워크 차이에 대한 내용은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/28/kubernetes-docker-k8s-network-difference-host-db-access/&quot;&gt;Docker vs k8s 네트워크 차이: Django에서 로컬 DB 연결 시 Host IP의 차이&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;이제 설정 변경을 완료했으므로 Docker 이미지를 빌드한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01/myDjango04/myapp/myapp&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ../..
assu@myserver01:~/work/ch10/ex01/myDjango04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Dockerfile  myapp  requirements.txt

&lt;span class=&quot;c&quot;&gt;# 이미지 빌드(Tag: 0.2)&lt;/span&gt;
assu@myserver01:~/work/ch10/ex01/myDjango04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker image build &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; mydjango_ch10:0.2
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;+] Building 9.7s &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;11/11&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; FINISHED                                                                                                           docker:default
...
 1 warning found &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;use docker &lt;span class=&quot;nt&quot;&gt;--debug&lt;/span&gt; to &lt;span class=&quot;nb&quot;&gt;expand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;:
 - JSONArgsRecommended: JSON arguments recommended &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;CMD to prevent unintended behavior related to OS signals &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;line 11&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

assu@myserver01:~/work/ch10/ex01/myDjango04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker image &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;REPOSITORY       TAG       IMAGE ID       CREATED         SIZE
mydjango_ch10    0.2       a5b8ae94299f   8 seconds ago   1.19GB
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이미지가 정상적으로 생성되었다. 이제 이 이미지를 쿠버네티스 클러스터가 가져갈 수 있도록 &lt;strong&gt;Docker Hub&lt;/strong&gt;에 업로드한다.&lt;/p&gt;

&lt;p&gt;먼저 웹 브라우저로 Docker Hub에 접속하여 &lt;em&gt;mydjango_ch10&lt;/em&gt; 이라는 이름의 리포지토리를 생성한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1228/docker_hub.png&quot; alt=&quot;django 리포지토리 생성&quot; /&gt;&lt;/p&gt;

&lt;p&gt;리포지토리 생성이 완료되었다면, 터미널에서 로그인을 진행한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01/myDjango04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker login
USING WEB-BASED LOGIN
...
WARNING! Your credentials are stored unencrypted &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;/home/assu/.docker/config.json&apos;&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/

Login Succeeded
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;로그인 성공 후, 생성한 이미지를 Docker Hub 계정 경로에 맞게 태그를 변경하고 Push 한다.&lt;br /&gt;
(assu 부분은 본인의 Docker Hub ID로 변경해야 합니다.)&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 도커 이미지 태그 생성(형식: &amp;lt;DockerHub_ID&amp;gt;/&amp;lt;Repo_Name&amp;gt;:&amp;lt;Tag&amp;gt;)&lt;/span&gt;
assu@myserver01:~/work/ch10/ex01/myDjango04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker tag mydjango_ch10:0.2 assu/mydjango_ch10:0.2

&lt;span class=&quot;c&quot;&gt;# 도커 허브에 업로드&lt;/span&gt;
assu@myserver01:~/work/ch10/ex01/myDjango04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker push assu/mydjango_ch10:0.2
The push refers to repository &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;docker.io/assu/mydjango_ch10]
5f70bf18a086: Pushed
...
0.2: digest: sha256:c6f8562dc49e19e246e40bb0f57d31b40dce153a8fccc035bffa2675ffc99e76 size: 2840
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker tag&lt;/code&gt; 명령은 기존 로컬 이미지를 가리키는 새로운 Alias를 만드는 과정이며, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker push&lt;/code&gt;를 통해 원격 저장소로 업로드된다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1228/tag2.png&quot; alt=&quot;도커 허브 업로드 확인&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;만일 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker push&lt;/code&gt; 시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;denied: requested access to the resource is denied&lt;/code&gt; 오류가 발생한다면, 로그인이 제대로 되지 않았거나 
리포지토리 권한 문제일 수 있습니다. 리포지토리 권한 문제일 경우 
&lt;a href=&quot;https://assu10.github.io/dev/2025/12/28/trouble-shooting/&quot;&gt;TroubleShooting - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker push&lt;/code&gt; 시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;denied: requested access to the resource is denied&lt;/code&gt;&lt;/a&gt; 을 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;23-nginx-이미지-빌드&quot;&gt;2.3. Nginx 이미지 빌드&lt;/h2&gt;

&lt;p&gt;Django 애플리케이션 앞단에서 리버스 프록시(Reverse Proxy) 역할을 수행할 Nginx 이미지를 빌드한다.&lt;/p&gt;

&lt;p&gt;먼저 Nginx 관련 파일이 있는 디렉터리로 이동한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01/myNginx04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;
/home/assu/work/ch10/ex01/myNginx04

assu@myserver01:~/work/ch10/ex01/myNginx04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;default.conf  Dockerfile
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;쿠버네티스 파드 환경에 맞게 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;default.conf&lt;/code&gt; 설정 파일을 수정한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01/myNginx04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim default.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;수정 전(Docker Compose 환경)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;upstream myweb{
        server djangotest:8000;
}

server{
        listen 80;
        server_name localhost;
        location /{
                proxy_pass http://myweb;
        }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;수정 후(Kubernetes &lt;a href=&quot;https://assu10.github.io/dev/2025/12/20/sidecar-pattern/&quot;&gt;Sidecar 패턴&lt;/a&gt; 환경)&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;server{
        listen 80;
        server_name localhost;
        location /{
                proxy_pass http://127.0.0.1:8000;
        }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;중요 변경 사항&lt;/strong&gt;&lt;br /&gt;
기존의 Docker Compose 환경에서는 컨테이너 간 통신 시 서비스 이름(예: &lt;em&gt;djangotest&lt;/em&gt;)을 호스트명으로 사용했었다.&lt;br /&gt;
하지만 이번 쿠버네티스 환경에서는 &lt;strong&gt;하나의 파드 안에 Nginx 컨테이너와 Django 컨테이너를 함께 배치&lt;/strong&gt;할 예정이다.&lt;/p&gt;

&lt;p&gt;동일한 파드 내의 컨테이너들은 네트워크 네임스페이스를 공유하므로 &lt;strong&gt;localhost&lt;/strong&gt;를 통해 서로 통신할 수 있다.&lt;br /&gt;
따라서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;upstream&lt;/code&gt; 설정 없이 바로 &lt;em&gt;http://127.0.0.1:8000&lt;/em&gt; 으로 패킷을 전달하도록 수정한다.&lt;/p&gt;

&lt;p&gt;이제 수정된 설정을 바탕으로 Nginx 도커 이미지를 빌드한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01/myNginx04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker image build &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; mynginxd_ch10:0.3
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;+] Building 2.1s &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;9/9&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; FINISHED                                                     docker:default
 &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;internal] load build definition from Dockerfile                                           0.0s
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이미지가 정상적으로 생성되었는지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01/myNginx04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker image &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;REPOSITORY               TAG       IMAGE ID       CREATED         SIZE
mynginxd_ch10            0.3       abb07383ce25   3 minutes ago   192MB
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;생성된 이미지를 Docker Hub에 올리기 위해 웹 브라우저에서 &lt;em&gt;mynginxd_ch10&lt;/em&gt; 리포지토리를 생성한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1228/dockerhub2.png&quot; alt=&quot;nginx 리포지토리 생성&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Django 이미지와 동일한 방식으로 태그를 생성하고 Push를 진행한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 태그 생성&lt;/span&gt;
assu@myserver01:~/work/ch10/ex01/myNginx04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker tag mynginxd_ch10:0.3 assu/mynginxd_ch10:0.3

&lt;span class=&quot;c&quot;&gt;# 도커 허브 업로드&lt;/span&gt;
assu@myserver01:~/work/ch10/ex01/myNginx04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker push assu/mynginxd_ch10:0.3
The push refers to repository &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;docker.io/assu/mynginxd_ch10]
98888136d0ec: Pushed
...
0.3: digest: sha256:de72045142edf66d03dcf150732ad9acad026fb278371e20d0a3923d2dfe4e4a size: 2192
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1228/tag1.png&quot; alt=&quot;도커 허브 업로드 확인&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;24-디플로이먼트deployment-실행&quot;&gt;2.4. 디플로이먼트(Deployment) 실행&lt;/h2&gt;

&lt;p&gt;이제 앞서 만든 두 개의 이미지(Django, Nginx)를 사용하여 실제 애플리케이션을 구동할 디플로이먼트를 정의한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;myDjango04  myNginx04
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim django-deploy.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deployment&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;deploy-django&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; 
        &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Nginx 컨테이너 (Reverse Proxy)&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-d&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;assu/mynginxd_ch10:0.3&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Docker Hub에 업로드한 이미지&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;containerPort&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# Django 컨테이너 (App)&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;django-web&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;assu/mydjango_ch10:0.2&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;containerPort&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 YAML 파일의 핵심은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.containers&lt;/code&gt; 항목에 두 개의 컨테이너가 정의되어 있다는 점이다.&lt;br /&gt;
이를 통해 &lt;strong&gt;멀티 컨테이너 파드&lt;/strong&gt;가 생성되며, 두 컨테이너는 항상 함께 스케줄링되고 동일한 IP와 포트 공간을 공유한다.&lt;/p&gt;

&lt;p&gt;이제 디플로이먼트를 실행한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-deploy.yml
deployment.apps/deploy-django created

assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pods
NAME                             READY   STATUS    RESTARTS   AGE
deploy-django-77675fcbcf-4rbhn   2/2     Running   0          72s
deploy-django-77675fcbcf-6f5gf   2/2     Running   0          72s
deploy-django-77675fcbcf-p5ptn   2/2     Running   0          72s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;READY&lt;/code&gt; 열이 &lt;em&gt;2/2&lt;/em&gt;로 표시되는 것은 파드 내의 두 컨테이너(Django, Nginx)가 모두 정상적으로 실행 중임을 의미한다.&lt;br /&gt;
도커 허브에서 이미지를 다운로드해야 하기 때문에 파드가 실행되기까지 다소 시간이 소요된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;25-서비스service-실행&quot;&gt;2.5. 서비스(Service) 실행&lt;/h2&gt;

&lt;p&gt;파드들이 정상적으로 실행되었으므로, 이들에 접근할 수 있는 단일 진입점인 서비스(Service)를 생성한다.&lt;br /&gt;
외부 노출을 인그레스가 담당할 예정이므로, 서비스 타입은 내부 통신용인 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/08/kubernetes-service-concept-and-types/#2-clusterip&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterIP&lt;/code&gt;&lt;/a&gt;를 사용한다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Service&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;django-service&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 서비스와 연결한 파드 설정, 디플로이먼트의 라벨과 일치해야 함&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ClusterIP&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;protocol&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;TCP&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# [서비스용] 클러스터 내부 다른 리소스(Ingress 등)가 접근할 포트&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;targetPort&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# [파드용] 파드 내 Nginx 컨테이너의 포트&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;서비스를 생성하고 상태를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-service.yml
service/django-service created

assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod,svc
NAME                                 READY   STATUS    RESTARTS   AGE
pod/deploy-django-77675fcbcf-4rbhn   2/2     Running   0          2m40s
pod/deploy-django-77675fcbcf-6f5gf   2/2     Running   0          2m40s
pod/deploy-django-77675fcbcf-p5ptn   2/2     Running   0          2m40s

NAME                     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/django-service   ClusterIP   10.98.94.163   &amp;lt;none&amp;gt;        80/TCP    5s
service/kubernetes       ClusterIP   10.96.0.1      &amp;lt;none&amp;gt;        443/TCP   7d1h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;service/django-service&lt;/em&gt; 서비스가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterIP&lt;/code&gt; 타입으로 생성되었으며, 클러스터 IP &lt;em&gt;10.98.94.163&lt;/em&gt; 을 할당받은 것을 확인할 수 있다.&lt;br /&gt;
이제 클러스터 내부에서는 이 IP를 통해 Django 앱(정확히는 Nginx)에 접근할 수 있게 되었다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;26-인그레스ingress-실행&quot;&gt;2.6. 인그레스(Ingress) 실행&lt;/h2&gt;

&lt;p&gt;이제 클러스터 외부에서 내부의 서비스로 접근할 수 있도록 &lt;strong&gt;인그레스&lt;/strong&gt;를 설정한다.&lt;br /&gt;
인그레스는 외부 요청(HTTP/HTTPS)을 클러스터 내부의 서비스(Service)로 라우팅해주는 규칙의 모음이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim django-ingress.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Ingress&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;django-ingress&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Nginx Ingress Controller가 /test01 경로를 제거하고 백엔드로 전달하도록 설정 (Rewrite)&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/$2&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; 
        &lt;span class=&quot;na&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# 정규표현식을 사용하여 /test01 뒤에 오는 모든 경로를 포착&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/test01(/|$)(.*)&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ImplementationSpecific&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;django-service&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 인그레스와 연동할 서비스 이름, 앞서 생성한 서비스 이름&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;인그레스 설정값에 대한 좀 더 상세한 설명은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-ingress-helm-metallb-baremetal-loadbalancer/#6-%EC%9D%B8%EA%B7%B8%EB%A0%88%EC%8A%A4%EB%A1%9C-%ED%95%98%EB%82%98%EC%9D%98-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%B0%B0%ED%8F%AC&quot;&gt;6. 인그레스로 하나의 서비스 배포&lt;/a&gt;
를 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;이제 인그레스 리소스를 생성하고 상태를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-ingress.yml
ingress.networking.k8s.io/django-ingress created

assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod,svc,ingress
NAME                                 READY   STATUS    RESTARTS   AGE
pod/deploy-django-77675fcbcf-4rbhn   2/2     Running   0          8m49s
pod/deploy-django-77675fcbcf-6f5gf   2/2     Running   0          8m49s
pod/deploy-django-77675fcbcf-p5ptn   2/2     Running   0          8m49s

NAME                     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/django-service   ClusterIP   10.98.94.163   &amp;lt;none&amp;gt;        80/TCP    6m14s
service/kubernetes       ClusterIP   10.96.0.1      &amp;lt;none&amp;gt;        443/TCP   7d1h

NAME                                       CLASS   HOSTS   ADDRESS   PORTS   AGE
ingress.networking.k8s.io/django-ingress   nginx   &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;                 80      28s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;django-ingress&lt;/em&gt;가 정상적으로 생성된 것을 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;이제 웹 브라우저를 통해 접속 테스트를 진행한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;접속 URL: &lt;a href=&quot;http://127.0.0.1:2000/test01&quot;&gt;http://127.0.0.1:2000/test01&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;브라우저에서 Django 환영 페이지가 보인다면 외부 요청이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ingress → Service → Pod(Nginx → Django)&lt;/code&gt; 순서로 정상적으로 도달한 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;261-파드-내부-접속-및-상태-확인&quot;&gt;2.6.1. 파드 내부 접속 및 상태 확인&lt;/h3&gt;

&lt;p&gt;정말 설정한 대로 파일들이 배치되어 있고, DB 연결이 잘 되는지 확인하기 위해 &lt;strong&gt;실행 중인 파드 내부&lt;/strong&gt;로 직접 들어가본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;1) Nginx 컨테이너 접속&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; pod/deploy-django-77675fcbcf-p5ptn &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /bin/bash
Defaulted container &lt;span class=&quot;s2&quot;&gt;&quot;nginx-d&quot;&lt;/span&gt; out of: nginx-d, django-web

root@deploy-django-77675fcbcf-p5ptn:/etc/nginx/conf.d# &lt;span class=&quot;nb&quot;&gt;cat &lt;/span&gt;default.conf
server&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
	listen 80&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
	server_name localhost&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
	location /&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
		proxy_pass http://127.0.0.1:8000&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
root@deploy-django-77675fcbcf-p5ptn:/etc/nginx/conf.d# &lt;span class=&quot;nb&quot;&gt;exit
exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl exec&lt;/code&gt; 명령어 실행 시 별도의 컨테이너 옵션을 주지 않으면, 쿠버네티스는 &lt;strong&gt;첫 번째 컨테이너(nginx-d)&lt;/strong&gt;로 접속시킨다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;default.conf&lt;/code&gt; 파일을 확인해보니 우리가 빌드한 대로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;proxy_pass&lt;/code&gt; 설정이 잘 들어가있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2) Django 컨테이너 접속(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-c&lt;/code&gt; 옵션 사용)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이번에는 같은 파드 내에 있는 &lt;strong&gt;Django 컨테이너&lt;/strong&gt;로 접속해본다.&lt;br /&gt;
멀티 컨테이너 파드에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-c&lt;/code&gt;(container) 옵션을 사용하여 접속할 대상을 명시해야 한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; pod/deploy-django-77675fcbcf-4rbhn &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; django-web &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /bin/bash
root@deploy-django-77675fcbcf-4rbhn:/usr/src/app/myapp# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;db.sqlite3  manage.py  myapp

&lt;span class=&quot;c&quot;&gt;# 데이터베이스 연결 확인&lt;/span&gt;
root@deploy-django-77675fcbcf-4rbhn:/usr/src/app/myapp# python manage.py inspectdb
...
from django.db import models
root@deploy-django-77675fcbcf-4rbhn:/usr/src/app/myapp# &lt;span class=&quot;nb&quot;&gt;exit
exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inspect&lt;/code&gt; 명령어가 에러 없이 실행되었다면, Django 컨테이너가 &lt;strong&gt;호스트(Node)에 설치된 PostgreSQL&lt;/strong&gt; 데이터베이스와 정상적으로 통신하고 있음을 의미한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;262-전체-아키텍처-및-리소스-정리&quot;&gt;2.6.2. 전체 아키텍처 및 리소스 정리&lt;/h3&gt;

&lt;p&gt;지금까지 진행한 내용의 전체 네트워크 흐름은 아래 그림과 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1228/flow.png&quot; alt=&quot;네트워크 흐름&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;User&lt;/strong&gt;: /test01 경로로 요청&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ingress&lt;/strong&gt;: 요청을 받아 &lt;em&gt;django-service&lt;/em&gt;로 라우팅(경로는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;로 Rewrite)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Service&lt;/strong&gt;: 적절한 파드를 찾아 트래픽 분산&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pod(Nginx)&lt;/strong&gt;: 80 포트로 요청을 받아 로컬호스트의 8000 포트로 전달&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Pod(Django)&lt;/strong&gt;: 요청을 처리하고 필요한 경우 노드(Host)의 DB에서 데이터를 조회&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이제 생성한 리소스를 정리한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-ingress.yml
ingress.networking.k8s.io &lt;span class=&quot;s2&quot;&gt;&quot;django-ingress&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-service.yml
service &lt;span class=&quot;s2&quot;&gt;&quot;django-service&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; django-deploy.yml
deployment.apps &lt;span class=&quot;s2&quot;&gt;&quot;deploy-django&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch10/ex01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod,svc,ingress
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   7d2h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며..&lt;/h1&gt;

&lt;p&gt;이번 포스트의 핵심 내용은 아래와 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Docker Hub 활용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;로컬 이미지가 아닌 Docker Hub에 업로드된 이미지를 사용하여 클러스터가 이미지를 받아오도록 구성하였다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Sidecar 패턴&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하나의 파드 안에 Django와 Nginx 컨테이너를 함께 배치하여 localhost 통신이 가능하도록 설계하였다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ingress 활용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;복잡한 외부 접속 설정을 Ingress 리소스 하나로 통합 관리하여 유연한 라우팅을 구현하였다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이러한 구조는 실제 환경에서도 매우 자주 사용되는 패턴이므로, 각 구성 요소(Pod, Service, Ingress)의 역할과 연결 고리를 확실히 이해해두는 것이 중요하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 장철원 저자의 &lt;strong&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/126115324&quot;&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/losskatsu/DockerKubernetes&quot;&gt;예제 코드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 28 Dec 2025 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/28/kubernetes-django-nginx-ingress-practice/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/28/kubernetes-django-nginx-ingress-practice/</guid>
        
        <category>devops</category>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>docker</category>
        
        <category>django</category>
        
        <category>nginx</category>
        
        <category>ingress</category>
        
        <category>sidecar-pattern</category>
        
        <category>deployment</category>
        
        <category>service</category>
        
        <category>docker-hub</category>
        
        <category>container-orchestration</category>
        
        <category>python</category>
        
        <category>web-server</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>TroubleShooting - `docker push` 시 `denied: requested access to the resource is denied`</title>
        <description>&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker push&lt;/code&gt; 시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;denied: requested access to the resource is denied&lt;/code&gt; 아래와 같은 오류가 발생할 때 가장 흔한 원인은
로컬 리눅스 계정 이름과 Docker Hub의 실제 계정 이름이 다르기 때문이다.&lt;/p&gt;

&lt;p&gt;현재 도커 허브에 로그인된 계정을 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01/myDjango04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker info | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;Username
 Username: real_id
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그리고 실제 도커 허브 아이디를 넣어서 진행한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch10/ex01/myDjango04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker tag mydjango_ch10:0.2 real_id/mydjango_ch10:0.2
assu@myserver01:~/work/ch10/ex01/myDjango04&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker push real_id/mydjango_ch10:0.2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Sun, 28 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/28/trouble-shooting/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/28/trouble-shooting/</guid>
        
        <category>trouble_shooting</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Docker vs k8s 네트워크 차이: Django에서 로컬 DB 연결 시 Host IP의 차이</title>
        <description>&lt;p&gt;&lt;a href=&quot;https://assu10.github.io/dev/2025/11/28/docker-compose-nginx-django-postgresql-integration/#22-django-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9Chost-ip-%EC%97%B0%EA%B2%B0&quot;&gt;2.2. Django 이미지 빌드(Host IP 연결)&lt;/a&gt; 와 
&lt;a href=&quot;https://assu10.github.io/dev/2025/12/28/kubernetes-django-nginx-ingress-practice/#22-django-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9C&quot;&gt;2.2. Django 이미지 빌드&lt;/a&gt; 를 보면 
Docker 환경과 k8s 환경에서의 로컬의 차이가 나온다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#docker-단독-실행-환경&quot;&gt;Docker 단독 실행 환경&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#kubernetes-환경&quot;&gt;Kubernetes 환경&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#host-변경-이유&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HOST&lt;/code&gt; 변경 이유&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#네트워크의-주체가-다르다docker0-vs-cni&quot;&gt;네트워크의 주체가 다르다.(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker0&lt;/code&gt; vs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CNI&lt;/code&gt;)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#로컬의-정의가-다르다&quot;&gt;‘로컬’의 정의가 다르다.&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#확장성을-고려한-표준-방식이다&quot;&gt;확장성을 고려한 표준 방식이다.&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#요약&quot;&gt;요약&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;Docker 환경에서 사용하던 Django 애플리케이션을 Kubernetes 환경에 배포하기 위해, 로컬(Node)에 설치된 PostgreSQL 데이터베이스를 바라보도록 설정을 수정한다.&lt;/p&gt;

&lt;h1 id=&quot;docker-단독-실행-환경&quot;&gt;Docker 단독 실행 환경&lt;/h1&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# ... 기존 코드 ...
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DATABASES&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;default&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# ... 생략 ...
&lt;/span&gt;        &lt;span class=&quot;s&quot;&gt;&apos;HOST&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;172.17.0.1&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# docker0 (Docker Host Gateway)
&lt;/span&gt;        &lt;span class=&quot;s&quot;&gt;&apos;PORT&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5432&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;kubernetes-환경&quot;&gt;Kubernetes 환경&lt;/h1&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# ... 기존 코드 ...
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DATABASES&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&apos;default&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;ENGINE&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;django.db.backends.postgresql&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;NAME&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;postgres&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;USER&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;postgres&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;PASSWORD&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;mysecretpassword&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;&apos;HOST&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;10.0.2.4&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# Node의 실제 LAN IP (혹은 VM의 eth0 IP)
&lt;/span&gt;        &lt;span class=&quot;s&quot;&gt;&apos;PORT&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5432&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;host-변경-이유&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HOST&lt;/code&gt; 변경 이유&lt;/h1&gt;

&lt;h2 id=&quot;네트워크의-주체가-다르다docker0-vs-cni&quot;&gt;네트워크의 주체가 다르다.(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker0&lt;/code&gt; vs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CNI&lt;/code&gt;)&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Docker에서도 로컬이었고, k8s에서도 로컬인데 왜 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HOST&lt;/code&gt; IP를 172.17.0.1 에서 10.0.2.4 로 변경했을까?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이는 Docker 컨테이너와 Kubernetes Pod의 &lt;strong&gt;네트워크 구조 차이&lt;/strong&gt; 때문이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Docker 환경(172.17.0.1)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docker 데몬은 기본적으로 호스트에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker0&lt;/code&gt; 이라는 가상 브리지 네트워크를 생성한다.&lt;br /&gt;
컨테이너 입장에서 호스트는 바로 이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker0&lt;/code&gt; 게이트웨이 너머에 있다. 그래서 172.17.0.1 로 접근하면 호스트의 프로세스(PostgreSQL)에 닿을 수 있다.&lt;br /&gt;
컨테이너 내부에서 172.17.0.1 은 호스트 머신(Gateway)을 가리키는 가장 확실한 경로이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes 환경(10.0.2.4)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes는 Pod 간 통신을 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker0&lt;/code&gt; 브리지를 사용하지 않고, 별도의 네트워크 플러그인인 CNI(Container Network Interface, 예: Calico, Flannel 등)를 
사용하여 자체적인 오버레이 네트워크(Pod 네트워크 구역)를 구성한다.&lt;/p&gt;

&lt;p&gt;Pod 내부에서 172.17.0.1(docker0)으로의 접근은 네트워크 정책이나 CNI 설정에 따라 막혀있거나, 라우팅되지 않을 수 있다.&lt;/p&gt;

&lt;p&gt;따라서 Pod(Kubernetes 내부)에서 Node(Kubernetes 외부, 즉 호스트 OS)의 프로세스(PostgreSQL)에 접근하기 위해서는 &lt;strong&gt;Node가 실제로 사용하고 있는 LAN IP(10.0.2.4)&lt;/strong&gt;를 
사용하여 명시적으로 밖으로 나가는 트래픽을 만들어야 한다.&lt;/p&gt;

&lt;p&gt;Pod는 가상 네트워크(Overlay Network) 안에 격리되어 있으므로, Node의 물리적 인터페이스(eth0)의 IP인 10.0.2.4를 타겟으로 해야 호스트에 설치된 PostgreSQL에 도달할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;로컬의-정의가-다르다&quot;&gt;‘로컬’의 정의가 다르다.&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;물리적 관점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Django 컨테이너와 PostgreSQL 모두 10.0.2.4(myserver01) &lt;strong&gt;같은 기계&lt;/strong&gt; 안에 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;네트워크 관점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;Kubernetes 환경에서 Pod 는 &lt;strong&gt;논리적으로 완전히 격리된 별도의 네트워크 섬&lt;/strong&gt;에 떠 있는 것과 같다.
        &lt;ul&gt;
          &lt;li&gt;Pod 입장에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt;는 Pod 자기 자신만을 의미한다.&lt;/li&gt;
          &lt;li&gt;Pod 입장에서 호스트(Node)는 ‘자신이 떠 있는 서버’라기보다는, &lt;strong&gt;네트워크를 타고 나가서 만나야 하는 외부 서버&lt;/strong&gt;처럼 취급된다.&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;확장성을-고려한-표준-방식이다&quot;&gt;확장성을 고려한 표준 방식이다.&lt;/h2&gt;

&lt;p&gt;Kubernetes는 기본적으로 &lt;strong&gt;클러스터(여러 대의 서버)&lt;/strong&gt; 환경을 가정한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;만일 나중에 Django Pod가 myserver01 이 아니라 워커 노드인 myserver02에 스케줄링되어 뜬다면?&lt;/li&gt;
  &lt;li&gt;이 때도 172.17.0.1 과 같은 내부 IP는 의미가 없어진다.&lt;/li&gt;
  &lt;li&gt;하지만 10.0.2.4(DB가 설치된 서버의 실제 IP)로 설정해두면, Pod가 어느 노드에 뜨든 상관없이 네트워크를 통해 정확히 DB를 찾아갈 수 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;요약&quot;&gt;요약&lt;/h1&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt; &lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Docker(ex08 디렉터리)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Kubernetes(ex01 디렉터리)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;네트워크 방식&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Docker Bridge(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker0&lt;/code&gt;)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;k8s CNI(Pod Network)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;Host 접근 IP&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;172.17.0.1(게이트웨이 IP)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;10.0.2.4(노드 실제 IP)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;비고&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;컨테이너가 호스트 브리지에 직접 연결됨&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Pod는 가상 네트워크 위에 있어 실제 IP로 통신함&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
</description>
        <pubDate>Sun, 28 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/28/kubernetes-docker-k8s-network-difference-host-db-access/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/28/kubernetes-docker-k8s-network-difference-host-db-access/</guid>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>docker-networking</category>
        
        <category>django</category>
        
        <category>postgresql</category>
        
        <category>host-access</category>
        
        <category>pod-communication</category>
        
        <category>overlay-network</category>
        
        <category>bridge-network</category>
        
        <category>trouble_shooting</category>
        
        <category>connection-refused</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Kubernetes - 배치 워크로드: Job,CronJob</title>
        <description>&lt;p&gt;쿠버네티스 대부분의 리소스(Deployment, ReplicaSet, DaemonSet 등)는 서비스가 &lt;strong&gt;중단 없이 계속 실행&lt;/strong&gt;되는 것에 초점을 맞춘다. 
웹 서버나 API 서버처럼 항상 요청을 기다려야 하는 애플리케이션이 이에 해당한다.&lt;/p&gt;

&lt;p&gt;하지만 데이터 백업, 로그 분석, 이메일 발송, 대용량 연산 등 &lt;strong&gt;특정 작업만 수행하고 완료되면 종료되어야 하는(Run-to-completion)&lt;/strong&gt; 워크로드는 어떻게 관리해야 할까?&lt;br /&gt;
계속 살아있으려고 노력하는 디플로이먼트를 사용한다면, 작업이 끝나고 프로세스가 종료될 때마다 쿠버네티스는 이를 ‘장애’로 판단하고 다시 시작시키려 할 것이다.&lt;/p&gt;

&lt;p&gt;이번 포스트에서는 이러한 배치 성격의 작업을 처리하기 위해 설계된 쿠버네티스의 컨트롤러인 &lt;strong&gt;잡(Job)&lt;/strong&gt;과, 이를 주기적으로 실행해주는 &lt;strong&gt;크론잡(CronJob)&lt;/strong&gt;에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-잡job&quot;&gt;1. 잡(Job)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#11-job-생성-및-실행&quot;&gt;1.1. Job 생성 및 실행&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-크론잡cronjob&quot;&gt;2. 크론잡(CronJob)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-cronjob-생성-및-실행&quot;&gt;2.1. CronJob 생성 및 실행&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#정리하며&quot;&gt;정리하며..&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;개발 환경&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Guest OS: Ubuntu 24.04.2 LTS&lt;/li&gt;
  &lt;li&gt;Host OS: Mac Apple M3 Max&lt;/li&gt;
  &lt;li&gt;Memory: 48 GB&lt;/li&gt;
  &lt;li&gt;Kubernetes: v1.29.15&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-잡job&quot;&gt;1. 잡(Job)&lt;/h1&gt;

&lt;p&gt;Job은 하나 이상의 파드를 생성하고, 지정된 수의 파드가 성공적으로 종료될 때까지 이를 관리하는 컨트롤러이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;핵심 로직&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;파드가 정상적으로 완료(exit code 0)되면 잡은 해당 작업을 성공으로 간주하고 기록한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;실패 처리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;파드가 실패하거나 노드 장애가 발생하면, Job은 새로운 파드를 생성하여 작업을 끝까지 완수하려고 시도한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;용도&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터베이스 마이그레이션, 일회성 스크립트 실행, 배치 처리 등&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;11-job-생성-및-실행&quot;&gt;1.1. Job 생성 및 실행&lt;/h2&gt;

&lt;p&gt;간단한 Nginx 이미지를 사용하여 Job을 생성해보자. Job은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;batch/v1&lt;/code&gt; API 그룹에 속한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;디렉터리 생성&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex15
assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex15
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Job 매니페스트 작성(job-test01.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim job-test01.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;batch/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Job&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;job-test01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-test01&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# 컨테이너가 시작되자마자 &quot;Hello world&quot;를 출력하고 종료되도록 설정&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;echo&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Hello&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;world&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# 중요: 재시작 정책, 잡에서는 Always를 사용할 수 없음 (OnFailure 또는 Never 사용)    &lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;restartPolicy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Never&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;backoffLimit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 잡 실행에 실패할 경우 재시도 횟수 (기본값: 6)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;restartPolicy&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;잡의 파드는 완료되는 것이 목표이므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Always&lt;/code&gt;(기본값)를 사용하면 안 된다.
        &lt;ul&gt;
          &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Never&lt;/code&gt;: 파드가 실패하면 새로운 파드 생성&lt;/li&gt;
          &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OnFailure&lt;/code&gt;: 파드가 실패하면 동일한 파드 내에서 컨테이너 재시작&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backoffLimit&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;작업 실패 시 최대 몇 번까지 재시도할지 설정&lt;/li&gt;
      &lt;li&gt;재시도 간격은 지수 단위(10s, 20s, 40s..)로 늘어난다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Job 실행 및 상태 확인&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; job-test01.yml
job.batch/job-test01 created

assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get job
NAME         COMPLETIONS   DURATION   AGE
job-test01   1/1           4m25s      5m42s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;COMPLETIONS&lt;/code&gt;가 1/1 이 되었다는 것은 작업이 성공적으로 끝났음을 의미한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Job 상세 정보 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl describe&lt;/code&gt; 를 통해 내부 동작을 살펴보자.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl describe job job-test01
Name:             job-test01
Namespace:        default
Selector:         batch.kubernetes.io/controller-uid&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;744a9b74-0d1f-4f0b-b254-76bdd7f2155e
Labels:           batch.kubernetes.io/controller-uid&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;744a9b74-0d1f-4f0b-b254-76bdd7f2155e
                  batch.kubernetes.io/job-name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;job-test01
                  controller-uid&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;744a9b74-0d1f-4f0b-b254-76bdd7f2155e
                  job-name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;job-test01
Annotations:      &amp;lt;none&amp;gt;
Parallelism:      1
Completions:      1
Completion Mode:  NonIndexed
...
Pods Statuses:    0 Active &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0 Ready&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; / 1 Succeeded / 0 Failed
...
Events:
  Type    Reason            Age   From            Message
  &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;    &lt;span class=&quot;nt&quot;&gt;------&lt;/span&gt;            &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;  &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;            &lt;span class=&quot;nt&quot;&gt;-------&lt;/span&gt;
  Normal  SuccessfulCreate  6m7s  job-controller  Created pod: job-test01-7xf2p
  Normal  Completed         102s  job-controller  Job completed
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이벤트 로그를 보면 컨트롤러가 파드를 생성하고(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SuccessfulCreate&lt;/code&gt;), 작업이 완료되자 Job을 완료 상태(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Completed&lt;/code&gt;)로 변경한 것을 알 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;실행된 파드의 로그 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Job이 완료되어도 파드는 삭제되지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Completed&lt;/code&gt; 상태로 남아있다. 이를 통해 로그나 결과를 확인할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod
NAME               READY   STATUS      RESTARTS   AGE
job-test01-7xf2p   0/1     Completed   0          6m39s

assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl logs job-test01-7xf2p
Hello world
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;리소스 정리&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Job을 삭제하면 관련 파드들도 함께 삭제된다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; job-test01.yml
job.batch &lt;span class=&quot;s2&quot;&gt;&quot;job-test01&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod
No resources found &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;default namespace.
assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get job
No resources found &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;default namespace.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-크론잡cronjob&quot;&gt;2. 크론잡(CronJob)&lt;/h1&gt;

&lt;p&gt;CronJob은 리눅스의 crontab과 유사하게 &lt;strong&gt;일정 주기마다 Job을 자동으로 생성&lt;/strong&gt;하는 컨트롤러이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;구조&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;CronJob → Job → Pod의 계층 구조&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;용도&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;매일 밤 데이터베이스 백업, 매시간 리포트 생성, 주기적인 캐시 청소 등&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-cronjob-생성-및-실행&quot;&gt;2.1. CronJob 생성 및 실행&lt;/h2&gt;

&lt;p&gt;1분마다 Hello World~ 를 출력하는 CronJob을 만들어본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;CronJob 매니페스트 작성(cronjob-test01.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim cronjob-test01.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;batch/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CronJob&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cronjob-test01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;schedule&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*/1&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*&quot;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 1분에 한 번씩 실행&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;jobTemplate&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 생성될 Job의 템플릿&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; 
        &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-test02&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/bin/sh&lt;/span&gt;
                &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;-c&lt;/span&gt;
                &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;echo Hello World~&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 텍스트 출력&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;restartPolicy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Never&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 재시작 정책&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;CronJob 실행 및 확인&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; cronjob-test01.yml
cronjob.batch/cronjob-test01 created

assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get cronjob
NAME             SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cronjob-test01   &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;/1 &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;   False     1        5s              6s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LAST SCHEDULE&lt;/code&gt;을 통해 마지막으로 언제 실행되었는지 확인할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;CronJob 실행 이력 및 관리&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;시간이 조금 지난 후 파드 상태를 확인해보면, 주기에 맞춰 생성된 여러 개의 파드를 볼 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod
NAME                            READY   STATUS      RESTARTS   AGE
cronjob-test01-29454240-mrjbc   0/1     Completed   0          116s
cronjob-test01-29454241-z7qbj   0/1     Completed   0          56s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;파드 이름 뒤의 숫자 29454240 는 작업이 생성된 시간을 의미한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;상세 정보 확인&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl describe cronjob cronjob-test01
Name:                          cronjob-test01
...
Successful Job History Limit:  3
Failed Job History Limit:      1
...
Events:
  Type    Reason            Age   From                Message
  &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;    &lt;span class=&quot;nt&quot;&gt;------&lt;/span&gt;            &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;  &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;                &lt;span class=&quot;nt&quot;&gt;-------&lt;/span&gt;
  Normal  SuccessfulCreate  71s   cronjob-controller  Created job cronjob-test01-29454240
  Normal  SawCompletedJob   66s   cronjob-controller  Saw completed job: cronjob-test01-29454240, status: Complete
  Normal  SuccessfulCreate  11s   cronjob-controller  Created job cronjob-test01-29454241
  Normal  SawCompletedJob   6s    cronjob-controller  Saw completed job: cronjob-test01-29454241, status: Complete
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;History Limit&lt;/code&gt;은 CronJob이 무한정 데이터를 쌓는 것을 방지하기 위해 이력을 관리한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Successful Job History Limit&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;성공한 Job을 몇 개까지 남길지 설정(기본값: 3)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Failed Job History Limit&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;실패한 Job을 몇 개까지 남길지 설정(기본값: 1)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;기본값에 따라 성공한 Job은 최근 3개까지만 파드와 Job 리소스가 유지되고, 오래된 것은 자동으로 삭제된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;로그 확인&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl logs cronjob-test01-29454240-mrjbc
Hello World~
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;리소스 정리&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; cronjob-test01.yml
cronjob.batch &lt;span class=&quot;s2&quot;&gt;&quot;cronjob-test01&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get cronjob
No resources found &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;default namespace.
assu@myserver01:~/work/ch09/ex15&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod
No resources found &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;default namespace.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며..&lt;/h1&gt;

&lt;p&gt;Job은 애플리케이션이 지정된 횟수만큼 성공적으로 완료되는 것을 보장한다.&lt;br /&gt;
일회성 배치 작업에 적합하며, 파드 실패 시 재시작 정책(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OnFailure&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Never&lt;/code&gt;)과 백오프(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;backoffLimit&lt;/code&gt;) 설정을 통해 안정성을 보장할 수 있다.&lt;/p&gt;

&lt;p&gt;CronJob은 Job을 주기적으로 생성하는 스케줄러 역할을 한다.&lt;br /&gt;
리눅스의 crontab 문법을 사용하며, 실행 이력(History) 관리를 통해 클러스터 리소스 낭비를 방지한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 장철원 저자의 &lt;strong&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/126115324&quot;&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/losskatsu/DockerKubernetes&quot;&gt;예제 코드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/job/&quot;&gt;Doc:: Job&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/&quot;&gt;Doc:: CronJob&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 27 Dec 2025 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/27/kubernetes-job-cronjob-batch-workload/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/27/kubernetes-job-cronjob-batch-workload/</guid>
        
        <category>devops</category>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>job</category>
        
        <category>cronjob</category>
        
        <category>batch-processing</category>
        
        <category>automation</category>
        
        <category>controller</category>
        
        <category>pod-lifecycle</category>
        
        <category>run-to-completion</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Kubernetes - MetalLB와 Ingress, Helm(헬름)</title>
        <description>&lt;p&gt;쿠버네티스 클러스터를 구축하고 애플리케이션을 배포했다면, 그 다음으로 마주하는 것은 &lt;strong&gt;외부의 사용자가 내부의 서비스에 어떻게 접근하게 만들 것인가?&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;AWS나 GCP 같은 퍼블릭 클라우드 환경에서는 LoadBalancer 타입의 서비스를 생성하면 자동으로 외부 IP가 할당되지만, 지금 실습하는 로컬 VM이나 
베어메탈(Bare-Metal) 환경에서는 이러한 자동화된 로드밸런서가 존재하지 않아 IP가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;pending&amp;gt;&lt;/code&gt; 상태로 남게 된다.&lt;/p&gt;

&lt;p&gt;이 포스트에서는 이러한 환경적 제약을 극복하고 서비스를 우아하게 노출하는 방법에 대해 알아본다.&lt;br /&gt;
쿠버네티스의 패키지 매니저인 &lt;strong&gt;헬름(Helm)&lt;/strong&gt;을 사용하여 &lt;strong&gt;Nginx Ingress Controller&lt;/strong&gt;를 설치하고, &lt;strong&gt;MetalLB&lt;/strong&gt;를 통해 베어메탈 환경에서도 LoadBalancer IP를 
할당받는 전체 과정을 실습해본다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;베어메탈(Bare Metal) 환경&lt;/strong&gt;&lt;/p&gt;

  &lt;p&gt;어떠한 소프트웨어(가상화 계층)도 거치지 않고 하드웨어 위에 운영체제를 직접 설치하여 사용하는 환경&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;최종적으로는 Ingress 설정을 통해 단일 IP로 들어오는 트래픽을 여러 서비스로 라우팅하는 L7 로드밸런싱을 구현해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-인그레스ingress-개념&quot;&gt;1. 인그레스(Ingress) 개념&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-헬름helm-개념&quot;&gt;2. 헬름(Helm) 개념&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-헬름-설치&quot;&gt;3. 헬름 설치&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-헬름으로-nginx-ingress-controller-설치&quot;&gt;4. 헬름으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nginx ingress controller&lt;/code&gt; 설치&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-metallb를-통한-베어메탈-loadbalancer-구성&quot;&gt;5. metalLB를 통한 베어메탈 LoadBalancer 구성&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#6-인그레스로-하나의-서비스-배포&quot;&gt;6. 인그레스로 하나의 서비스 배포&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#7-인그레스로-두-개의-서비스-배포&quot;&gt;7. 인그레스로 두 개의 서비스 배포&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;개발 환경&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Guest OS: Ubuntu 24.04.2 LTS&lt;/li&gt;
  &lt;li&gt;Host OS: Mac Apple M3 Max&lt;/li&gt;
  &lt;li&gt;Memory: 48 GB&lt;/li&gt;
  &lt;li&gt;Kubernetes Cluster: Custom Setup (Bare-metal style on VM)&lt;/li&gt;
  &lt;li&gt;Kubernetes: v1.29.15&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-인그레스ingress-개념&quot;&gt;1. 인그레스(Ingress) 개념&lt;/h1&gt;

&lt;p&gt;쿠버네티스에서 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/08/kubernetes-service-concept-and-types/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Service&lt;/code&gt; 오브젝트(LoadBalancer, NodePort)&lt;/a&gt;만으로도 외부 연결이 가능하다.&lt;br /&gt;
하지만 서비스가 늘어날 때마다 매번 로드밸런서를 생성하고 IP를 할당받는 것은 비용과 관리 측면에서 매우 비효율적이다.&lt;br /&gt;
이 때 필요한 것이 바로 &lt;strong&gt;인그레스(Ingress)&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;인그레스는 클러스터 외부의 HTTP/HTTPS 트래픽을 내부의 서비스로 라우팅하는 규칙(Rules)의 집합이다.&lt;br /&gt;
쉽게 말해서 ‘어떤 주소로 들어오면 어디로 보내라’는 교통 정리 역할을 하는 &lt;strong&gt;L7(Application Layer) 로드밸런서&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;단순히 IP와 포트만 보고 연결해주는 L4 로드밸런서(예: MetalLB 자체)와 달리, 인그레스는 &lt;strong&gt;URL 경로(Path)&lt;/strong&gt;나 &lt;strong&gt;도메인(Host)&lt;/strong&gt;을 이해한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;단일 진입점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클라이언트는 하나의 IP(인그레스 컨트롤러)로만 접근함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;라우팅 규칙&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;http://my-ip/test01 → &lt;strong&gt;Service A&lt;/strong&gt;로 전달&lt;/li&gt;
      &lt;li&gt;http://my-ip/test02 → &lt;strong&gt;Service B&lt;/strong&gt;로 전달&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이처럼 인그레스를 사용하면 단 하나의 로드밸런서 IP만으로도 수십 개의 웹 서비스를 경로 기반으로 나누어 서비스할 수 있어, 운영 효율성을 극대화할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1222/ingress.png&quot; alt=&quot;Ingress 개념&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-헬름helm-개념&quot;&gt;2. 헬름(Helm) 개념&lt;/h1&gt;

&lt;p&gt;쿠버네티스 애플리케이션을 배포하려면 Pod, Service, ConfigMap, Ingress 등 수많은 YAML 파일을 작성하고 관리해야 한다.&lt;br /&gt;
버전이 업데이트되거나 환경 변수를 바꿀 때마다 이 파일들을 일일이 수정하는 것은 어려운 작업이다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;헬름(Helm)&lt;/strong&gt;은 이러한 문제를 해결해 주는 쿠버네티스 패키지 매니저이다.
헬름을 활용하면 YAML 파일을 만들지 않고도 쿠버네티스 환경에서 애플리케이션을 쉽게 설치할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1222/helm.png&quot; alt=&quot;Helm 개념&quot; /&gt;&lt;/p&gt;

&lt;p&gt;리눅스의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apt&lt;/code&gt;나 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;yum&lt;/code&gt;이 패키지를 관리하듯, 헬름은 쿠버네티스 리소스를 패키지 단위로 관리한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;차트(Chart)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;쿠버네티스 리소스 파일들의 묶음(패키지)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;리포지토리(Repository)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;차트들이 저장된 원격 저장소(도커 허브와 유사)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;릴리스(Release)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;차트가 클러스터에 설치되어 실행 중인 인스턴스&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;values.yaml&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 설정을 변경할 수 있는 설정 파일&lt;/li&gt;
      &lt;li&gt;이를 통해 YAML을 직접 수정하지 않고도 이미지 버전이나 포트 등을 쉽게 변경 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;즉, 헬름을 통해 쿠버네티스 애플리케이션을 설치한다는 말은 리포지토리에서 헬름 차트를 다운로드하고 해당 디렉터리에 있는 파일을 수정하여 자신의 환경에 맞게 
최적화한 후 쿠버네티스 클러스터에 설치하는 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-헬름-설치&quot;&gt;3. 헬름 설치&lt;/h1&gt;

&lt;p&gt;실습을 위해 헬름을 설치해보자.&lt;br /&gt;
헬름 클라이언트는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl&lt;/code&gt; 명령을 내리는 마스터 노드에만 설치하면 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;설치 스크립트 실행&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;작업 디렉터리를 생성하고 공식 스크립트를 다운로드한다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;헬름을 설치하는 자세한 방법은 &lt;a href=&quot;https://helm.sh/docs/intro/install/&quot;&gt;Doc:: Installing Helm&lt;/a&gt; 을 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; app/helm &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;app/helm
assu@myserver01:~/work/app/helm&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;curl &lt;span class=&quot;nt&quot;&gt;-fsSL&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4
assu@myserver01:~/work/app/helm&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;get_helm.sh

assu@myserver01:~/work/app/helm&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod &lt;/span&gt;700 get_helm.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;스크립트를 실행하여 헬름을 설치한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
assu@myserver01:~/work/app/helm&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;./get_helm.sh
Downloading https://get.helm.sh/helm-v4.0.4-linux-arm64.tar.gz
Verifying checksum... Done.
Preparing to &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;helm into /usr/local/bin
&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt; password &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;assu:
helm installed into /usr/local/bin/helm

assu@myserver01:~/work/app/helm&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm version
version.BuildInfo&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;Version:&lt;span class=&quot;s2&quot;&gt;&quot;v4.0.4&quot;&lt;/span&gt;, GitCommit:&lt;span class=&quot;s2&quot;&gt;&quot;8650e1dad9e6ae38b41f60b712af9218a0d8cc11&quot;&lt;/span&gt;, GitTreeState:&lt;span class=&quot;s2&quot;&gt;&quot;clean&quot;&lt;/span&gt;, GoVersion:&lt;span class=&quot;s2&quot;&gt;&quot;go1.25.5&quot;&lt;/span&gt;, KubeClientVersion:&lt;span class=&quot;s2&quot;&gt;&quot;v1.34&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-헬름으로-nginx-ingress-controller-설치&quot;&gt;4. 헬름으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nginx ingress controller&lt;/code&gt; 설치&lt;/h1&gt;

&lt;p&gt;이제 헬름을 사용하여 인그레스 규칙을 실제로 수행할 구현체인 &lt;strong&gt;Nginx Ingress Controller&lt;/strong&gt;를 설치한다.&lt;br /&gt;
이전에는 복잡한 설정 파일을 직접 수정해야 했지만, 헬름을 사용하면 명령어 한 줄로 필요한 옵션을 적용하여 간편하게 설치할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;리포지토리 추가&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;먼저 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ingress-nginx&lt;/code&gt; 공식 헬름 리포지토리를 추가한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/helm&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
&lt;span class=&quot;s2&quot;&gt;&quot;ingress-nginx&quot;&lt;/span&gt; has been added to your repositories

assu@myserver01:~/work/app/helm&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo update
Hang tight &lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;we grab the latest from your chart repositories...
...Successfully got an update from the &lt;span class=&quot;s2&quot;&gt;&quot;ingress-nginx&quot;&lt;/span&gt; chart repository
Update Complete. ⎈Happy Helming!⎈
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;디렉터리 정리&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;
/home/assu/work/app
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;helm

assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;nginx-ingress-controller
assu@myserver01:~/work/app&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;nginx-ingress-controller/
assu@myserver01:~/work/app/nginx-ingress-controller&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;네임 스페이스 생성&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;인그레스 컨트롤러를 관리할 전용 네임스페이스(mynginx)를 생성한다. 전용 네임스페이스를 사용하면 다른 리소스와 섞이지 않아 관리가 용이하다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 현재 있는 네임스페이스 확인&lt;/span&gt;
assu@myserver01:~/work/app/nginx-ingress-controller/nginx-ingress-controller-12.0.7&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get namespace
NAME               STATUS   AGE
calico-apiserver   Active   13d
calico-system      Active   13d
default            Active   13d
kube-node-lease    Active   13d
kube-public        Active   13d
kube-system        Active   13d
tigera-operator    Active   13d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/nginx-ingress-controller/nginx-ingress-controller-12.0.7&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl create namespace mynginx
namespace/mynginx created

assu@myserver01:~/work/app/nginx-ingress-controller/nginx-ingress-controller-12.0.7&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get namespace
NAME               STATUS   AGE
calico-apiserver   Active   13d
calico-system      Active   13d
default            Active   13d
kube-node-lease    Active   13d
kube-public        Active   13d
kube-system        Active   13d
mynginx            Active   7s
tigera-operator    Active   13d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;헬름으로 설치 진행&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이제 Nginx Ingress Controller를 설치한다.&lt;/p&gt;

&lt;p&gt;여기서 중요한 옵션은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;controller.publishService.enabled=true&lt;/code&gt; 이다. 이 옵션은 인그레스 컨트롤러가 할당받은 외부 IP(LoadBalancer IP)를 인그레스 리소스의 
status 필드에 업데이트하도록 하여, 트래픽이 올바르게 라우팅되도록 돕는다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/nginx-ingress-controller/nginx-ingress-controller-12.0.7&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nginx-ingress-controller ingress-nginx/ingress-nginx &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mynginx &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--set&lt;/span&gt; controller.publishService.enabled&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true

&lt;/span&gt;NAME: nginx-ingress-controller-1766819417
LAST DEPLOYED: Sat Dec 27 07:10:23 2025
NAMESPACE: mynginx
STATUS: deployed
REVISION: 1
DESCRIPTION: Install &lt;span class=&quot;nb&quot;&gt;complete
&lt;/span&gt;TEST SUITE: None
NOTES:
CHART NAME: nginx-ingress-controller
CHART VERSION: 12.0.7
APP VERSION: 1.13.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ingress-nginx/ingress-nginx&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;설치할 차트 이름(리포지토리/차트명)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--namespace mynginx&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;설치할 네임스페이스 지정&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--set controller.publishService.enabled=true&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;values.yaml&lt;/code&gt;을 직접 수정하지 않고, 설치 시점에 동적으로 설정 주입&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;설치 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;설치가 완료되면 파드와 서비스가 정상적으로 생성되었는지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# nginx-ingress-controller를 mynginx 네임스페이스에 설치했기 때문에 아무것도 안 나온다.&lt;/span&gt;
assu@myserver01:~/work/app/nginx-ingress-controller/nginx-ingress-controller-12.0.7&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;NAME	NAMESPACE	REVISION	UPDATED	STATUS	CHART	APP VERSION

&lt;span class=&quot;c&quot;&gt;# --namespace를 통해 네임스페이스를 지정해주면 설치가 된 것을 확인할 수 있다.&lt;/span&gt;
assu@myserver01:~/work/app/nginx-ingress-controller/nginx-ingress-controller-12.0.7&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm &lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mynginx
NAME                    	NAMESPACE	REVISION	UPDATED                                	STATUS  	CHART               	APP VERSION
nginx-ingress-controller	mynginx  	1       	2025-12-28 05:32:02.781702412 +0000 UTC	deployed	ingress-nginx-4.14.1	1.14.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 실행 중인 쿠버네티스 오브젝트를 확인해도 nginx-ingress-controller 는 확인할 수 없음. mynginx 네임스페이스를 지정해주어야 함&lt;/span&gt;
assu@myserver01:~/work/app/nginx-ingress-controller/nginx-ingress-controller-12.0.7&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE   SELECTOR
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   26h   &amp;lt;none&amp;gt;

&lt;span class=&quot;c&quot;&gt;# 네임스페이스 리스트 확인&lt;/span&gt;
assu@myserver01:~/work/app/nginx-ingress-controller/nginx-ingress-controller-12.0.7&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get namespace
NAME               STATUS   AGE
calico-apiserver   Active   13d
calico-system      Active   13d
default            Active   13d
kube-node-lease    Active   13d
kube-public        Active   13d
kube-system        Active   13d
mynginx            Active   45m
tigera-operator    Active   13d

&lt;span class=&quot;c&quot;&gt;# mynginx 네임스페이스에서 실행 중인 오브젝트 확인&lt;/span&gt;
assu@myserver01:~/work/app/nginx-ingress-controller/nginx-ingress-controller-12.0.7&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mynginx
NAME                                                                  READY   STATUS    RESTARTS   AGE
pod/nginx-ingress-controller-ingress-nginx-controller-79bdc88bq9qzp   1/1     Running   0          3h34m

&lt;span class=&quot;c&quot;&gt;# 서비스 영역을 보면 nginx-ingress-controller를 외부에서 접근할 수 있는 EXTERNAL-IP가 &amp;lt;pending&amp;gt;임&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 이는 IP가 할당되지 않았음을 의미함&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 이후에 metallb를 설치하여 nginx-ingress-controller-1766819417 에 EXTERNAL-IP를 할당함&lt;/span&gt;
NAME                                                                  TYPE           CLUSTER-IP      EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                      AGE
service/nginx-ingress-controller-ingress-nginx-controller             LoadBalancer   10.107.56.92    &amp;lt;pending&amp;gt;     80:32038/TCP,443:32311/TCP   3h34m
service/nginx-ingress-controller-ingress-nginx-controller-admission   ClusterIP      10.107.71.139   &amp;lt;none&amp;gt;        443/TCP                      3h34m

NAME                                                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-ingress-controller-ingress-nginx-controller   1/1     1            1           3h34m

NAME                                                                            DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-ingress-controller-ingress-nginx-controller-79bdc88b77   1         1         1       3h34m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;확인 포인트:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Pod Status&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Running&lt;/code&gt; 상태이어야 한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Service EXTERNAL-IP&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;현재는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;pending&amp;gt;&lt;/code&gt; 상태인 것이 정상이다.&lt;/li&gt;
      &lt;li&gt;아직 클라우드 환경이 아닌 베어메탈(VM) 환경에 있기 때문에, IP를 할당해 줄 로드밸런서가 아직 없기 때문이다.&lt;/li&gt;
      &lt;li&gt;바로 다음 단계에서 &lt;strong&gt;MetalLB&lt;/strong&gt;를 설치하여 이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;pending&amp;gt;&lt;/code&gt; 상태를 해결하고 실제 IP를 할당받을 것이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-metallb를-통한-베어메탈-loadbalancer-구성&quot;&gt;5. metalLB를 통한 베어메탈 LoadBalancer 구성&lt;/h1&gt;

&lt;p&gt;&lt;a href=&quot;#4-헬름으로-nginx-ingress-controller-설치&quot;&gt;4. 헬름으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nginx ingress controller&lt;/code&gt; 설치&lt;/a&gt;에서 확인했듯이, 온프레미스나 베어메탈(VM) 환경에서는 
클라우드 제공자(AWS, GCP)가 없기 때문에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadBalancer&lt;/code&gt; 타입의 서비스를 생성해도 IP가 할당되지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;pending&amp;gt;&lt;/code&gt; 상태로 남는다.&lt;/p&gt;

&lt;p&gt;이를 해결하기 위해 &lt;strong&gt;MetalLB&lt;/strong&gt;를 사용한다.&lt;br /&gt;
MetalLB는 베어메탈 환경에서 표준 라우팅 프로토콜(ARP/BGP)을 사용하여 로드밸런서 기능을 구현해준다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;MetalLB에 대한 좀 더 상세한 내용은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-loadbalancer-for-bare-metal/&quot;&gt;MetalLB&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;사전 준비: kube-proxy 설정(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MetalLB가 정상적으로 작동하려면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt; 모드가 활성화되어야 한다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt; 에 대한 좀 더 상세한 내용은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-ipvs-strict-arp-deep-dive/&quot;&gt;strictARP&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;먼저 현재 설정을 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get configmap kube-proxy &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; kube-system &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; yaml | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;strictARP
      strictARP: &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;가 false 이므로, 아래 명령어를 통해 true로 변경해준다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get configmap kube-proxy &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; kube-system &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; yaml | &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sed&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s/strictARP: false/strictARP: true/&quot;&lt;/span&gt; | &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; - &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; kube-system

Warning: resource configmaps/kube-proxy is missing the kubectl.kubernetes.io/last-applied-configuration annotation 
which is required by kubectl apply. 
kubectl apply should only be used on resources created declaratively by either kubectl create &lt;span class=&quot;nt&quot;&gt;--save-config&lt;/span&gt; or kubectl apply. 
The missing annotation will be patched automatically.
configmap/kube-proxy configured
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이제 strictARP 를 확인하면 true로 변경된 것을 알 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get configmap kube-proxy &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; kube-system &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; yaml | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;strictARP
      strictARP: &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;
      ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;MetalLB 헬름 설치&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MetalLB 설치를 위한 디렉터리를 생성하고 공식 리포지토리를 추가한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; app/metallb &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;app/metallb

&lt;span class=&quot;c&quot;&gt;# MetalLB 공식 리포지토리 추가&lt;/span&gt;
assu@myserver01:~/work/app/metallb&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo add metallb https://metallb.github.io/metallb
&lt;span class=&quot;s2&quot;&gt;&quot;metallb&quot;&lt;/span&gt; has been added to your repositories

assu@myserver01:~/work/app/metallb&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm repo update
Hang tight &lt;span class=&quot;k&quot;&gt;while &lt;/span&gt;we grab the latest from your chart repositories...
...Successfully got an update from the &lt;span class=&quot;s2&quot;&gt;&quot;metallb&quot;&lt;/span&gt; chart repository
...Successfully got an update from the &lt;span class=&quot;s2&quot;&gt;&quot;bitnami&quot;&lt;/span&gt; chart repository
Update Complete. ⎈Happy Helming!⎈
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;최신 버전의 차트를 다운로드(pull)하고 압축을 해제한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 2개의 검색 결과&lt;/span&gt;
assu@myserver01:~/work/app/metallb&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm search repo metallb
NAME           	CHART VERSION	APP VERSION	DESCRIPTION
bitnami/metallb	6.4.22       	0.15.2     	MetalLB is a load-balancer implementation &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;b...
metallb/metallb	0.15.3       	v0.15.3    	A network load-balancer implementation &lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;Kube...

assu@myserver01:~/work/app/metallb&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm pull metallb/metallb
assu@myserver01:~/work/app/metallb&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;metallb-0.15.3.tgz

assu@myserver01:~/work/app/metallb&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tar &lt;/span&gt;xvfz metallb-0.15.3.tgz
assu@myserver01:~/work/app/metallb&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;metallb  metallb-0.15.3.tgz

assu@myserver01:~/work/app/metallb&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mv &lt;/span&gt;metallb metallb-0.15.3
assu@myserver01:~/work/app/metallb&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;metallb-0.15.3  metallb-0.15.3.tgz
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;설치 시 사용할 기본 설정 파일(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;values.yaml&lt;/code&gt;)을 복사해 둔다.(필요 시 수정하여 사용)&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metallb&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;metallb-0.15.3/
assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Chart.lock  charts  Chart.yaml  policy  README.md  templates  values.schema.json  values.yaml

assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp &lt;/span&gt;values.yaml my-values.yaml
assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;Chart.lock  charts  Chart.yaml  my-values.yaml  policy  README.md  templates  values.schema.json  values.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;관리 목적의 네임스페이스인 mymetallb 를 생성하고 설치를 진행한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl create namespace mymetallb
namespace/mymetallb created

assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get namespace
NAME               STATUS   AGE
calico-apiserver   Active   13d
calico-system      Active   13d
default            Active   13d
kube-node-lease    Active   13d
kube-public        Active   13d
kube-system        Active   13d
mymetallb          Active   7s
mynginx            Active   4h10m
tigera-operator    Active   13d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;helm &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;metallb &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymetallb &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; my-values.yaml

NAME: metallb-1766834438
LAST DEPLOYED: Sat Dec 27 11:20:39 2025
NAMESPACE: mymetallb
STATUS: deployed
REVISION: 1
DESCRIPTION: Install &lt;span class=&quot;nb&quot;&gt;complete
&lt;/span&gt;TEST SUITE: None
NOTES:
MetalLB is now running &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;the cluster.

Now you can configure it via its CRs. Please refer to the metallb official docs
on how to use the CRs.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-custom-resource-definition-metallb-configuration/&quot;&gt;설치 후 메시지를 보면 CRs를 통해 설정을 할 수 있다고 한다.&lt;/a&gt;&lt;br /&gt;
따라서 지금부터는 metalLB가 관리할 IP주소 범위를 설정한다.&lt;/p&gt;

&lt;p&gt;먼저 metalLB를 통해 설치한 오브젝트가 원활하게 동작하고 있는지 확인한다.&lt;br /&gt;
controller와 노드마다 실행되는 speaker 파드가 보여야 한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# speaker와 controller가 정상적으로 작동 중(Running)이다.&lt;/span&gt;
assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymetallb
NAME                                               READY   STATUS    RESTARTS   AGE
pod/metallb-1766834438-controller-66c6c584-kbwdn   1/1     Running   0          22m
pod/metallb-1766834438-speaker-7sjlw               4/4     Running   0          22m
pod/metallb-1766834438-speaker-j99kv               4/4     Running   0          22m
pod/metallb-1766834438-speaker-zkktj               4/4     Running   0          22m

&lt;span class=&quot;c&quot;&gt;# metallb-webhook-service 의 EXTERNAL-IP 가 none 인 것은 정상이다. metallb-webhook-service 는 클러스터 내부에서만 사용하기 때문이다.&lt;/span&gt;
NAME                              TYPE        CLUSTER-IP     EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/metallb-webhook-service   ClusterIP   10.97.206.62   &amp;lt;none&amp;gt;        443/TCP   22m

NAME                                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/metallb-1766834438-speaker   3         3         3       3            3           kubernetes.io/os&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux   22m

NAME                                            READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/metallb-1766834438-controller   1/1     1            1           22m

NAME                                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/metallb-1766834438-controller-66c6c584   1         1         1       22m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;MetalLB 설정(IP 주소 풀 할당)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;설치는 완료되었지만 MetalLB가 어떤 IP 대역을 사용할지 아직 모르는 상태이다.&lt;br /&gt;
과거에는 ConfigMap을 사용했으나, 최근에는 &lt;strong&gt;CRD(Custom Resource Definition)&lt;/strong&gt;을 통해 설정한다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;MetalLB CRD에 대한 좀 더 상세한 내용은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-custom-resource-definition-metallb-configuration/#metallb-%EC%84%A4%EC%A0%95%EC%9D%98-%ED%95%B5%EC%8B%AC-crs&quot;&gt;MetalLB 설정의 핵심 CRs&lt;/a&gt;를 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;이제 metalLB의 설정을 변경하기 위해 metalLB를 설치했던 디렉터리에서 config 파일을 추가한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim my-config.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# metalLB가 로드밸런서 서비스에 할당할 IP 주소의 범위를 정의하는 리소스&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;metallb.io/v1beta1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;IPAddressPool&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my-metallb-config&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mymetallb&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;addresses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;10.0.2.20-10.0.2.40&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 로드밸런서가 사용할 IP 범위&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;autoAssign&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# 정의된 IP 주소 풀을 네트워크에 어떻게 알릴지(Advertisement) 설정하는 리소스&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;# Layer 2 모드(ARP 사용)로 동작할지, BGP 모드로 동작할지 결정하는 역할 수행&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;metallb.io/v1beta1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;L2Advertisement&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my-metallb-config&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mymetallb&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ipAddressPools&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;my-metallb-config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;주의사항:&lt;/strong&gt;&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;addresses&lt;/code&gt; 범위를 설정할 때, &lt;strong&gt;쿠버네티스 노드들이 실제로 사용 중인 IP(예: 10.0.2.4~10.0.2.6)과 겹치지 않도록&lt;/strong&gt; 주의해야 한다.&lt;br /&gt;
IP 충돌을 방지하기 위해 여유 있는 대역(예: 20~40)을 할당했다.&lt;/p&gt;

&lt;p&gt;작성한 설정을 적용한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; my-config.yaml
ipaddresspool.metallb.io/my-metallb-config created
l2advertisement.metallb.io/my-metallb-config created

assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get ipaddresspool.metallb.io &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymetallb
NAME                AUTO ASSIGN   AVOID BUGGY IPS   ADDRESSES
my-metallb-config   &lt;span class=&quot;nb&quot;&gt;true          false&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;10.0.2.20-10.0.2.40&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# my-metallb-config가 정상적으로 실행된 것을 확인할 수 있다.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;describe&lt;/code&gt; 를 통해 my-metallb-config 의 상세 정보를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl describe ipaddresspool.metallb.io my-metallb-config &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mymetallb
Name:         my-metallb-config
Namespace:    mymetallb
Labels:       &amp;lt;none&amp;gt;
Annotations:  &amp;lt;none&amp;gt;
API Version:  metallb.io/v1beta1
Kind:         IPAddressPool
Metadata:
  Creation Timestamp:  2025-12-27T12:13:54Z
  Generation:          1
  Resource Version:    292181
  UID:                 f03615d3-532e-401a-ae1e-170950e9e60b
Spec:
  Addresses:
    10.0.2.20-10.0.2.40  &lt;span class=&quot;c&quot;&gt;# IP 주소 범위가 정확히 설정되어 있는 것 확인&lt;/span&gt;
  Auto Assign:       &lt;span class=&quot;nb&quot;&gt;true
  &lt;/span&gt;Avoid Buggy I Ps:  &lt;span class=&quot;nb&quot;&gt;false
&lt;/span&gt;Status:
  assignedIPv4:   1
  assignedIPv6:   0
  availableIPv4:  20
  availableIPv6:  0
Events:           &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;IP 할당 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이제 MetalLB가 설정을 받아들였으므로, &lt;strong&gt;Nginx Ingress Controller&lt;/strong&gt; 서비스를 다시 확인해보자.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;pending&amp;gt;&lt;/code&gt; 상태였던 External-IP가 할당되었을 것이다.&lt;/p&gt;

&lt;p&gt;mynginx 네임스페이스에 존재하는 오브젝트를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; mynginx

NAME                                                                  READY   STATUS    RESTARTS   AGE
pod/nginx-ingress-controller-ingress-nginx-controller-79bdc88bq9qzp   1/1     Running   0          4h19m

NAME                                                                  TYPE           CLUSTER-IP      EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                      AGE
service/nginx-ingress-controller-ingress-nginx-controller             LoadBalancer   10.107.56.92    10.0.2.20     80:32038/TCP,443:32311/TCP   4h19m
service/nginx-ingress-controller-ingress-nginx-controller-admission   ClusterIP      10.107.71.139   &amp;lt;none&amp;gt;        443/TCP                      4h19m

NAME                                                                READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-ingress-controller-ingress-nginx-controller   1/1     1            1           4h19m

NAME                                                                           DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-ingress-controller-ingress-nginx-controller-79bdc88b77   1         1         1       4h19m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;서비스 영역의 nginx-ingress-controller 에 10.0.2.20 이라는 외부 아이피인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EXTERNAL-IP&lt;/code&gt;가 할당된 것을 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl describe&lt;/code&gt; 명령어로 상세 이벤트를 확인해보면 MetalLB가 IP를 할당한 기록을 볼 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/app/metallb/metallb-0.15.3&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl describe service/nginx-ingress-controller-1766819417 &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mynginx
...
Events:
  Type    Reason       Age   From                Message
  &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;    &lt;span class=&quot;nt&quot;&gt;------&lt;/span&gt;       &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;  &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;                &lt;span class=&quot;nt&quot;&gt;-------&lt;/span&gt;
  Normal  IPAllocated  14h   metallb-controller  Assigned IP &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;10.0.2.20&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;호스트 포트 포워딩(VM 환경)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;VM 환경에서 실습 중이므로, VM 내부의 IP(10.0.2.20)로 호스트 PC(내 로컬)에서 직접 접근이 불가할 수 있다.&lt;br /&gt;
이 경우 VM 설정에서 &lt;strong&gt;포트 포워딩&lt;/strong&gt;을 해주어야 한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1222/portforwarding.png&quot; alt=&quot;포트 포워딩&quot; /&gt;&lt;/p&gt;

&lt;p&gt;위 그림처럼 호스트의 포트(예: 2000)를 게스트 VM의 인그레스 IP(10.0.2.20)이나 노드 IP의 포트로 전달하도록 설정하면, 로컬 브라우저에서도 접속 테스트가 가능하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-인그레스로-하나의-서비스-배포&quot;&gt;6. 인그레스로 하나의 서비스 배포&lt;/h1&gt;

&lt;p&gt;이제 환경 구성이 되었으니, 실제로 인그레스를 활용해 웹 서비스를 배포해보자.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;디렉터리 준비&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex13
assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex13
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;디플로이먼트 생성(ingress01-deploy.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;가장 먼저 웹 서비스를 수행할 애플리케이션(Nginx 파드)를 배포한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim ingress01-deploy.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deployment&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ingress-deploy-test01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 이 라벨을 가진 파드를 관리함&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy01&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# selector로 적용되는 이름이 되므로 파드를 생성했을 때의 이름과 동일해야 함&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 생성될 파드의 스펙&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy01&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 서비스와 연결될 라벨&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:1.25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;디플로이먼트를 실행하고 파드가 정상적으로 생성되었는지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress01-deploy.yml
deployment.apps/ingress-deploy-test01 created

assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
&lt;span class=&quot;c&quot;&gt;# 파드 3개가 실행됨&lt;/span&gt;
NAME                                         READY   STATUS    RESTARTS   AGE
pod/ingress-deploy-test01-68d47df476-8qq8q   1/1     Running   0          6s
pod/ingress-deploy-test01-68d47df476-db5gk   1/1     Running   0          6s
pod/ingress-deploy-test01-68d47df476-l2ctz   1/1     Running   0          6s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   47h

&lt;span class=&quot;c&quot;&gt;# 디플로이먼트 실행됨&lt;/span&gt;
NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ingress-deploy-test01   3/3     3            3           6s

&lt;span class=&quot;c&quot;&gt;# 레플리카셋 실행됨&lt;/span&gt;
NAME                                               DESIRED   CURRENT   READY   AGE
replicaset.apps/ingress-deploy-test01-68d47df476   3         3         3       6s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;서비스(Service) 생성(ingress01-service.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;파드 앞단에서 트래픽을 받아줄 서비스를 생성한다.&lt;br /&gt;
여기서 중요한 점은 서비스 타입이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterIP&lt;/code&gt;라는 점이다. 외부 노출은 인그레스가 담당하므로 이 서비스는 &lt;strong&gt;클러스터 내부에서 인그레스 컨트롤러와 통신&lt;/strong&gt;만 되면 충분하다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim ingress01-service.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Service&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ingress-service-test01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 디플로이먼트의 파드 라벨과 일치해야 함&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy01&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 디플로이먼트에서 만든 web-deploy01 앱과 연동&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ClusterIP&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 인그레스 연결용이므로 내부 IP만 있으면 됨&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 서비스를 사용하기 위한 포트 정의&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;protocol&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;TCP&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 서비스가 사용하는 포트&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;targetPort&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 파드가 받게 될 포트&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;서비스를 생성하고 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress01-service.yml
service/ingress-service-test01 created

assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get service
NAME                     TYPE        CLUSTER-IP      EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
ingress-service-test01   ClusterIP   10.108.252.69   &amp;lt;none&amp;gt;        80/TCP    4s  &lt;span class=&quot;c&quot;&gt;# 정상적으로 실행&lt;/span&gt;
kubernetes               ClusterIP   10.96.0.1       &amp;lt;none&amp;gt;        443/TCP   47h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;인그레스(Ingress) 생성(ingress01-ingress.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이제 외부의 요청을 서비스로 연결해 줄 라우팅 규칙(Ingress)을 정의한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim ingress01-ingress.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Ingress&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ingress-test01&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 인그레스 컨트롤러에 대해 옵션을 설정&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# [중요] 사용자가 /test01로 접근하더라도 백엔드 파드에는 / 경로로 전달하도록 재작성&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 설치한 Nginx Ingress Controller 사용, kubectl get ingressclass 입력 시 나오는 결과인 nginx 로 기재&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# http 사용&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/test01&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 사용자가 접근할 URL 경로&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Prefix&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# pathType은 path를 인식하는 방식을 정하는 옵션으로 Prefix는 접두사가 일치하면 해당 경로가 적용됨, Exact로 하면 정확히 일치해야 함&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 백엔드 설정&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 인그레스에 연동할 서비스 등록&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ingress-service-test01&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 연결할 서비스 이름&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;설정 포인트:&lt;/strong&gt;&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rewrite-target: /&lt;/code&gt;: 사용자가 http://IP/test01 로 요청을 보낼 때, 실제 Nginx 웹 서버는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/test01&lt;/code&gt; 이라는 경로를 알지 못한다.(기본값은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;가 루트)&lt;br /&gt;
이 애너테이션은 요청을 백엔드로 보낼 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/test01&lt;/code&gt;을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;로 바꿔서 전달해 주는 역할을 한다.&lt;/p&gt;

&lt;p&gt;인그레스를 생성 후 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress01-ingress.yml
ingress.networking.k8s.io/ingress-test01 created

assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get ingress
NAME             CLASS   HOSTS   ADDRESS     PORTS   AGE
ingress-test01   nginx   &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;       10.0.2.20   80      71m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ADDRESS&lt;/code&gt; 필드에 IP(10.0.2.20)이 표시되기까지 약 1분 정도 소요된다.&lt;br /&gt;
이 IP는 MetalLB가 Nginx Ingress Controller 서비스에 할당한 IP와 동일하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;아키텍처 및 접속 테스트&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;지금까지 작성한 YAML 파일들의 유기적인 연결 관계는 아래와 같다.&lt;br /&gt;
Ingress가 Service를 가리키고, Service가 Deployment(Pod)를 가리키는 구조이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1222/yml.png&quot; alt=&quot;Ingress yml 관계&quot; /&gt;&lt;/p&gt;

&lt;p&gt;실제 트래픽이 흐르는 경로는 다음과 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1222/flow.png&quot; alt=&quot;인그레스를 통한 서비스 배포&quot; /&gt;&lt;/p&gt;

&lt;p&gt;사용자는 &lt;strong&gt;MetalLB IP&lt;/strong&gt;로 진입하고, &lt;strong&gt;Ingress Controller&lt;/strong&gt;가 경로를 확인한 뒤 &lt;strong&gt;Service&lt;/strong&gt;를 거쳐 &lt;strong&gt;Pod&lt;/strong&gt;로 도달한다.&lt;/p&gt;

&lt;p&gt;이제 브라우저에서 접속을 시도해보자.&lt;br /&gt;
VM 환경에서 포트 포워딩(Host: 2000 → VM LB IP: 80)을 설정했으므로, 호스트 PC에서 아래 주소로 접속한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://127.0.0.1:2000/test01&quot;&gt;http://127.0.0.1:2000/test01&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nginx 의 환영 페이지가 보인다면 성공이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;리소스 정리&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;현재 생성한 리소스들을 삭제한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress01-ingress.yml
ingress.networking.k8s.io &lt;span class=&quot;s2&quot;&gt;&quot;ingress-test01&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress01-service.yml
service &lt;span class=&quot;s2&quot;&gt;&quot;ingress-service-test01&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress01-deploy.yml
deployment.apps &lt;span class=&quot;s2&quot;&gt;&quot;ingress-deploy-test01&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get ingress
No resources found &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;default namespace.

assu@myserver01:~/work/ch09/ex13&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE   SELECTOR
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   2d    &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;7-인그레스로-두-개의-서비스-배포&quot;&gt;7. 인그레스로 두 개의 서비스 배포&lt;/h1&gt;

&lt;p&gt;여기서는 인그레스의 꽃이라 할 수 있는 &lt;strong&gt;경로 기반 라우팅(Path-based Routing)&lt;/strong&gt;을 통해, 하나의 IP로 여러 서비스를 운영하는 &lt;strong&gt;L7 로드밸런싱&lt;/strong&gt;을 구현해본다.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;#6-인그레스로-하나의-서비스-배포&quot;&gt;6. 인그레스로 하나의 서비스 배포&lt;/a&gt;에서는 인그레스를 통해 하나의 서비스만 연결했다.&lt;br /&gt;
하지만 인그레스의 진정한 가치는 &lt;strong&gt;단일 진입점(IP/도메인)으로 들어온 트래픽을 URL 경로나 호스트 이름에 따라 여러 서비스로 분산&lt;/strong&gt;시키는 데 있다.&lt;br /&gt;
이를 &lt;a href=&quot;https://assu10.github.io/dev/2025/05/27/fanout/&quot;&gt;&lt;strong&gt;Fan-out&lt;/strong&gt;&lt;/a&gt; 구성이라고도 한다.&lt;/p&gt;

&lt;p&gt;여기서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/test01&lt;/code&gt; 경로는 첫 번째 서비스로, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/test02&lt;/code&gt; 경로는 두 번째 서비스로 연결하는 구성을 해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;디렉터리 준비&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; ex13 ex14
assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex14

assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ll
total 24
drwxrwxr-x  2 assu assu 4096 Dec 28 07:09 ./
drwxrwxr-x 16 assu assu 4096 Dec 28 07:09 ../
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt;  1 assu assu  333 Dec 28 07:09 1
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt;  1 assu assu  332 Dec 28 07:09 ingress01-deploy.yml
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt;  1 assu assu  410 Dec 28 07:09 ingress01-ingress.yml
&lt;span class=&quot;nt&quot;&gt;-rw-rw-r--&lt;/span&gt;  1 assu assu  212 Dec 28 07:09 ingress01-service.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;두 번째 웹 서비스(Deployment &amp;amp; Service) 정의&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;첫 번째 서비스(/test01)은 이미 준비되어 있으므로, 두 번째 서비스인 /test02를 위한 디플로이먼트와 서비스 매니페스트를 작성한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;디플로이먼트(ingress02-deploy.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;기존 파일에서 이름과 라벨을 02로 변경한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim ingress02-deploy.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deployment&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ingress-deploy-test02&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 이름 변경&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy02&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 라벨 변경&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy02&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 라벨 변경&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:1.25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;서비스(ingress02-service.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;마찬가지로 web-deploy02 파드를 바라보도록 셀렉터를 설정한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim ingress02-service.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Service&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ingress-service-test02&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 서비스 이름 변경&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-deploy02&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 위에서 만든 파드와 연결&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ClusterIP&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;protocol&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;TCP&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;targetPort&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;멀티 패스 인그레스 정의&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이제 가장 중요한 인그레스 규칙을 작성한다.&lt;br /&gt;
하나의 host 아래 두 개의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;path&lt;/code&gt;를 정의하여 트래픽을 분기한다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Ingress&lt;/span&gt; 
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ingress-test02&lt;/span&gt; 
  &lt;span class=&quot;na&quot;&gt;annotations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nginx.ingress.kubernetes.io/rewrite-target&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ingressClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;rules&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# 첫 번째 경로&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/test01&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Prefix&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ingress-service-test01&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# 두 번째 경로 (추가됨)&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/test02&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;pathType&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Prefix&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;backend&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ingress-service-test02&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 두 번째 path 가 바라보는 서비스 이름&lt;/span&gt;
                &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
                  &lt;span class=&quot;na&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 YAML 파일들의 관계는 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1222/yml2.png&quot; alt=&quot;Ingress 구조&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;배포 및 실행 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이제 디플로이먼트, 서비스, 인그레스를 실행한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 서비스 1 배포&lt;/span&gt;
assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress01-deploy.yml
deployment.apps/ingress-deploy-test01 created
assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress01-service.yml
service/ingress-service-test01 created

&lt;span class=&quot;c&quot;&gt;# 서비스 2 배포&lt;/span&gt;
assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress02-deploy.yml
deployment.apps/ingress-deploy-test02 created
assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress02-service.yml
service/ingress-service-test02 created
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;모든 파드와 서비스가 정상적으로 실행 중인지 확인한다.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;^Cassu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubect get all
NAME                                         READY   STATUS    RESTARTS   AGE
pod/ingress-deploy-test01-68d47df476-bf2kg   1/1     Running   0          5m56s
pod/ingress-deploy-test01-68d47df476-m9h8k   1/1     Running   0          5m56s
pod/ingress-deploy-test01-68d47df476-wsdrs   1/1     Running   0          5m56s
pod/ingress-deploy-test02-6c574cb47c-8xqhv   1/1     Running   0          5m42s
pod/ingress-deploy-test02-6c574cb47c-ntbxj   1/1     Running   0          5m42s
pod/ingress-deploy-test02-6c574cb47c-whr4p   1/1     Running   0          5m42s

NAME                             TYPE        CLUSTER-IP      EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/ingress-service-test01   ClusterIP   10.107.58.239   &amp;lt;none&amp;gt;        80/TCP    5m49s
service/ingress-service-test02   ClusterIP   10.102.186.26   &amp;lt;none&amp;gt;        80/TCP    5m34s
service/kubernetes               ClusterIP   10.96.0.1       &amp;lt;none&amp;gt;        443/TCP   2d2h

NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ingress-deploy-test01   3/3     3            3           5m56s
deployment.apps/ingress-deploy-test02   3/3     3            3           5m42s

NAME                                               DESIRED   CURRENT   READY   AGE
replicaset.apps/ingress-deploy-test01-68d47df476   3         3         3       5m56s
replicaset.apps/ingress-deploy-test02-6c574cb47c   3         3         3       5m42s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;마지막으로 인그레스를 생성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress02-ingress.yml
ingress.networking.k8s.io/ingress-test02 created

assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get ingress
NAME             CLASS   HOSTS   ADDRESS     PORTS   AGE
ingress-test02   nginx   &lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;       10.0.2.20   80      32s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;마찬가지로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ADDRESS&lt;/code&gt; 는 처음에 확인하면 안 나오고 약 1분정도 기다리면 나온다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ADDRESS&lt;/code&gt;에 MetalLB가 제공한 IP(10.0.2.20)가 할당된 것을 확인할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; mynginx
NAME                                                                  READY   STATUS    RESTARTS   AGE
pod/nginx-ingress-controller-ingress-nginx-controller-79bdc88bq9qzp   1/1     Running   0          127m

NAME                                                                  TYPE           CLUSTER-IP      EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;                      AGE
service/nginx-ingress-controller-ingress-nginx-controller             LoadBalancer   10.107.56.92    10.0.2.20     80:32038/TCP,443:32311/TCP   127m
service/nginx-ingress-controller-ingress-nginx-controller-admission   ClusterIP      10.107.71.139   &amp;lt;none&amp;gt;        443/TCP                      127m

NAME                                                                READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-ingress-controller-ingress-nginx-controller   1/1     1            1           127m

NAME                                                                           DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-ingress-controller-ingress-nginx-controller-79bdc88b77   1         1         1       127m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;전체 트래픽 흐름 및 테스트&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;지금까지의 전체적인 트래픽을 그림으로 나타내면 아래와 같다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1222/flow2.png&quot; alt=&quot;인그레스로 두 개의 서비스 배포&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;L7 로드밸런서(Ingress)&lt;/strong&gt;가 URL 경로를 분석하여 트래픽을 분기한다.&lt;/p&gt;

&lt;p&gt;웹 브라우저(로컬 호스트)를 통해 접속 테스트를 진행한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;http://127.0.0.1:2000/test01&quot;&gt;http://127.0.0.1:2000/test01&lt;/a&gt; → ingress-service-test01 연결 → Nginx 환영 페이지 출력&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://127.0.0.1:2000/test02&quot;&gt;http://127.0.0.1:2000/test02&lt;/a&gt; → ingress-service-test02 연결 → Nginx 환영 페이지 출력&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;두 주소 모두 동일한 Nginx 이미지를 사용했기에 화면은 같지만, 실제로는 서로 다른 파드로 라우팅 되고 있음을 알 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;생성했던 리소스들을 삭제하여 클러스터를 정리한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress02-ingress.yml
ingress.networking.k8s.io &lt;span class=&quot;s2&quot;&gt;&quot;ingress-test02&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress01-service.yml
service &lt;span class=&quot;s2&quot;&gt;&quot;ingress-service-test01&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress02-service.yml
service &lt;span class=&quot;s2&quot;&gt;&quot;ingress-service-test02&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress01-deploy.yml
deployment.apps &lt;span class=&quot;s2&quot;&gt;&quot;ingress-deploy-test01&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ingress02-deploy.yml
deployment.apps &lt;span class=&quot;s2&quot;&gt;&quot;ingress-deploy-test02&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get ingress
No resources found &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;default namespace.

assu@myserver01:~/work/ch09/ex14&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   2d2h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 장철원 저자의 &lt;strong&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/126115324&quot;&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/losskatsu/DockerKubernetes&quot;&gt;예제 코드&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://helm.sh/docs/intro/install/&quot;&gt;Doc:: Installing Helm&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-loadbalancer-for-bare-metal/&quot;&gt;MetalLB&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-ipvs-strict-arp-deep-dive/&quot;&gt;strictARP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 22 Dec 2025 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/22/kubernetes-ingress-helm-metallb-baremetal-loadbalancer/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/22/kubernetes-ingress-helm-metallb-baremetal-loadbalancer/</guid>
        
        <category>devops</category>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>helm</category>
        
        <category>ingress</category>
        
        <category>nginx-ingress-controller</category>
        
        <category>metallb</category>
        
        <category>bare-metal</category>
        
        <category>on-premise</category>
        
        <category>loadbalancer</category>
        
        <category>l7-loadbalancer</category>
        
        <category>service-exposure</category>
        
        <category>routing</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>MetalLB</title>
        <description>&lt;p&gt;MetalLB는 클라우드 공급자(AWS, GCP, Azure 등)가 제공하는 로드밸런서 기능이 없는 &lt;strong&gt;베어메탈(Bare Metal) 환경의 쿠버네티스 클러스터를 위해 만들어진 로드밸런서 구현체&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;표준 라우팅 프로토콜(ARP, BGP)을 사용하여, 쿠버네티스 서비스(Service) 중 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type: LoadBalancer&lt;/code&gt;에 외부 접속 가능한 IP(External IP)를 할당하고 
외부 트래픽을 클러스터 내부로 끌어오는 역할을 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#metallb가-필요한-이유&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MetalLB&lt;/code&gt;가 필요한 이유&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#주요-요소&quot;&gt;주요 요소&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#두-가지-동작-모드&quot;&gt;두 가지 동작 모드&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#layer-2-모드arpndp&quot;&gt;Layer 2 모드(ARP/NDP)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#bgp-모드border-gateway-protocol&quot;&gt;BGP 모드(Border Gateway Protocol)&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#트래픽-흐름&quot;&gt;트래픽 흐름&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#정리하며&quot;&gt;정리하며..&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;metallb가-필요한-이유&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MetalLB&lt;/code&gt;가 필요한 이유&lt;/h1&gt;

&lt;p&gt;쿠버네티스를 AWS와 같은 퍼블릭 클라우드에서 사용할 때, 서비스 타입을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadBalancer&lt;/code&gt;로 지정하면 클라우드 공급자가 자동으로 외부 IP를 가진 로드밸런서를 생성해준다.&lt;/p&gt;

&lt;p&gt;하지만 &lt;strong&gt;베어메탈(온프레미스) 환경&lt;/strong&gt;에서는 어떨까?&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;현상&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;External-IP&lt;/code&gt; 상태가 영원히 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;pending&amp;gt;&lt;/code&gt; 상태로 멈춰있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;이유&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;쿠버네티스 자체에는 로드밸런서를 구현하는 기능이 포함되어 있지 않기 때문이다.&lt;/li&gt;
      &lt;li&gt;쿠버네티스는 로드밸런서가 필요하다고 요청만 할 뿐, 실제 IP를 주고 연결해 줄 주체가 베어메탈에는 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MetalLB는 베어메탈에서도 클라우드처럼 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LoadBalancer&lt;/code&gt; 서비스를 즉시 사용할 수 있게 해준다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;주요-요소&quot;&gt;주요 요소&lt;/h1&gt;

&lt;p&gt;MetalLB는 크게 두 가지 요소로 구성되어 파드 형태로 실행된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Controller&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;설정된 IP pool에서 어떤 IP를 서비스에 할당할지 결정하고 관리한다.&lt;/li&gt;
      &lt;li&gt;즉, IP 자원 관리자이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Speaker&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;각 노드마다 실행(DaemonSet)되며, 할당된 IP를 네트워크 상에서 알리는(Advertising) 역할을 한다.&lt;/li&gt;
      &lt;li&gt;‘이 IP의 주인은 나(노드)야’라고 라우터에게 알려서 트래픽을 유도한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;두-가지-동작-모드&quot;&gt;두 가지 동작 모드&lt;/h1&gt;

&lt;p&gt;MetalLB는 네트워크 환경에 따라 두 가지 모드 중 하나를 선택하여 동작한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;layer-2-모드arpndp&quot;&gt;Layer 2 모드(ARP/NDP)&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동작 방식&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;클러스터 노드 중 하나가 리더가 되어 로드밸런서 IP에 대한 ARP 요청에 응답한다.&lt;/li&gt;
      &lt;li&gt;이 때 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-ipvs-strict-arp-deep-dive/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;&lt;/a&gt; 설정이 충돌 방지를 위해 사용된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;특별한 네트워크 장비 설정 없이 공유기 수준의 환경에서도 즉시 사용 가능하다. 설정이 가장 쉽다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;리더 노드 한 곳으로만 트래픽이 몰린다. (단일 노드 대역폭 제한)&lt;/li&gt;
      &lt;li&gt;리더가 죽으면 다른 노드가 리더가 되기까지 짧은 끊김이 발생한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;bgp-모드border-gateway-protocol&quot;&gt;BGP 모드(Border Gateway Protocol)&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;동작 방식&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;라우터와 직접 BGP 피어링을 맺고 라우팅 정보를 교환한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;장점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;진정한 로드밸런싱이 가능하다.&lt;/li&gt;
      &lt;li&gt;라우터가 여러 노드로 트래픽을 분산(ECMP)하여 보내준다. 대역폭 한계가 노드 수만큼 늘어난다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;단점&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;BGP를 지원하는 고가/고기능의 라우터가 필요하며, 네트워크 설정이 복잡하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;트래픽-흐름&quot;&gt;트래픽 흐름&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;사용자가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Service (type: LoadBalancer)&lt;/code&gt; 생성 요청&lt;/li&gt;
  &lt;li&gt;MetalLB Controller가 설정된 범위(예: 192.168.1.200~240) 내에서 IP 하나를 할당&lt;/li&gt;
  &lt;li&gt;MetalLB Speaker가 해당 IP를 네트워크에 알림(Layer 2 모드라면 ARP 응답)&lt;/li&gt;
  &lt;li&gt;외부 사용자가 해당 IP로 접속하면, 라우터가 MAC 주소를 확인하고 해당 노드로 트래픽 전송&lt;/li&gt;
  &lt;li&gt;노드의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt; (IPVS/iptables)가 받아서 최종 파드로 전달&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며..&lt;/h1&gt;

&lt;p&gt;MetalLB가 없다면 온프레미스 쿠버네티스에서 외부 통신을 위해 
&lt;a href=&quot;https://assu10.github.io/dev/2025/12/08/kubernetes-service-concept-and-types/#3-nodeport&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodePort&lt;/code&gt;&lt;/a&gt;나 
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ingress&lt;/code&gt;만을 제한적으로 사용해야 하는 불편함이 있다.&lt;br /&gt;
MetalLB를 통해 비로소 베어메탈 클러스터도 클라우드와 동일한 수준의 &lt;strong&gt;네트워크 유연성&lt;/strong&gt;을 갖추게 된다.&lt;br /&gt;
특히 &lt;strong&gt;Layer 2 모드&lt;/strong&gt;는 복잡한 장비 없이도 누구나 쉽게 구축할 수 있어 홈랩이나 중소규모 온프레미스 환경의 표준으로 자리잡았다.&lt;/p&gt;
</description>
        <pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-loadbalancer-for-bare-metal/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-loadbalancer-for-bare-metal/</guid>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>metallb</category>
        
        <category>load-balancer</category>
        
        <category>bare-metal</category>
        
        <category>layer2</category>
        
        <category>bgp</category>
        
        <category>on-premise</category>
        
        <category>networking</category>
        
        <category>external-ip</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>strictARP</title>
        <description>&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;는 쿠버네티스 환경, 특히 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-loadbalancer-for-bare-metal/&quot;&gt;&lt;strong&gt;MetalLB&lt;/strong&gt;&lt;/a&gt;와 같은 로드밸런서를 온프레미스(Bare Metal) 환경에서 &lt;strong&gt;IPVS 모드&lt;/strong&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt;와 함께 
사용할 때 짚고 넘어가야 하는 부분이다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;는 쿠버네티스 클러스터의 네트워크 프록시인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt;의 설정 옵션 중 하나이다.&lt;br /&gt;
이 설정을 활성화(true)하면, 리눅스 커널의 ARP(Address Resolution Protocol) 동작 방식인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arp_ignore&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arp_announce&lt;/code&gt; 값을 수정하여, 
&lt;strong&gt;물리 인터페이스가 자신이 보유하지 않은 IP 주소(예: LoadBalancer의 ExternalIP)에 대한 ARP 요청에 응답하지 않도록 제한&lt;/strong&gt;한다.&lt;/p&gt;

&lt;p&gt;쉽게 말해서 ‘이 IP는 본인이 진짜 주인일 때만 응답하겠다’라고 커널을 엄격하게 단속하는 설정이며, 주로 MetalLB가 ARP 응답을 전담해야 할 때 충돌을 방지하기 위해 사용한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#strictarp가-필요한-이유&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;가 필요한 이유&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#strictarp의-동작-원리kernel-sysctl&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;의 동작 원리(Kernel Sysctl)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#설정-방법-및-코드-예시&quot;&gt;설정 방법 및 코드 예시&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#장점&quot;&gt;장점&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#주의-사항&quot;&gt;주의 사항&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#정리하며&quot;&gt;정리하며..&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;strictarp가-필요한-이유&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;가 필요한 이유&lt;/h1&gt;

&lt;p&gt;쿠버네티스 서비스 프록시 모드를 &lt;strong&gt;IPVS(IP Virtual Server)&lt;/strong&gt;로 설정하고, 온프레미스 로드밸런서인 &lt;strong&gt;MetalLB&lt;/strong&gt;를 Layer 2 모드로 사용할 때 문제가 발생할 수 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;IPVS의 동작&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt;가 IPVS 모드일 때, 서비스의 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/08/kubernetes-service-concept-and-types/#2-clusterip&quot;&gt;ClusterIP&lt;/a&gt;나 LoadBalancer IP는 노드의 가상 인터페이스(주로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-ipvs&lt;/code&gt;)에 할당된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;커널의 기본 동작&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;리눅스 커널은 기본적으로 깐깐하지 않다. 어떤 인터페이스로 ARP 요청이 들어오든, 그 IP가 내 노드 어딘가(예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-ipvs0&lt;/code&gt;)에 있다면 본인에게 있는 IP라고 판단하여 물리 인터페이스(예: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eth0&lt;/code&gt;)가 대신 응답해버릴 수 있다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;충돌 발생&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;MetalLB는 특정 노드 하나가 리더가 되어 ‘내가 그 LoadBalancer의 주인이다’라고 ARP 응답을 보내려 한다.&lt;/li&gt;
      &lt;li&gt;그런데 커널이 모든 노드에서 ARP 응답을 보내버리면, 라우터는 혼란에 빠져서 트래픽이 분산되지 않거나 끊기는 현상이 발생한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;strictarp의-동작-원리kernel-sysctl&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;의 동작 원리(Kernel Sysctl)&lt;/h1&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt; 설정에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP: true&lt;/code&gt;를 적용하면, 실제로는 해당 노드의 물리 인터페이스에 대해 리눅스 커널 파라미터(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sysctl&lt;/code&gt;) 두 가지를 수정한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arp_ignore = 1&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;자신이 ARP 요청을 받은 인터페이스에 설정된 IP에 대해서만 응답함&lt;/li&gt;
      &lt;li&gt;즉, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eth0&lt;/code&gt;으로 들어온 요청인데 IP가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-ipvs0&lt;/code&gt;에만 있다면 응답하지 않고 무시함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arp_announce = 2&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;ARP 요청을 보낼 때, 보내는 인터페이스(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;eth0&lt;/code&gt;)의 서브넷에 맞는 소스 IP만 사용하도록 강제함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 조치를 통해 커널이 불필요하게 ARP 응답을 하는 것을 막고, &lt;strong&gt;ARP 응답의 권한을 온전히 사용자 공간(User Space)의 애플리케이션인 MetalLB에게 넘겨주는 것&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;설정-방법-및-코드-예시&quot;&gt;설정 방법 및 코드 예시&lt;/h1&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt;의 ConfigMap을 수정하여 적용한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# kube-proxy 설정 수정&lt;/span&gt;
kubectl edit configmap &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; kube-system kube-proxy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ConfigMap 내용(YAML)&lt;/p&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;kubeproxy.config.k8s.io/v1alpha1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;KubeProxyConfiguration&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;mode&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ipvs&quot;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 반드시 IPVS 모드여야 함&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;ipvs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;strictARP&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 이 값을 false에서 true로 변경&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ... 기타 설정&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 설정을 변경한 후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt; 파드를 재시작하거나, 롤아웃을 적용해야 한다.&lt;br /&gt;
MetalLB 설치 전 사전 작업으로 수행하는 것이 일반적이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;장점&quot;&gt;장점&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;네트워크 안정성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;MetalLB Layer 2 모드에서 다중 노드가 동일한 VIP에 대해 ARP 응답을 보내는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ARP Flap&lt;/code&gt; 현상을 근본적으로 차단함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;명확한 역할 분담&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;커널과 MetalLB 간의 역할 충돌 방지&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;주의-사항&quot;&gt;주의 사항&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;IPVS 모드 종속&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;이 설정은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt;가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iptables&lt;/code&gt; 모드일 때는 해당되지 않거나 동작 방식이 다르다. (MetalLB 문서에는 IPVS 사용 시 필수라고 명시)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;기존 네트워크 영향&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;매우 드물지만, 커널의 ARP 동작을 엄격하게 제한하므로, 복잡한 커스텀 네트워크 라우팅을 사용하는 노드에서는 사이드 이펙트가 없는지 확인이 필요하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며..&lt;/h1&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt;는 쿠버네티스 베어메탈 환경에서 &lt;strong&gt;IPVS 모드&lt;/strong&gt;와 &lt;strong&gt;MetalLB&lt;/strong&gt;를 함께 사용할 때, 네트워크 통신의 교통 정리를 담당하는 핵심 설정이다.&lt;/p&gt;

&lt;p&gt;리눅스 커널이 모든 IP에 대해 ARP 응답을 하지 못하도록 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arp_ignore&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arp_announce&lt;/code&gt;를 엄격하게 조정함으로써, &lt;strong&gt;MetalLB가 LoadBalancer IP에 대한 트래픽을 
온전히 제어할 수 있도록 보장&lt;/strong&gt;한다.&lt;br /&gt;
안정적인 온프레미스 서비스 운영을 위해서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strictARP&lt;/code&gt; 설정을 꼭 활성화하는 것을 권장한다.&lt;/p&gt;
</description>
        <pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-ipvs-strict-arp-deep-dive/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/22/kubernetes-metallb-ipvs-strict-arp-deep-dive/</guid>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>metallb</category>
        
        <category>load-balancer</category>
        
        <category>ipvs</category>
        
        <category>kube-proxy</category>
        
        <category>strict-arp</category>
        
        <category>arp-ignore</category>
        
        <category>arp-announce</category>
        
        <category>bare-metal-network</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>MetalLB Layer 2 모드 설정: CRs(IPAddressPool, L2Advertisement)</title>
        <description>&lt;p&gt;CRs(Custom Resources, 사용자 정의 리소스) 혹은 CR(Custom Resource)은 쿠버네티스 API를 확장하여 사용자가 직접 정의한 리소스 타입을 의미한다.&lt;/p&gt;

&lt;p&gt;쿠버네티스는 기본적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pod&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Service&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Deployment&lt;/code&gt;와 같은 빌트인 리소스를 제공한다.&lt;br /&gt;
하지만 특정 애플리케이션이나 오퍼레이터가 자신만의 설정이나 상태를 관리해야 할 때, 기본 리소스만으로는 부족할 수 있다.&lt;br /&gt;
이 때 &lt;strong&gt;CRD(Custom Resource Definition)&lt;/strong&gt;를 통해 새로운 리소스 타입을 정의하고, 이를 기반으로 생성한 인스턴스가 바로 &lt;strong&gt;CR&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;쉽게 말해서 &lt;strong&gt;쿠버네티스 스타일로 만든 개인 전용 설정 파일&lt;/strong&gt;이라고 이해하면 된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#metallb가-cr을-사용하는-이유&quot;&gt;MetalLB가 CR을 사용하는 이유&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#metallb-설정의-핵심-crs&quot;&gt;MetalLB 설정의 핵심 CRs&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#ipaddresspool&quot;&gt;IPAddressPool&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#l2advertisement&quot;&gt;L2Advertisement&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#cr과-crd의-관계&quot;&gt;CR과 CRD의 관계&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#정리하며&quot;&gt;정리하며..&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;metallb가-cr을-사용하는-이유&quot;&gt;MetalLB가 CR을 사용하는 이유&lt;/h1&gt;

&lt;p&gt;과거의 MetalLB(v0.12 이전)는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ConfigMap&lt;/code&gt;을 사용하여 설정을 관리했다. 하지만 현재 버전에서는 &lt;strong&gt;CRD 기반의 설정&lt;/strong&gt;을 권장한다. 그 이유는 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;유효성 검사&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;CRD 스키마를 통해 IP 형식이 올바른지, 필수값이 있는지 API 서버 레벨에서 미리 검증 가능&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;동적 적용&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;설정을 변경했을 때 MetalLB 컨트롤러가 이를 즉시 감지하고 반영하기가 훨씬 수월함&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;k8s 네이티브&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl get ipaddresspool&lt;/code&gt;과 같이 쿠버네티스 표준 명령어를 사용하여 설정을 조회하고 관리할 수 있음&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;metallb-설정의-핵심-crs&quot;&gt;MetalLB 설정의 핵심 CRs&lt;/h1&gt;

&lt;p&gt;MetalLB를 구성하기 위해 주로 다루게 되는 핵심 CR은 2가지가 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;ipaddresspool&quot;&gt;IPAddressPool&lt;/h2&gt;

&lt;p&gt;MetalLB가 로드밸런서 서비스에 할당할 &lt;strong&gt;IP 주소의 범위(Pool)&lt;/strong&gt;를 정의하는 리소스이다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;metallb.io/v1beta1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;IPAddressPool&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;first-pool&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mymetallb&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;addresses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;192.168.1.240-192.168.1.250&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;l2advertisement&quot;&gt;L2Advertisement&lt;/h2&gt;

&lt;p&gt;정의된 IP 주소 풀을 네트워크에 어떻게 알릴지(Advertisement) 설정하는 리소스이다.&lt;br /&gt;
Layer 2 모드(ARP 사용)로 동작할지, BGP 모드로 동작할지 결정하는 역할을 수행한다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;metallb.io/v1beta1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;L2Advertisement&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;l2-advert&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mymetallb&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ipAddressPools&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;first-pool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;cr과-crd의-관계&quot;&gt;CR과 CRD의 관계&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;CRD(Custom Resource Definition)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;붕어빵 틀과 같다. ‘IPAddressPool이라는 리소스는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;addresses&lt;/code&gt;라는 필드를 가져야 한다’라고 정의한다.&lt;/li&gt;
      &lt;li&gt;MetalLB 설치 시(Helm 차트 내 포함) 이미 클러스터에 등록되었다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;CR(Custom Resource)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;붕어빵 틀로 찍어낸 실제 붕어빵이다. 사용자가 작성하는 YAML 파일이 여기에 해당한다.&lt;/li&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl api-resources | grep metallb&lt;/code&gt; 명령어를 입력하면 현재 클러스터에 등록된 MetalLB 관련 CRD 목록을 확인할 수 있다.
 설치 메시지에서 “Now you can configure it via its CRs”라고 나오는 것은 이제 이 틀(CRD)에 맞춰 내용(CR)을 채워넣으라는 의미이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며..&lt;/h1&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;CR(Custom Resource)&lt;/strong&gt;은 쿠버네티스의 기능을 확장하여 특정 애플리케이션(여기서는 MetalLB) 전용 설정을 관리하는 방식이다.&lt;/li&gt;
  &lt;li&gt;MetalLB 설치 후 메시지에서 말하는 CRs 설정은 &lt;strong&gt;“어떤 IP 대역을 사용할 것인가(IPAddressPool)”&lt;/strong&gt;와 &lt;strong&gt;“어떻게 통신할 것인가(L2Advertisement)”&lt;/strong&gt;를 정의하는 YAML 파일을 작성하여 적용하라는 의미이다.&lt;/li&gt;
  &lt;li&gt;이를 통해 MetalLB는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;pending&amp;gt;&lt;/code&gt; 상태인 로드밸런서 서비스에 실제 IP를 부여할 수 있게 된다.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Mon, 22 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/22/kubernetes-custom-resource-definition-metallb-configuration/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/22/kubernetes-custom-resource-definition-metallb-configuration/</guid>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>metallb</category>
        
        <category>load-balancer</category>
        
        <category>custom-resource</category>
        
        <category>custom-resource-definition</category>
        
        <category>crd</category>
        
        <category>ip-address-pool</category>
        
        <category>l2-advertisement</category>
        
        <category>layer-2-mode</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Kubernetes - 스테이트풀셋(StatefulSet) vs 디플로이먼트(Deployment)</title>
        <description>&lt;p&gt;&lt;a href=&quot;https://assu10.github.io/dev/2025/12/07/kubernetes-pod-deployment-replicaset-guide/#2-%EB%94%94%ED%94%8C%EB%A1%9C%EC%9D%B4%EB%A8%BC%ED%8A%B8deployment&quot;&gt;디플로이먼트(Deployment)&lt;/a&gt;는 
쿠버네티스에서 가장 널리 사용되는 리소스 중 하나로, 웹 서버와 같이 상태가 없는(Stateless) 애플리케이션을 배포하고 관리하는데 최적화되어 있다.&lt;br /&gt;
디플로이먼트 환경에서 각 파드는 언제든지 삭제되고 새로운 파드로 대체될 수 있는, 이른바 &lt;strong&gt;‘대체 가능한(Fungible)’&lt;/strong&gt; 존재이다.&lt;/p&gt;

&lt;p&gt;하지만 데이터베이스나 분산 코디네이터(예: ZooKeeper)와 같이 &lt;strong&gt;상태(State)&lt;/strong&gt;를 유지해야 하는 애플리케이션을 운영해야 한다면 어떻게 해야 할까?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;스테이트풀셋(StatefulSet)&lt;/strong&gt;은 바로 이러한 요구사항을 충족하기 위해 설계되었다.&lt;br /&gt;
개념적으로는 디플로이먼트와 유사하게 파드 집합을 관리하지만, 그 동작 방식에는 결정적인 차이가 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;디플로이먼트(Deployment)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;각 파드는 서로 &lt;strong&gt;대체 가능&lt;/strong&gt;하다. 이름이 바뀌거나 IP가 변경되어도 서비스에 지장이 없는 경우에 사용한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;스테이트풀셋(StatefulSet)&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;각 파드는 동일한 스펙이라 하더라도 고유한 식별자를 가지며 &lt;strong&gt;독자성을 유지&lt;/strong&gt;한다. 따라서 하나의 파드를 다른 파드로 임의로 &lt;strong&gt;대체할 수 없다.&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;여기서는 쿠버네티스에서 상태가 있는 애플리케이션을 안정적으로 운영하기 위한 &lt;strong&gt;스테이트풀셋의 개념과 구체적인 사용 방법&lt;/strong&gt;에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-스테이트풀셋statefulset-개념&quot;&gt;1. 스테이트풀셋(StatefulSet) 개념&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-헤드리스-서비스headless-service&quot;&gt;2. 헤드리스 서비스(Headless Service)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-스테이트풀셋-생성&quot;&gt;3. 스테이트풀셋 생성&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-데이터베이스의-리플리케이션과-스테이트풀셋&quot;&gt;3.1. 데이터베이스의 리플리케이션과 스테이트풀셋&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-스테이트풀셋-접속-테스트&quot;&gt;4. 스테이트풀셋 접속 테스트&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#5-스테이트풀셋-볼륨&quot;&gt;5. 스테이트풀셋 볼륨&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#51-스케일링-이슈-정적-프로비저닝의-한계&quot;&gt;5.1. 스케일링 이슈: 정적 프로비저닝의 한계&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#정리하며&quot;&gt;정리하며..&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;개발 환경&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ubuntu 24.04.2 LTS&lt;/li&gt;
  &lt;li&gt;Mac Apple M3 Max&lt;/li&gt;
  &lt;li&gt;Memory 48 GB&lt;/li&gt;
  &lt;li&gt;Kubernetes: v1.29.15&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-스테이트풀셋statefulset-개념&quot;&gt;1. 스테이트풀셋(StatefulSet) 개념&lt;/h1&gt;

&lt;p&gt;쿠버네티스에서 &lt;strong&gt;스테이트풀셋&lt;/strong&gt;은 애플리케이션의 상태(State)를 관리해야 하는 워크로드 API 오브젝트이다.&lt;br /&gt;
여기서 워크로드는 쿠버네티스 상에서 구동되는 애플리케이션을 의미한다.&lt;/p&gt;

&lt;p&gt;위에서 언급했듯이 스테이트풀셋은 파드를 관리한다는 점에서 디플로이먼트와 유사하지만 &lt;strong&gt;파드의 독자성(Identity)&lt;/strong&gt;이 있다는 점에서 차이가 있다.&lt;br /&gt;
디플로이먼트에서의 각 파드는 이름이 바뀌거나 IP가 변경되어도 각 파드가 서로 대체가 가능하지만, 스테이트풀셋은 각 파드가 고유한 식별자를 가지므로 파드 간 대체가 불가능하다.&lt;/p&gt;

&lt;p&gt;스테이트풀셋으로 생성된 파드들은 동일한 컨테이너 스펙을 가지고 있더라도 서로 교체될 수 없으며, 파드가 재스케줄링 되더라도 &lt;strong&gt;영구적인 식별자&lt;/strong&gt;를 유지한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;언제 스테이트풀셋을 사용해야 할까?&lt;/strong&gt;&lt;br /&gt;
스테이트풀셋은 아래와 같은 요구사항이 있을 때 유용하다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;안정적이고 고유한 네트워크 식별자&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;파드가 재시작되어도 변하지 않는 호스트 이름이 필요할 때&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;안정적인 퍼시스턴트 스토리지&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;각 파드가 자신만의 고유한 데이터를 유지해야 할 때&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;순차적인 배포와 스케일링&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;순서에 맞춰 파드를 생성하거나 제거해야 할 때&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;순차적이고 자동화된 롤링 업데이트&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;롤링 업데이트: 구버전 파드를 하나씩 줄이고 신버전 파드를 하나씩 늘리는 방식으로 배포를 진행&lt;/li&gt;
      &lt;li&gt;정해진 순서대로 업데이트가 필요할 때&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-헤드리스-서비스headless-service&quot;&gt;2. 헤드리스 서비스(Headless Service)&lt;/h1&gt;

&lt;p&gt;스테이트풀셋을 제대로 활용하기 위해서는 파드들의 개별 네트워크를 식별해 줄 서비스가 필요하다. 이 때 사용하는 것이 바로 &lt;strong&gt;헤드리스 서비스&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;헤드리스 서비스의 가장 큰 특징은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/08/kubernetes-service-concept-and-types/#2-clusterip&quot;&gt;ClusterIP&lt;/a&gt;가 없는 것이다.&lt;br /&gt;
일반적인 서비스가 부하 분산(LoadBalancing)을 위해 가상의 IP(ClusterIP)를 가지는 것과 달리, 헤드리스 서비스는 DNS 쿼리에 대해 파드들의 IP 주소 목록을 직접 반환한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;실습 환경 준비&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;먼저 실습을 위한 디렉터리를 생성하고 정리한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01  ex02  ex03  ex04  ex05  ex06  ex07  ex08  ex09  ex10  ex11

assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex12
assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex12
assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;헤드리스 서비스 매니페스트 작성(statefulset-service.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ClusterIP를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;None&lt;/code&gt;으로 설정하여 헤드리스 서비스를 정의한다.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim statefulset-service.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Service&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-service01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 어떤 스테이트풀셋과 연동할지 정의&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# 이 서비스가 트래픽을 전달할 파드를 선택하는 라벨(이후 생성할 스테이트풀셋과 일치해야 함)&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-sfs01&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ClusterIP&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 서비스 타입 정의&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;clusterIP&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 핵심: None으로 설정하여 헤드리스 서비스로 만듦&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;protocol&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;TCP&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;헤드리스 서비스 실행 및 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;작성한 서비스를 생성하고 정보를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; statefulset-service.yml
service/sfs-service01 created

assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME                    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE   SELECTOR
service/kubernetes      ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   12d   &amp;lt;none&amp;gt;
service/sfs-service01   ClusterIP   None         &amp;lt;none&amp;gt;        80/TCP    24s   app.kubernetes.io/name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;web-sfs01
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 결과에서 볼 수 있듯이 sfs-service01의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLUSTER-IP&lt;/code&gt;가 &lt;strong&gt;None&lt;/strong&gt; 으로 설정된 것을 확인할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-스테이트풀셋-생성&quot;&gt;3. 스테이트풀셋 생성&lt;/h1&gt;

&lt;p&gt;이제 헤드리스 서비스와 연동될 스테이트풀셋을 생성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim statefulset-web01.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;StatefulSet&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-test01&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 스테이트풀셋 리소스의 이름&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 스테이트풀셋 상태 정의&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;3&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 스테이트풀셋이 생성할 파드의 개수 정의(0,1,2 순서로 생성됨)&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 스테이트풀셋의 selector의 matchLabels 를 통해 파드를 하나로 묶어줌&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-sfs01&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;serviceName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-service01&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 중요: 앞서 생성한 헤드리스 서비스의 이름을 지정하여 네트워크 식별자 생성&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# selector.matchLabels와 동일해야 함&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-sfs01&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;스테이트풀셋과 헤드리스 서비스의 연결&lt;/strong&gt;&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.serviceName&lt;/code&gt; 필드는 스테이트풀셋 내의 파드들이 네트워크 도메인을 가질 수 있도록 헤드리스 서비스를 지정하는 역할을 한다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1221/yml.png&quot; alt=&quot;스테이트풀셋과 헤드리스 서비스 연결&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;스테이트풀셋 실행 및 파드 이름 확인&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; statefulset-web01.yml

assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME               READY   STATUS    RESTARTS   AGE   IP                NODE         NOMINATED NODE   READINESS GATES
pod/sfs-test01-0   1/1     Running   0          10s   192.168.131.87    myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
pod/sfs-test01-1   1/1     Running   0          7s    192.168.149.209   myserver03   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
pod/sfs-test01-2   1/1     Running   0          4s    192.168.131.88    myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

NAME                    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE   SELECTOR
service/kubernetes      ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   12d   &amp;lt;none&amp;gt;
service/sfs-service01   ClusterIP   None         &amp;lt;none&amp;gt;        80/TCP    21m   app.kubernetes.io/name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;web-sfs01

NAME                          READY   AGE   CONTAINERS   IMAGES
statefulset.apps/sfs-test01   3/3     10s   nginx        nginx:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;실행 결과를 보면 파드의 이름이 규칙적임을 알 수 있다.&lt;br /&gt;
디플로이먼트가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pod-name-x9d2s&lt;/code&gt; 처럼 무작위 해시값을 사용하는 것과 달리, 스테이트풀셋은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;스테이트풀셋이름&amp;gt;-&amp;lt;순서(ordinal)&amp;gt;&lt;/code&gt; 형식(0부터 시작)으로 이름을 부여한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;sfs-test01-0&lt;/li&gt;
  &lt;li&gt;sfs-test01-1&lt;/li&gt;
  &lt;li&gt;sfs-test01-2&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;스테이트풀셋 스케일링(Scale Down) 테스트&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이미 실행 중인 파드 개수를 줄였을 때(Scale Down), 어떤 파드가 삭제되는지 확인해보자.&lt;br /&gt;
디플로이먼트는 랜덤하게 파드를 삭제했지만, 스테이트풀셋은 다르다.&lt;/p&gt;

&lt;p&gt;파드 개수(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replicas&lt;/code&gt;)를 3개에서 2개로 줄여보자.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim statefulset-web02.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;StatefulSet&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-test01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 기존 3개에서 2개로 변경&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-sfs01&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;serviceName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-service01&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; 
        &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-sfs01&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;변경 사항을 반영한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; statefulset-web02.yml
statefulset.apps/sfs-test01 configured
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME               READY   STATUS    RESTARTS   AGE     IP                NODE         NOMINATED NODE   READINESS GATES
pod/sfs-test01-0   1/1     Running   0          4m23s   192.168.131.87    myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
pod/sfs-test01-1   1/1     Running   0          4m20s   192.168.149.209   myserver03   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

NAME                    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE   SELECTOR
service/kubernetes      ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   12d   &amp;lt;none&amp;gt;
service/sfs-service01   ClusterIP   None         &amp;lt;none&amp;gt;        80/TCP    25m   app.kubernetes.io/name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;web-sfs01

NAME                          READY   AGE     CONTAINERS   IMAGES
statefulset.apps/sfs-test01   2/2     4m23s   nginx        nginx:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;결과를 보면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sfs-test01-1&lt;/code&gt; 파드만 삭제되고, 0번과 1번 파드는 유지된 것을 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;이처럼 스테이트풀셋은 스케일 다운 시 &lt;strong&gt;가장 나중에 생성된 파드(가장 높은 번호)부터 순차적으로 삭제&lt;/strong&gt;한다.&lt;br /&gt;
이는 데이터베이스의 리플리케이션 등 순서가 중요한 환경에서 매우 중요한 특성이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-데이터베이스의-리플리케이션과-스테이트풀셋&quot;&gt;3.1. 데이터베이스의 리플리케이션과 스테이트풀셋&lt;/h2&gt;

&lt;p&gt;데이터베이스 리플리케이션이란, 데이터의 안정성과 가용성을 높이기 위해 원본 데이터베이스의 데이터를 복제하여 하나 이상의 복제본 서버에 실시간 혹은 비동기로 동기화하는 기술을 말한다.&lt;/p&gt;

&lt;p&gt;대부분의 스테이트풀(Stateful) 애플리케이션, 특히 데이터베이스는 Primary(Master)-Replica(Slave) 구조를 가진다.&lt;/p&gt;

&lt;p&gt;Primary는 주 서버로 보통 쓰기와 읽기는 모두 담당하며, 데이터의 원본이 된다.&lt;br /&gt;
StatefulSet에서는 관례적으로 인덱스 번호가 가장 낮은 &lt;strong&gt;Pod-0&lt;/strong&gt;이 이 역할을 맡는 경우가 많다.&lt;/p&gt;

&lt;p&gt;Replica는 Primary의 데이터를 복제해 오며, 보통 읽기 전용으로 쓰거나 Primary가 죽었을 때를 대비한 예비 서버 역할을 한다. (&lt;strong&gt;Pod-1, Pod-2 등&lt;/strong&gt;)&lt;/p&gt;

&lt;p&gt;스케일 다운 시 &lt;strong&gt;가장 높은 번호(가장 나중에 생성된 파드)부터 역순으로 삭제하는 이유&lt;/strong&gt;는 &lt;strong&gt;데이터의 무결성과 클러스터의 Leader를 보호&lt;/strong&gt;하기 위해서이다.&lt;br /&gt;
랜덤으로 삭제하다가 만일 Pod-0(Primary)이 삭제된다면 데이터의 원본이 사라져 남은 Pod-1, Pod-2는 새로운 리더를 선출(Election)하는 과정을 거쳐야 하는데 
이 과정에서 &lt;strong&gt;서비스 중단(Downtime)&lt;/strong&gt;이나 &lt;strong&gt;데이터 유실&lt;/strong&gt;이 발생할 수 있다.&lt;/p&gt;

&lt;p&gt;요약하면 아래와 같다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;리플리케이션 구조 보호&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;데이터의 원본인 Primary(낮은 인덱스)를 끝까지 살려두어 서비스 지속성 보장&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 정합성 유지&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;복제본(Replica)부터 순차적으로 제거함으로써 리더 선출에 따른 불필요한 장애 상황 방지&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이것이 &lt;strong&gt;Stateless한 웹 서버를 다루는 Deployment와의 가장 큰 차이점&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-스테이트풀셋-접속-테스트&quot;&gt;4. 스테이트풀셋 접속 테스트&lt;/h1&gt;

&lt;p&gt;스테이트풀셋으로 생성된 파드들이 정상적으로 네트워크 통신이 가능한지 확인해보자.&lt;br /&gt;
외부에서 직접 접속하는 대신, 클러스터 내부에 테스트용 임시 파드(Nginx)를 생성하여 내부 통신을 시도한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;테스트용 파드 생성(nginx-test01.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;먼저 curl 명령어를 사용할 수 있는 Nginx 파드를 하나 생성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim nginx-test01.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-test01&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; nginx-test01.yml
pod/nginx01 created
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;파드가 정상적으로 실행(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Running&lt;/code&gt;) 상태가 될 때까지 기다린 후 IP 정보를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME               READY   STATUS    RESTARTS   AGE     IP                NODE         NOMINATED NODE   READINESS GATES
pod/nginx01        1/1     Running   0          6s      192.168.131.89    myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
pod/sfs-test01-0   1/1     Running   0          7m34s   192.168.131.87    myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
pod/sfs-test01-1   1/1     Running   0          7m31s   192.168.149.209   myserver03   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

NAME                    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE   SELECTOR
service/kubernetes      ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   12d   &amp;lt;none&amp;gt;
service/sfs-service01   ClusterIP   None         &amp;lt;none&amp;gt;        80/TCP    28m   app.kubernetes.io/name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;web-sfs01

NAME                          READY   AGE     CONTAINERS   IMAGES
statefulset.apps/sfs-test01   2/2     7m34s   nginx        nginx:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;파드 간 통신 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이제 nginx01 파드 내부로 진입하여 스테이트풀셋의 첫 번째 파드(sfs-test01-0, IP: 192.168.131.87)로 HTTP 요청을 보내보자.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; nginx01 &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /bin/bash

&lt;span class=&quot;c&quot;&gt;# 스테이트풀셋 파드(sfs-test01-0)의 IP로 접속 시도&lt;/span&gt;
root@nginx01:/# curl 192.168.131.87
&amp;lt;&lt;span class=&quot;o&quot;&gt;!&lt;/span&gt;DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;&lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&amp;lt;title&amp;gt;Welcome to nginx!&amp;lt;/title&amp;gt;
&amp;lt;style&amp;gt;
html &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt; color-scheme: light dark&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
...

root@nginx01:/# &lt;span class=&quot;nb&quot;&gt;exit
exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Nginx의 환영 메시지가 출력되는 것으로 보아 파드 간 네트워크 통신이 원활함을 확인할 수 있다.&lt;br /&gt;
테스트가 끝났으므로 리소스를 정리한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete pods &lt;span class=&quot;nt&quot;&gt;--all&lt;/span&gt;
pod &lt;span class=&quot;s2&quot;&gt;&quot;nginx01&quot;&lt;/span&gt; deleted
pod &lt;span class=&quot;s2&quot;&gt;&quot;sfs-test01-0&quot;&lt;/span&gt; deleted
pod &lt;span class=&quot;s2&quot;&gt;&quot;sfs-test01-1&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete services,statefulsets &lt;span class=&quot;nt&quot;&gt;--all&lt;/span&gt;
service &lt;span class=&quot;s2&quot;&gt;&quot;kubernetes&quot;&lt;/span&gt; deleted
service &lt;span class=&quot;s2&quot;&gt;&quot;sfs-service01&quot;&lt;/span&gt; deleted
statefulset.apps &lt;span class=&quot;s2&quot;&gt;&quot;sfs-test01&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   27s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-스테이트풀셋-볼륨&quot;&gt;5. 스테이트풀셋 볼륨&lt;/h1&gt;

&lt;p&gt;스테이트풀셋의 강력한 기능 중 하나는 &lt;strong&gt;각 파드마다 고유한 스토리지를 자동으로 생성하고 연결&lt;/strong&gt;할 수 있다는 점이다.&lt;br /&gt;
이를 이해하기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumeClaimTemplates&lt;/code&gt;를 사용해보자.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;헤드리스 서비스 재실행&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;#2-헤드리스-서비스headless-service&quot;&gt;2. 헤드리스 서비스(Headless Service)&lt;/a&gt;에서 작성했던 statefulset-service.yml 을 다시 실행한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; statefulset-service.yml
service/sfs-service01 created

assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME                    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE   SELECTOR
service/kubernetes      ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   10m   &amp;lt;none&amp;gt;
service/sfs-service01   ClusterIP   None         &amp;lt;none&amp;gt;        80/TCP    6s    app.kubernetes.io/name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;web-sfs01
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;정적 프로비저닝: PV(PersistentVolume) 생성(statefulset-vol01-pv.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;테스트를 위한 물리적 볼륨 역할을 할 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;를 생성한다.&lt;br /&gt;
여기서는 호스트 경로(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;)를 사용한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim statefulset-vol01-pv.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;PersistentVolume&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pv-sfs01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# PersistentVolume의 내부 상태 정의&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;accessModes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;capacity&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;100Mi&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 100MB 용량&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;persistentVolumeReclaimPolicy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Retain&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;storageClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pv-sfs-test01&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 중요: 이 클래스 이름을 기억해야 함&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 볼륨 타입은 hostPath&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/home/assu/work/volhost01&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DirectoryOrCreate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;persistentVolumeReclaimPolicy&lt;/code&gt; 에 대한 설명은 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/20/kubernetes-volume-basic-emptydir-hostpath-pv/#42-pv-%EB%B0%8F-pvc-%EC%83%9D%EC%84%B1&quot;&gt;4.2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt; 및 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt; 생성&lt;/a&gt;을 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; statefulset-vol01-pv.yml
persistentvolume/pv-sfs01 created

assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pv &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS    VOLUMEATTRIBUTESCLASS   REASON   AGE   VOLUMEMODE
pv-sfs01   100Mi      RWO            Retain           Available           pv-sfs-test01   &amp;lt;&lt;span class=&quot;nb&quot;&gt;unset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;                          11s   Filesystem

&lt;span class=&quot;c&quot;&gt;# 스테이트볼륨은 kubectl get all 로 조회되지 않음&lt;/span&gt;
assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME                    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE     SELECTOR
service/kubernetes      ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   4h50m   &amp;lt;none&amp;gt;
service/sfs-service01   ClusterIP   None         &amp;lt;none&amp;gt;        80/TCP    4h39m   app.kubernetes.io/name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;web-sfs01
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumeClaimTemplates&lt;/code&gt;를 이용한 스테이트풀셋 생성(statefulset-vol02.yml)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이제 스테이트풀셋을 정의한다.&lt;br /&gt;
여기서 핵심은 &lt;strong&gt;PVC를 별도로 생성하지 않고, 스테이트풀셋 명세 안에 템플릿으로 정의&lt;/strong&gt;한다는 점이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim statefulset-vol02.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;StatefulSet&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
 &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-test01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
 &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
 &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
   &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-sfs01&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 스테이트풀셋이 관리할 앱 정의&lt;/span&gt;
 &lt;span class=&quot;na&quot;&gt;serviceName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-service01&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 스테이트풀셋이 사용할 헤드리스 서비스 정의&lt;/span&gt;
 &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
   &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-sfs01&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# selector.matchLabels.app.kubernetes.io/name과 동일하게 설정&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
   &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 컨테이너 내부에 마운트할 볼륨 정보 설정&lt;/span&gt;
       &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-vol01&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 아래 templates에서 정의한 이름과 일치&lt;/span&gt;
         &lt;span class=&quot;na&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/mount01&lt;/span&gt;
 &lt;span class=&quot;na&quot;&gt;volumeClaimTemplates&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# PVC 역할, 따라서 스테이트풀셋이 볼륨을 사용할 때는 PVC 파일을 따로 작성하지 않음&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-vol01&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;accessModes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ReadWriteOnce&quot;&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;storageClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pv-sfs-test01&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# statefulset-vol01-pv.yml 의 storageClassName과 동일해야 함&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
       &lt;span class=&quot;na&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;20Mi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;스테이트풀셋을 실행하고 파드가 정상적으로 생성되는지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod,pv,pvc &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME               READY   STATUS    RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
pod/sfs-test01-0   1/1     Running   0          17m   192.168.131.91   myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

NAME                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                            STORAGECLASS    VOLUMEATTRIBUTESCLASS   REASON   AGE   VOLUMEMODE
persistentvolume/pv-sfs01   100Mi      RWO            Retain           Bound    default/sfs-vol01-sfs-test01-0   pv-sfs-test01   &amp;lt;&lt;span class=&quot;nb&quot;&gt;unset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;                          22m   Filesystem

NAME                                           STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS    VOLUMEATTRIBUTESCLASS   AGE   VOLUMEMODE
persistentvolumeclaim/sfs-vol01-sfs-test01-0   Bound    pv-sfs01   100Mi      RWO            pv-sfs-test01   &amp;lt;&lt;span class=&quot;nb&quot;&gt;unset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;                 17m   Filesystem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;결과를 보면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt; 이름이 &lt;em&gt;sfs-vol01-sfs-test01-0&lt;/em&gt; 으로 생성된 것을 볼 수 있다.&lt;br /&gt;
이는 다음과 같은 규칙으로 자동 생성된 것이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;PVC 이름 규칙&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;volumeClaimTemplates 이름&amp;gt;-&amp;lt;파드 이름&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1221/yml2.png&quot; alt=&quot;헤드리스 서비스, 스테이트풀셋, 볼륨 간의 관계&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;51-스케일링-이슈-정적-프로비저닝의-한계&quot;&gt;5.1. 스케일링 이슈: 정적 프로비저닝의 한계&lt;/h2&gt;

&lt;p&gt;만약 파드 개수(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replicas&lt;/code&gt;)를 1개에서 2개로 늘리면 어떻게 될까?&lt;br /&gt;
기존의 statefulset-vol02.yml 에서 파드 개수만 2개로 늘려서 적용해보자.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim statefulset-vol03.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;StatefulSet&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-test01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 파드 개수만 2개로 변경&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-sfs01&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;serviceName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-service01&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web-sfs01&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-vol01&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/mount01&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;volumeClaimTemplates&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sfs-vol01&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;accessModes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ReadWriteOnce&quot;&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;storageClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pv-sfs-test01&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;na&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;20Mi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; statefulset-vol03.yml
statefulset.apps/sfs-test01 configured

assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME               READY   STATUS    RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
pod/sfs-test01-0   1/1     Running   0          30m   192.168.131.91   myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
pod/sfs-test01-1   0/1     Pending   0          16s   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

NAME                    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE     SELECTOR
service/kubernetes      ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   5h26m   &amp;lt;none&amp;gt;
service/sfs-service01   ClusterIP   None         &amp;lt;none&amp;gt;        80/TCP    5h15m   app.kubernetes.io/name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;web-sfs01

NAME                          READY   AGE   CONTAINERS   IMAGES
statefulset.apps/sfs-test01   1/2     30m   nginx        nginx:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;상태를 확인해보면 두 번째 파드인 sfs-test01-1이 생성되지 못하고 &lt;strong&gt;Pending&lt;/strong&gt; 상태에 머물러있다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME               READY   STATUS    RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
pod/sfs-test01-0   1/1     Running   0          30m   192.168.131.91   myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

&lt;span class=&quot;c&quot;&gt;# 문제 발생&lt;/span&gt;
pod/sfs-test01-1   0/1     Pending   0          16s   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;원인을 파악하기 위해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt; 상태를 확인해보자.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod,pv,pvc &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME               READY   STATUS    RESTARTS   AGE     IP               NODE         NOMINATED NODE   READINESS GATES
pod/sfs-test01-0   1/1     Running   0          32m     192.168.131.91   myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;span class=&quot;c&quot;&gt;# 새롭게 생긴 파드의 상태가 Pending&lt;/span&gt;
pod/sfs-test01-1   0/1     Pending   0          2m13s   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;       &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

&lt;span class=&quot;c&quot;&gt;# PV 는 정상&lt;/span&gt;
NAME                        CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                            STORAGECLASS    VOLUMEATTRIBUTESCLASS   REASON   AGE   VOLUMEMODE
persistentvolume/pv-sfs01   100Mi      RWO            Retain           Bound    default/sfs-vol01-sfs-test01-0   pv-sfs-test01   &amp;lt;&lt;span class=&quot;nb&quot;&gt;unset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;                          37m   Filesystem

&lt;span class=&quot;c&quot;&gt;# PVC가 2개 존재하는데 sfs-vol01-sfs-test01-0 은 정상 동작&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# sfs-vol01-sfs-test01-1은 Pending 상태임&lt;/span&gt;
NAME                                           STATUS    VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS    VOLUMEATTRIBUTESCLASS   AGE     VOLUMEMODE
persistentvolumeclaim/sfs-vol01-sfs-test01-0   Bound     pv-sfs01   100Mi      RWO            pv-sfs-test01   &amp;lt;&lt;span class=&quot;nb&quot;&gt;unset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;                 32m     Filesystem
persistentvolumeclaim/sfs-vol01-sfs-test01-1   Pending                                        pv-sfs-test01   &amp;lt;&lt;span class=&quot;nb&quot;&gt;unset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;                 2m13s   Filesystem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Pending 상태였던 sfs-vol01-sfs-test01-1 PVC 에 대해 자세히 알아보자.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl describe pvc sfs-vol01-sfs-test01-1
Name:          sfs-vol01-sfs-test01-1
Namespace:     default
StorageClass:  pv-sfs-test01
Status:        Pending
Volume:
Labels:        app.kubernetes.io/name&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;web-sfs01
Annotations:   &amp;lt;none&amp;gt;
Finalizers:    &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;kubernetes.io/pvc-protection]
Capacity:
Access Modes:
VolumeMode:    Filesystem
Used By:       sfs-test01-1
Events:
  Type     Reason              Age                  From                         Message
  &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;     &lt;span class=&quot;nt&quot;&gt;------&lt;/span&gt;              &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;                 &lt;span class=&quot;nt&quot;&gt;----&lt;/span&gt;                         &lt;span class=&quot;nt&quot;&gt;-------&lt;/span&gt;
  Warning  ProvisioningFailed  3s &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;x20 over 4m37s&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;  persistentvolume-controller  storageclass.storage.k8s.io &lt;span class=&quot;s2&quot;&gt;&quot;pv-sfs-test01&quot;&lt;/span&gt; not found
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;왜 이런 일이 발생한 걸까?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;스테이트풀셋은 두 번째 파드(…-1)을 위해 새로운 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;(…-1)를 자동으로 생성했다.&lt;/li&gt;
  &lt;li&gt;이 새로운 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storageClassName: pv-sfs-test01&lt;/code&gt;을 가진 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;를 요청했다.&lt;/li&gt;
  &lt;li&gt;하지만 우리가 수동으로 만든 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;(pv-sfs01)은 이미 첫 번째 파드의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;에 바인딩(Bound)되어 사용 중이다.&lt;/li&gt;
  &lt;li&gt;남는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;가 없으므로 두 번째 파드는 스토리지를 할당받지 못해 Pending 상태가 된다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;해결책: 동적 프로비저닝(Dynamic Provisioning)&lt;/strong&gt;&lt;br /&gt;
실제 운영 환경에서는 파드가 늘어날 때마다 관리자가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;를 일일이 만들어줄 수 없다.&lt;br /&gt;
따라서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Rook&lt;/code&gt; 이나 클라우드 제공업체(AWS EBS 등)의 스토리지 클래스를 사용하여, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt; 요청이 들어오면 &lt;strong&gt;자동으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;를 생성하고 연결해주는 동적 프로비저닝&lt;/strong&gt;을 사용해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;이제 리소스를 정리한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; statefulset-vol03.yml
statefulset.apps &lt;span class=&quot;s2&quot;&gt;&quot;sfs-test01&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete pv,pvc &lt;span class=&quot;nt&quot;&gt;--all&lt;/span&gt;
persistentvolume &lt;span class=&quot;s2&quot;&gt;&quot;pv-sfs01&quot;&lt;/span&gt; deleted
persistentvolumeclaim &lt;span class=&quot;s2&quot;&gt;&quot;sfs-vol01-sfs-test01-0&quot;&lt;/span&gt; deleted
persistentvolumeclaim &lt;span class=&quot;s2&quot;&gt;&quot;sfs-vol01-sfs-test01-1&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; statefulset-service.yml
service &lt;span class=&quot;s2&quot;&gt;&quot;sfs-service01&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex12&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   5h34m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리하며&quot;&gt;정리하며..&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;스테이트풀셋의 독자성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;디플로이먼트의 파드는 대체 가능하지만, 스테이트풀셋의 파드는 고유한 식별자를 가지기 때문에 파드가 재시작되어도 이름과 네트워크 ID가 유지된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;헤드리스 서비스&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterIP: None&lt;/code&gt;으로 설정된 서비스를 통해 로드밸런싱 없이 각 파드의 고유 IP 주소를 DNS로 직접 반환받아 통신한다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;순차적 관리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;파드의 생성, 삭제, 스케일링이 순차적(0,1,2..)으로 이루어져 데이터의 정합성과 순서가 중요한 애플리케이션에 적합하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;스토리지 관리&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumeClaimTemplates&lt;/code&gt;를 사용하면 각 파드마다 전용 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;를 자동으로 생성하여 고유한 데이터를 안전하게 저장할 수 있다.&lt;/li&gt;
      &lt;li&gt;실무에서는 유연한 확장을 위해 동적 프로비저닝 환경 구성이 필수적이다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;데이터베이스나 분산 시스템을 쿠버네티스 위에 구축할 때 스테이트풀셋은 선택이 아닌 필수이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 장철원 저자의 &lt;strong&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/126115324&quot;&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/losskatsu/DockerKubernetes&quot;&gt;예제 코드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sun, 21 Dec 2025 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/21/kubernetes-statefulset-vs-deployment-difference/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/21/kubernetes-statefulset-vs-deployment-difference/</guid>
        
        <category>devops</category>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>statefulset-vs-deployment</category>
        
        <category>statefulset</category>
        
        <category>headless-service</category>
        
        <category>k8s-networking</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>Kubernetes - 스토리지 볼륨(emptyDir, hostPath, PV)</title>
        <description>&lt;p&gt;컨테이너는 기본적으로 ‘Stateless(상태가 없는)’ 특성을 지향하며, 변경 가능한 파일 시스템 레이어를 가지고 있지만 이는 컨테이너가 삭제되는 순간 함께 소멸한다.&lt;br /&gt;
도커를 다뤄본 사람이라면 이러한 문제를 해결하기 위해 &lt;a href=&quot;https://assu10.github.io/dev/2025/11/16/docker-basics-and-commands-guide-2/#2-%EB%8F%84%EC%BB%A4-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80&quot;&gt;&lt;strong&gt;도커 볼륨(Docker Volume)&lt;/strong&gt;&lt;/a&gt;을 
마운트하여 컨테이너 라이프사이클과 데이터를 분리했던 경험이 있을 것이다.&lt;/p&gt;

&lt;p&gt;쿠버네티스 환경에서도 이 원칙은 동일하게 적용된다.&lt;br /&gt;
파드 내의 컨테이너가 오류로 인해 재시작되거나, 파드 자체가 삭제될 때 내부 데이터가 유실되는 것을 막기 위해 &lt;strong&gt;스토리지 볼륨(Storage Volume)&lt;/strong&gt;이라는 개념을 사용한다.&lt;/p&gt;

&lt;p&gt;하지만 쿠버네티스의 볼륨은 도커의 볼륨보다 훨씬 더 다양한 기능을 제공한다.&lt;br /&gt;
단순히 디스크를 보존하는 것을 넘어, 파드 내 컨테이너 간의 데이터 공유나 클러스터 외부 스토리지와의 연동 등 복잡한 요구사항을 처리할 수 있다.&lt;/p&gt;

&lt;p&gt;이 포스트에서는 쿠버네티스 볼륨의 가장 기본이 되는 볼륨 타입인 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt; 그리고 영구 스토리지를 위한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV(PersistentVolume)&lt;/code&gt;에 대해 알아본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;!-- TOC --&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;#1-스토리지-볼륨storage-volume-개념&quot;&gt;1. 스토리지 볼륨(Storage Volume) 개념&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#2-emptydir&quot;&gt;2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#21-emptydir-생명주기-확인&quot;&gt;2.1. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt; 생명주기 확인&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#3-hostpath&quot;&gt;3. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#31-노드-간-데이터-공유-확인&quot;&gt;3.1. 노드 간 데이터 공유 확인&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#4-pvpersistentvolume&quot;&gt;4. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;(PersistentVolume)&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#41-nfs-서버-구축&quot;&gt;4.1. NFS 서버 구축&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#42-pv-및-pvc-생성&quot;&gt;4.2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt; 및 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt; 생성&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#43-파드에서-pvc-사용&quot;&gt;4.3. 파드에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt; 사용&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#44-데이터-영속성-검증&quot;&gt;4.4. 데이터 영속성 검증&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#45-리소스-삭제-및-데이터-보존-확인&quot;&gt;4.5. 리소스 삭제 및 데이터 보존 확인&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/a&gt;
&lt;!-- TOC --&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;개발 환경&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ubuntu 24.04.2 LTS&lt;/li&gt;
  &lt;li&gt;Mac Apple M3 Max&lt;/li&gt;
  &lt;li&gt;Memory 48 GB&lt;/li&gt;
  &lt;li&gt;Kubernetes: v1.29.15&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-스토리지-볼륨storage-volume-개념&quot;&gt;1. 스토리지 볼륨(Storage Volume) 개념&lt;/h1&gt;

&lt;p&gt;쿠버네티스를 사용할 때 헷갈리는 부분 중 하나는 &lt;strong&gt;데이터의 수명&lt;/strong&gt;이다.&lt;br /&gt;
도커와 마찬가지로 쿠버네티스 컨테이너 내부의 파일 시스템은 기본적으로 &lt;strong&gt;일시적(Ephemeral)&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;즉, 컨테이너가 정상적으로 실행되다가 오류로 인해 셧다운 되고, 쿠버네티스 컨트롤러에 의해 재시작되면 기존 컨테이너 내부에 저장해 뒀던 로그나 데이터 파일이 모두 
사라진 상태의 컨테이너가 새롭게 뜬다.&lt;/p&gt;

&lt;p&gt;이러한 데이터 유실 문제를 해결하기 위해 쿠버네티스는 &lt;strong&gt;스토리지 볼륨&lt;/strong&gt;이라는 추상화된 개념을 제공한다.&lt;br /&gt;
파드가 실행되는 동안 데이터를 보존하거나, 파드가 재시작되더라도 데이터를 유지할 수 있게 하는 것이다.&lt;/p&gt;

&lt;p&gt;쿠버네티스의 볼륨은 저장 위치와 생명 주기에 따라 크게 세 가지 유형으로 나눌 수 있다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: center&quot;&gt;볼륨 유형&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;설명&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;데이터 보존 범위&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;파드 내부에서 임시적으로 사용하는 볼륨&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;파드 생명주기와 동일(파드 삭제 시 삭제됨)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;노드의 파일 시스템을 사용하는 볼륨&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;노드 생명주기와 동일(파드는 삭제되어도 데이터 유지)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: center&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PersistentVolume&lt;/code&gt;&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;클러스터 외부의 전문 스토리지 시스템 사용&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;영구적(클러스터나 노드가 바뀌어도 유지)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1220/storage.png&quot; alt=&quot;스토리지 볼륨&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-emptydir&quot;&gt;2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt;&lt;/h1&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt;은 이름 그대로 &lt;strong&gt;비어 있는 디렉터리&lt;/strong&gt;로 시작하는 가장 기본적인 볼륨 타입이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1220/emptydir.png&quot; alt=&quot;emptyDir&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;생명주기&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;파드가 노드에 할당될 때 생성되며, &lt;strong&gt;파드가 실행되는 동안에만 존재&lt;/strong&gt;한다. 파드가 삭제되면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt; 내부의 데이터도 영구적으로 삭제된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;데이터 공유&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;하나의 파드 내에 여러 개의 컨테이너가 있을 경우(예: &lt;a href=&quot;https://assu10.github.io/dev/2025/12/20/sidecar-pattern/&quot;&gt;사이드카 패턴(Sidecar Pattern)&lt;/a&gt;), 이 컨테이너들은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt; 볼륨을 공유하여 동일한 파일을 읽고 쓸 수 있다.&lt;/li&gt;
      &lt;li&gt;다른 파드에서는 접근이 불가하다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;활용 사례&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;디스크 기반의 병합 정렬(Merge sort)과 같은 임시 대용량 연산&lt;/li&gt;
      &lt;li&gt;크래시 복구 등을 위한 체크포인트 임시 저장&lt;/li&gt;
      &lt;li&gt;웹 서버 컨테이너가 서빙할 데이터를 콘텐츠 매니저 컨테이너가 갱신하는 경우&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;21-emptydir-생명주기-확인&quot;&gt;2.1. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt; 생명주기 확인&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt;을 사용하여 파드가 재시작되었을 때와 파드가 삭제되었을 때 데이터가 어떻게 되는지 직접 확인해보자.&lt;/p&gt;

&lt;p&gt;먼저 디렉터리를 생성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;➜  ~ ssh &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 2201 assu@127.0.0.1

assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;
/home/assu/work/ch09
assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01  ex02  ex03  ex04  ex05  ex06  ex07  ex08

assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex09
assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01  ex02  ex03  ex04  ex05  ex06  ex07  ex08  ex09

assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex09
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;파드 정의(YAML)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;nginx 컨테이너를 하나 띄우고, /mount01 경로에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt; 볼륨을 마운트하는 설정이다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim volume-test01.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-volume-01&lt;/span&gt;             &lt;span class=&quot;c1&quot;&gt;# 파드명&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;                               &lt;span class=&quot;c1&quot;&gt;# 파드의 내부 상태 정의, 파드 내부에는 컨테이너와 볼륨 생성&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-test01&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;                 &lt;span class=&quot;c1&quot;&gt;# 컨테이너가 사용할 볼륨 마운트&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;empty-test01&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# 컨테이너가 사용할 볼륨 이름(하단 volumes에 정의된 이름과 일치해야 함)&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/mount01&lt;/span&gt;       &lt;span class=&quot;c1&quot;&gt;# 컨테이너 내부의 마운트 경로&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;                          &lt;span class=&quot;c1&quot;&gt;# 파드 내부에 생성할 볼륨 생성&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;empty-test01&lt;/span&gt;            &lt;span class=&quot;c1&quot;&gt;# 볼륨 식별자&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;emptyDir&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;{}&lt;/span&gt;                  &lt;span class=&quot;c1&quot;&gt;# 볼륨 타입 설정, {}는 기본 옵션 사용 (메모리가 아닌 디스크 사용)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;파드 생성 및 데이터 쓰기&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;파드를 생성하고 정상적으로 실행 중인지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test01.yml
pod/nginx-volume-01 created

assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                  READY   STATUS    RESTARTS   AGE
pod/nginx-volume-01   1/1     Running   0          4m17s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   8d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이제 파드 내부로 진입하여 마운트 된 경로에 파일을 하나 생성해본다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 파드 내부 쉘 접속&lt;/span&gt;
assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; nginx-volume-01 &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /bin/bash

&lt;span class=&quot;c&quot;&gt;# 마운트 경로로 이동하여 파일 생성&lt;/span&gt;
root@nginx-volume-01:/# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;bin   dev		   docker-entrypoint.sh  home  media  mount01  proc  run   srv	tmp  var
boot  docker-entrypoint.d  etc			 lib   mnt    opt      root  sbin  sys	usr

root@nginx-volume-01:/# &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;mount01
root@nginx-volume-01:/mount01# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;root@nginx-volume-01:/mount01# &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ./test.txt

&lt;span class=&quot;c&quot;&gt;# 파일 확인&lt;/span&gt;
root@nginx-volume-01:/mount01# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;test.txt

root@nginx-volume-01:/mount01# &lt;span class=&quot;nb&quot;&gt;cat &lt;/span&gt;test.txt
&lt;span class=&quot;nb&quot;&gt;test

&lt;/span&gt;root@nginx-volume-01:/mount01# &lt;span class=&quot;nb&quot;&gt;exit
exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1220/yml.png&quot; alt=&quot;yml 파일 비교&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;파드 삭제 후 재생성(데이터 유실 확인)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이제 핵심 테스트이다. 파드를 삭제했다가 재생성했을 때 위에서 만든 test.txt 파일이 삭제되었는지 확인해보자.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 기존 파드 삭제(파드가 삭제되면 emptyDir도 삭제됨)&lt;/span&gt;
assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test01.yml
pod &lt;span class=&quot;s2&quot;&gt;&quot;nginx-volume-01&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   8d

&lt;span class=&quot;c&quot;&gt;# 동일한 설정으로 파드 재생성&lt;/span&gt;
assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test01.yml
pod/nginx-volume-01 created
assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                  READY   STATUS    RESTARTS   AGE
pod/nginx-volume-01   1/1     Running   0          4s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   8d

&lt;span class=&quot;c&quot;&gt;# 파드 내부 확인&lt;/span&gt;
assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; nginx-volume-01 &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /bin/bash
root@nginx-volume-01:/# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;bin   dev		   docker-entrypoint.sh  home  media  mount01  proc  run   srv	tmp  var
boot  docker-entrypoint.d  etc			 lib   mnt    opt      root  sbin  sys	usr

root@nginx-volume-01:/# &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;mount01

&lt;span class=&quot;c&quot;&gt;# 생성했던 test.txt 파일이 존재하지 않음을 확인&lt;/span&gt;
root@nginx-volume-01:/mount01# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;root@nginx-volume-01:/mount01# &lt;span class=&quot;nb&quot;&gt;exit
exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;test.txt 파일이 보이지 않는다.&lt;br /&gt;
이를 통해 &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt;은 파드의 생명 주기와 동일하며, 파드가 삭제되면 데이터도 초기화된다&lt;/strong&gt;는 것을 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;리소스를 정리한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test01.yml
pod &lt;span class=&quot;s2&quot;&gt;&quot;nginx-volume-01&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   8d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-hostpath&quot;&gt;3. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;쿠버네티스 공식 문서에는 &lt;strong&gt;보안상의 이유로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt; 사용을 최대한 지양할 것을 권고&lt;/strong&gt;한다.&lt;/p&gt;

  &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt; 볼륨에는 많은 보안 위험이 있으며, 가능하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;를 사용하지 않는 것이 좋다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt; 볼륨을 사용해야 하는 경우, 필요한 파일 또는 디렉터리로만 범위를 지정하고 ReadOnly로 마운트해야 한다.&lt;br /&gt;
AdmissionPolicy를 사용하여 특정 디렉터리로의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt; 액세스를 제한하는 경우,&lt;br /&gt;
readOnly 마운트를 사용하는 정책이 유효하려면 volumeMounts 가 반드시 지정되어야 한다.&lt;/p&gt;

  &lt;p&gt;참고 링크: &lt;a href=&quot;https://kubernetes.io/ko/docs/concepts/storage/volumes/#hostpath&quot;&gt;https://kubernetes.io/ko/docs/concepts/storage/volumes/#hostPath&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt;이 파드의 생명주기에 묶여 있는 임시 저장소라면, &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;&lt;/strong&gt;는 파드가 실행 중인 &lt;strong&gt;호스트 노드의 실제 파일 시스템&lt;/strong&gt;을 파드에 마운트하여 사용하는 방식이다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1220/hostpath.png&quot; alt=&quot;hostPath&quot; /&gt;&lt;/p&gt;

&lt;p&gt;이 방식은 도커의 &lt;a href=&quot;https://assu10.github.io/dev/2025/11/16/docker-basics-and-commands-guide-2/#24-bind-mount-%ED%98%B8%EC%8A%A4%ED%8A%B8-%EA%B2%BD%EB%A1%9C-%EC%A7%81%EC%A0%91-%EA%B3%B5%EC%9C%A0&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bind mount&lt;/code&gt;&lt;/a&gt;와 유사한 개념으로, 
파드가 삭제되어도 노드의 디스크에 파일이 남아있기 때문에 데이터가 유지된다는 장점이 있다.&lt;br /&gt;
하지만 치명적인 단점과 보안 이슈가 존재하여 사용 시 각별한 주의가 필요하다.&lt;/p&gt;

&lt;p&gt;&amp;lt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;의 특징과 한계&lt;/strong&gt;&amp;gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;노드 종속성&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;데이터가 특정 노드의 디스크에 저장되므로, 파드가 다른 노드로 스케줄링되면 기존 데이터에 접근할 수 없다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;활용 사례&lt;/strong&gt;
    &lt;ul&gt;
      &lt;li&gt;주로 시스템 모니터링 에이전트나 로그 수집기처럼 노드의 정보(예: /var/log, /proc)를 읽어야 하는 시스템 데몬 파드에서 제한적으로 사용된다.&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;31-노드-간-데이터-공유-확인&quot;&gt;3.1. 노드 간 데이터 공유 확인&lt;/h2&gt;

&lt;p&gt;여기서는 3개의 노드(myserver01, 02, 03) 환경에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;가 어떻게 동작하는지 확인해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;클러스터 노드 확인 및 특정 노드에 디렉터리 생성&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;쿠버네티스 클러스터 노드 이름을 확인해보자.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;➜  ~ ssh &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 2201 assu@127.0.0.1

assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get nodes &lt;span class=&quot;nt&quot;&gt;--show-labels&lt;/span&gt;
NAME         STATUS   ROLES           AGE   VERSION    LABELS
myserver01   Ready    control-plane   10d   v1.29.15   beta.kubernetes.io/arch&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;arm64,beta.kubernetes.io/os&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux,kubernetes.io/arch&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;arm64,kubernetes.io/hostname&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;myserver01,kubernetes.io/os&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux,node-role.kubernetes.io/control-plane&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;,node.kubernetes.io/exclude-from-external-load-balancers&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;
myserver02   Ready    &amp;lt;none&amp;gt;          10d   v1.29.15   beta.kubernetes.io/arch&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;arm64,beta.kubernetes.io/os&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux,kubernetes.io/arch&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;arm64,kubernetes.io/hostname&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;myserver02,kubernetes.io/os&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux
myserver03   Ready    &amp;lt;none&amp;gt;          10d   v1.29.15   beta.kubernetes.io/arch&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;arm64,beta.kubernetes.io/os&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux,kubernetes.io/arch&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;arm64,kubernetes.io/hostname&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;myserver03,kubernetes.io/os&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;linux
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 결과의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubernetes.io/hostname=myserver01&lt;/code&gt; 에서 각각 노드 이름이 myserver01, myserver02, myserver03 인 것을 확인할 수 있다.&lt;/p&gt;

&lt;p&gt;그럼 이제 데이터의 영속성을 확인하기 위해 myserver03 노드에 직접 접속하여 데이터를 저장할 폴더를 미리 생성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;~ ssh &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 2203 assu@127.0.0.1

assu@myserver03:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;work

assu@myserver03:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;work
assu@myserver03:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ch04  ch05  ch06

assu@myserver03:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;volhost01
assu@myserver03:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ch04  ch05  ch06  volhost01

&lt;span class=&quot;c&quot;&gt;# hostPath 볼륨을 생성할 volhost01 디렉터리 생성&lt;/span&gt;
assu@myserver03:~/work&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;volhost01/
assu@myserver03:~/work/volhost01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;pwd&lt;/span&gt;
/home/assu/work/volhost01

assu@myserver03:~/work/volhost01&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exit
logout
&lt;/span&gt;Connection to 127.0.0.1 closed.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;파드 생성(myserver03 지정)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이제 마스터 노드(myserver01)로 돌아와서 파드를 생성한다.&lt;br /&gt;
이 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;의 테스트를 위해 파드가 반드시 데이터를 생성해 둔 myserver03에 뜨도록 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nodeSelector&lt;/code&gt;를 설정한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;custom-resources.yaml  work

assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;work/ch09
assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01  ex02  ex03  ex04  ex05  ex06  ex07  ex08  ex09

assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex10
assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01  ex02  ex03  ex04  ex05  ex06  ex07  ex08  ex09  ex10

assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex10
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim volume-test02.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-volume-02&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 파드 내부 상태 정의, 파드 내부에는 컨테이너와 볼륨 생성&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;nodeSelector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 파드가 실행될 노드 정의&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;kubernetes.io/hostname&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;myserver03&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 파드를 강제로 myserver03에 배포&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 컨테이너 생성&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-test01&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 컨테이너가 사용할 볼륨 마운트&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;hostpath-test01&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 컨테이너가 사용할 볼륨 이름 정의&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/mount01&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 볼륨을 마운트할 경로 작성&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 파드 내부에 생성할 볼륨 정의&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;hostpath-test01&lt;/span&gt; 
      &lt;span class=&quot;na&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 볼륨 타입 정의&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/home/assu/work/volhost01&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 호스트(myserver03)의 실제 경로&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DirectoryOrCreate&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 경로가 없으면 생성 (권한 주의)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;파드를 실행하고 상태를 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test02.yml
pod/nginx-volume-02 created

assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                  READY   STATUS    RESTARTS   AGE
pod/nginx-volume-02   1/1     Running   0          6m49s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   10d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;만일 파드가 READY 상태로 변경되지 않거나 CNI 관련 에러가 발생한다면 &lt;a href=&quot;https://assu10.github.io/dev/2025/12/20/trouble-shooting/&quot;&gt;TroubleShooting - plugin type=calico failed (add): error getting ClusterInformation: connection is unauthorized: Unauthorized
2025-12-20 in DEV on Trouble Shooting&lt;/a&gt; 를 참고하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;데이터 영속성 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;파드 내부에 접속하여 파일을 생성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; nginx-volume-02 &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /bin/bash

root@nginx-volume-02:/# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;bin   dev		   docker-entrypoint.sh  home  media  mount01  proc  run   srv	tmp  var
boot  docker-entrypoint.d  etc			 lib   mnt    opt      root  sbin  sys	usr

&lt;span class=&quot;c&quot;&gt;# 볼륨이 마운트되는 디렉터리로 이동&lt;/span&gt;
root@nginx-volume-02:/# &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;mount01
root@nginx-volume-02:/mount01# &lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 볼륨을 통해 저장할 파일 생성&lt;/span&gt;
root@nginx-volume-02:/mount01# &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hello world&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ./test01.txt
root@nginx-volume-02:/mount01# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;test01.txt

root@nginx-volume-02:/mount01# &lt;span class=&quot;nb&quot;&gt;exit
exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이제 파드를 &lt;strong&gt;삭제하고 재생성&lt;/strong&gt;해본다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;emptyDir&lt;/code&gt;과 달리 데이터가 남아있어야 한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 기존 파드 삭제&lt;/span&gt;
assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test02.yml
pod &lt;span class=&quot;s2&quot;&gt;&quot;nginx-volume-02&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   10d

&lt;span class=&quot;c&quot;&gt;# 파드 재생성&lt;/span&gt;
assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test02.yml
pod/nginx-volume-02 created
assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                  READY   STATUS    RESTARTS   AGE
pod/nginx-volume-02   1/1     Running   0          4s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   10d

assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME                  READY   STATUS    RESTARTS   AGE     IP                NODE         NOMINATED NODE   READINESS GATES
pod/nginx-volume-02   1/1     Running   0          3m19s   192.168.149.208   myserver03   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE   SELECTOR
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   10d   &amp;lt;none&amp;gt;

&lt;span class=&quot;c&quot;&gt;# 재생성된 파드에서 파일 확인&lt;/span&gt;
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; nginx-volume-02 &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; /mount01/test01.txt
hello world
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;파드가 재시작되어도 myserver03 노드의 디스크에 파일이 저장되어 있으므로 데이터가 유지된다.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;의 한계 확인(다른 노드에 배포 시)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;그렇다면 myserver02 노드에 배포된 파드는 이 데이터를 볼 수 있을까?&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;~ ssh &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 2202 assu@127.0.0.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver02:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim volume-test03.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-volume-03&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;nodeSelector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;kubernetes.io/hostname&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;myserver02&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 이번엔 myserver02에 배포&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-test01&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;hostpath-test01&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/mount01&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; 
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;hostpath-test01&lt;/span&gt; 
      &lt;span class=&quot;na&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/home/assu/work/volhost01&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DirectoryOrCreate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver02:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test03.yml
pod/nginx-volume-03 created

&lt;span class=&quot;c&quot;&gt;# myserver02 노드에 nginx-volume-03 파드가 추가됨&lt;/span&gt;
assu@myserver02:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME                  READY   STATUS    RESTARTS   AGE     IP                NODE         NOMINATED NODE   READINESS GATES
pod/nginx-volume-02   1/1     Running   0          5m58s   192.168.149.208   myserver03   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
pod/nginx-volume-03   1/1     Running   0          4s      192.168.131.81    myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE   SELECTOR
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   10d   &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver02:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; nginx-volume-03 &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /bin/bash
root@nginx-volume-03:/# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;bin   dev		   docker-entrypoint.sh  home  media  mount01  proc  run   srv	tmp  var
boot  docker-entrypoint.d  etc			 lib   mnt    opt      root  sbin  sys	usr

&lt;span class=&quot;c&quot;&gt;# test01.txt 파일이 존재하지 않음&lt;/span&gt;
root@nginx-volume-03:/# &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;mount01
root@nginx-volume-03:/mount01# &lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;myserver02 노드에는 해당 파일이 없다.&lt;br /&gt;
즉, &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;는 파드가 실행되는 노드에 종속되므로, 멀티 노드 환경에서의 범용적인 스토리지 솔루션으로는 적합하지 않다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이제 리소스를 정리한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test02.yml
pod &lt;span class=&quot;s2&quot;&gt;&quot;nginx-volume-02&quot;&lt;/span&gt; deleted

assu@myserver02:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test03.yml
pod &lt;span class=&quot;s2&quot;&gt;&quot;nginx-volume-03&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex10&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   11d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-pvpersistentvolume&quot;&gt;4. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;(PersistentVolume)&lt;/h1&gt;

&lt;p&gt;위에서 살펴본 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt;는 데이터가 노드에 종속된다는 치명적인 단점이 있었다.&lt;br /&gt;
엔터프라이즈 환경에서는 파드가 어느 노드에 뜨더라도 동일한 데이터에 접근할 수 있어야 하며, 파드나 노드가 죽더라도 데이터는 안전하게 별도의 저장소에 보관되어야 한다.&lt;/p&gt;

&lt;p&gt;이를 위해 쿠버네티스는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;(PersistentVolume)와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;(PersistentVolumeClaim) 라는 개념을 도입하였다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1220/pv.png&quot; alt=&quot;PV&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;관리자가 프로비저닝할 실제 스토리지 리소스&lt;/li&gt;
      &lt;li&gt;예: NFS, AWS EBS, GCE PersistentDisk 등&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;사용자가 스토리지를 사용하기 위해 요청하는 명세서&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;핵심은 분리(Decoupling)&lt;/strong&gt;이다.&lt;br /&gt;
개발자는 구체적인 스토리지 내부 구현(NFS인지, 클라우드 스토리지인지)을 알 필요 없이, 필요한 용량과 접근 모드가 적힌 &lt;strong&gt;PVC&lt;/strong&gt;만 생성하면 된다.&lt;br /&gt;
그러면 쿠버네티스가 조건에 맞는 &lt;strong&gt;PV&lt;/strong&gt;를 찾아 자동으로 연결해준다.&lt;/p&gt;

&lt;p&gt;여기서는 NFS(Network File System)을 이용하여 외부 스토리지를 구성하고, 이를 PV로 연결하여 데이터 영속성을 확인해본다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;41-nfs-서버-구축&quot;&gt;4.1. NFS 서버 구축&lt;/h2&gt;

&lt;p&gt;먼저 쿠버네티스 클러스터 외부의 스토리지 역할을 할 NFS 서버를 myserver03 노드에 구축한다. 실제 운영 환경에서는 별도의 스토리지 서버를 사용한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;필수 패키지 설치&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;NFS 통신을 위해 모든 노드(클라이언트)에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nfs-common&lt;/code&gt;을, 서버(myserver03)에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nfs-kernel-server&lt;/code&gt;를 설치한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 2201 assu@127.0.0.1
assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nfs-common

assu@myserver01:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh myserver02
assu@myserver02:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nfs-common

assu@myserver02:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh myserver03
assu@myserver03:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nfs-common
assu@myserver03:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;nfs-kernel-server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver03:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;systemctl status nfs-server.service
● nfs-server.service - NFS server and services
     Loaded: loaded &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;/usr/lib/systemd/system/nfs-server.service&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; enabled&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; preset: enabled&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
     Active: active &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;exited&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; since Thu 2025-12-25 04:29:17 UTC&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; 46s ago
   Main PID: 228689 &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;code&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;exited, &lt;span class=&quot;nv&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;0/SUCCESS&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        CPU: 8ms

Dec 25 04:29:17 myserver03 systemd[1]: Starting nfs-server.service - NFS server and services...
Dec 25 04:29:17 myserver03 exportfs[228688]: exportfs: can&lt;span class=&quot;s1&quot;&gt;&apos;t open /etc/exports for reading
Dec 25 04:29:17 myserver03 systemd[1]: Finished nfs-server.service - NFS server and services.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;공유 디렉터리 생성 및 설정&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;myserver03에서 데이터를 저장할 실제 디렉터리(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;)를 만들고, 권한을 설정한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 루트 권한 획득&lt;/span&gt;
assu@myserver03:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt;

root@myserver03:~# &lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /tmp
root@myserver03:/tmp# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;snap-private-tmp
systemd-private-f315992405a246bca2c4317c045351c1-fwupd.service-su3wED
systemd-private-f315992405a246bca2c4317c045351c1-ModemManager.service-aI4KYx
systemd-private-f315992405a246bca2c4317c045351c1-polkit.service-qobsnC
systemd-private-f315992405a246bca2c4317c045351c1-systemd-logind.service-qoCRgT
systemd-private-f315992405a246bca2c4317c045351c1-systemd-resolved.service-OxMJXc
systemd-private-f315992405a246bca2c4317c045351c1-systemd-timesyncd.service-7ANjuO

&lt;span class=&quot;c&quot;&gt;# PV용 디렉터리로 k8s-pv 디렉터리 생성&lt;/span&gt;
root@myserver03:/tmp# &lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;k8s-pv
root@myserver03:/tmp# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;k8s-pv
snap-private-tmp
systemd-private-f315992405a246bca2c4317c045351c1-fwupd.service-su3wED
systemd-private-f315992405a246bca2c4317c045351c1-ModemManager.service-aI4KYx
systemd-private-f315992405a246bca2c4317c045351c1-polkit.service-qobsnC
systemd-private-f315992405a246bca2c4317c045351c1-systemd-logind.service-qoCRgT
systemd-private-f315992405a246bca2c4317c045351c1-systemd-resolved.service-OxMJXc
systemd-private-f315992405a246bca2c4317c045351c1-systemd-timesyncd.service-7ANjuO
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이제 /etc/exports 파일을 수정하여 공유 정책을 설정한다.&lt;br /&gt;
여기서는 myserver02(10.0.2.5)가 접근할 수 있도록 설정한다.&lt;br /&gt;
설정은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[공유할 디렉터리][접속을 허용할 IP(옵션)]&lt;/code&gt; 형태로 작성한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;root@myserver03:/tmp# &lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;vim /etc/exports

...

&lt;span class=&quot;c&quot;&gt;# 파일 끝에 추가&lt;/span&gt;
/tmp/k8s-pv 10.0.2.5&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;rw,no_root_squash&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rw&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;읽기 및 쓰기 권한 부여&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;no_root_squash&lt;/code&gt;
    &lt;ul&gt;
      &lt;li&gt;클라이언트의 root 권한을 서버에서도 root로 인정(이 옵션이 없으면 권한 문제로 파일 쓰기가 실패할 수 있음)&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;서비스 재시작&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;설정을 적용하기 위해 NFS 서비스를 재시작한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver03:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl restart nfs-server
assu@myserver03:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;systemctl status nfs-server.service
● nfs-server.service - NFS server and services
     Loaded: loaded &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;/usr/lib/systemd/system/nfs-server.service&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; enabled&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; preset: enabled&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
     Active: active &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;exited&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; since Thu 2025-12-25 04:36:47 UTC&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; 10s ago
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;42-pv-및-pvc-생성&quot;&gt;4.2. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt; 및 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt; 생성&lt;/h2&gt;

&lt;p&gt;이제 인프라 준비가 끝났으니 쿠버네티스 리소스를 생성한다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;(PersistentVolume) 정의&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01  ex02  ex03  ex04  ex05  ex06  ex07  ex08  ex09  ex10

assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;ex11
assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;ex01  ex02  ex03  ex04  ex05  ex06  ex07  ex08  ex09  ex10  ex11
assu@myserver01:~/work/ch09&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;ex11
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;관리자 입장에서 ‘100MB 용량의 NFS 스토리지’를 정의한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim volume-test-04-1-pv.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;PersistentVolume&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pv-01&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;accessModes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ReadWriteOnce&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 하나의 노드에서만 R/W 가능&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;capacity&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;100Mi&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 용량 100MB&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;persistentVolumeReclaimPolicy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Retain&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 중요: 반환 정책&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;storageClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pv-test-01&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# PVC와 연결을 위한 식별자, 여기서 설정하는 storageClassName은 이후 작성할 PVC와의 연결점이 됨&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;nfs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;server&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;10.0.2.6&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# NFS 서버(myserver03) IP&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/tmp/k8s-pv&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# NFS 서버 내부에서 PV로 사용할 경로&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;persistentVolumeReclaimPolicy&lt;/code&gt;(반환 정책)&lt;/strong&gt;&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;가 삭제되었을 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;의 데이터를 어떻게 처리할지 결정한다.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Retain&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;가 삭제되어도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt; 내부의 데이터는 그대로 유지&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Delete&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;가 삭제될 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt; 역시 삭제하고 연계되어 있는 외부 스토리지 데이터도 삭제(AWS EBS 등에서 주로 사용)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Recycle&lt;/code&gt;(Deprecated): &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rm -rf&lt;/code&gt; 명령으로 데이터를 지우고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;를 재사용 가능하게 만듦&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;(PersistentVolumeClaim) 정의&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;사용자 입장에서 ‘30MB 정도의 스토리지가 필요해’라고 요청한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;vim volume-test-04-2-pvc.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pvc-01&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 실행할 PVC 이름&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# PVC 내부 상태 정의&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;accessModes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 사용할 자원 설정&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# PV에게 보낼 요청사항 작성&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;storage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;30Mi&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 최소 30MB 요청&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;storageClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pv-test-01&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# PV의 storageClassName과 일치해야 바인딩 됨&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;생성 및 바인딩 확인&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PV와 PVC를 순서대로 생성하고 상태 변화를 관찰한다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;를 생성한다.&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test-04-1-pv.yml
persistentvolume/pv-01 created

assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pv
NAME    CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pv-01   100Mi      RWO            Retain           Available           pv-test-01     &amp;lt;&lt;span class=&quot;nb&quot;&gt;unset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;                          19s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;의 STATUS가 Available 인 것을 확인할 수 있다. 이후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;를 생성하면 변경된다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test-04-2-pvc.yml
persistentvolumeclaim/pvc-01 created

assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pvc
NAME     STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
pvc-01   Bound    pv-01    100Mi      RWO            pv-test-01     &amp;lt;&lt;span class=&quot;nb&quot;&gt;unset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;                 11s

assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pv
NAME    CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM            STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pv-01   100Mi      RWO            Retain           Bound    default/pvc-01   pv-test-01     &amp;lt;&lt;span class=&quot;nb&quot;&gt;unset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;                          79s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;의 STATUS 가 &lt;strong&gt;Available&lt;/strong&gt; 에서 &lt;strong&gt;Bound&lt;/strong&gt; 로 변경되었다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;가 정상적으로 연결된 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;43-파드에서-pvc-사용&quot;&gt;4.3. 파드에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt; 사용&lt;/h2&gt;

&lt;p&gt;이제 파드를 생성하여 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;를 마운트한다.&lt;br /&gt;
파드는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;를 직접 지정하는 것이 아니라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;의 이름을 참조한다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-volume-04&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;nodeSelector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;kubernetes.io/hostname&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;myserver02&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# myserver02에 배포&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx-test01&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nginx:latest&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nfs-pv-01&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# 컨테이너가 사용하게 될 볼륨 이름, 이후 생성할 볼륨 이름과 일치해야 함&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/mount01&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 볼륨을 마운트할 경로&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# 파드 내부에 생성할 볼륨&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nfs-pv-01&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;persistentVolumeClaim&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;claimName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pvc-01&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# 위에서 생성한 PVC 이름 참조&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1220/yml2.png&quot; alt=&quot;pod, pvc, pv 관계&quot; /&gt;&lt;/p&gt;

&lt;p&gt;파드가 정상적으로 실행되었는지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test-04-3-pod.yml
pod/nginx-volume-04 created

assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pod &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
NAME              READY   STATUS    RESTARTS   AGE   IP               NODE         NOMINATED NODE   READINESS GATES
nginx-volume-04   1/1     Running   0          8s    192.168.131.82   myserver02   &amp;lt;none&amp;gt;           &amp;lt;none&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;44-데이터-영속성-검증&quot;&gt;4.4. 데이터 영속성 검증&lt;/h2&gt;

&lt;p&gt;이제 &lt;strong&gt;파드(myserver02) → PV/PVC → NFS서버(myserver03)&lt;/strong&gt; 으로 이어지는 연결을 통해 데이터가 실제로 유지되는지 확인해보자.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;파드에서 파일 생성&lt;/strong&gt;: myserver02에 있는 파드 내부로 들어가 파일 생성&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;NFS 서버에서 확인&lt;/strong&gt;: myserver03의 실제 디렉터리에 파일이 생성되었는지 확인&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;파드 내부에서 파일 생성&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-it&lt;/span&gt; nginx-volume-04 &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /bin/bash

root@nginx-volume-04:/# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;bin   dev		   docker-entrypoint.sh  home  media  mount01  proc  run   srv	tmp  var
boot  docker-entrypoint.d  etc			 lib   mnt    opt      root  sbin  sys	usr

root@nginx-volume-04:/# &lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;mount01
root@nginx-volume-04:/mount01# &lt;span class=&quot;nb&quot;&gt;ls

&lt;/span&gt;root@nginx-volume-04:/mount01# &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;hello world!&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ./nfs_test.txt
root@nginx-volume-04:/mount01# &lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;nfs_test.txt

root@nginx-volume-04:/mount01# &lt;span class=&quot;nb&quot;&gt;exit
exit
&lt;/span&gt;assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;err&quot;&gt;$&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;NFS 서버에서 확인&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh myserver03

assu@myserver03:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /tmp/k8s-pv
assu@myserver03:/tmp/k8s-pv&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;nfs_test.txt

assu@myserver03:/tmp/k8s-pv&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cat &lt;/span&gt;nfs_test.txt
hello world!

assu@myserver03:/tmp/k8s-pv&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exit
logout
&lt;/span&gt;Connection to myserver03 closed.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;파드는 자신이 어느 서버에 있는지, 실제 스토리지가 어디인지 모르지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;를 통해 안전하게 데이터를 외부 서버에 저장하였다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/img/dev/2025/1220/pv_all.png&quot; alt=&quot;전체 흐름&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;45-리소스-삭제-및-데이터-보존-확인&quot;&gt;4.5. 리소스 삭제 및 데이터 보존 확인&lt;/h2&gt;

&lt;p&gt;마지막으로 쿠버네티스 리소스(파드, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PVC&lt;/code&gt;)를 모두 삭제했을 때 데이터가 어떻게 되는지 확인해보자.&lt;br /&gt;
우리는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PV&lt;/code&gt; 정책을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Retain&lt;/code&gt; 으로 설정했었다.&lt;/p&gt;

&lt;p&gt;리소스 삭제&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test-04-1-pv.yml
persistentvolume &lt;span class=&quot;s2&quot;&gt;&quot;pv-01&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test-04-3-pod.yml
pod &lt;span class=&quot;s2&quot;&gt;&quot;nginx-volume-04&quot;&lt;/span&gt; deleted
assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; volume-test-04-2-pvc.yml
persistentvolumeclaim &lt;span class=&quot;s2&quot;&gt;&quot;pvc-01&quot;&lt;/span&gt; deleted

assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get all
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;S&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;   AGE
service/kubernetes   ClusterIP   10.96.0.1    &amp;lt;none&amp;gt;        443/TCP   11d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;NFS 서버 데이터 확인&lt;/p&gt;
&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;assu@myserver01:~/work/ch09/ex11&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ssh myserver03
assu@myserver03:~&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /tmp/k8s-pv
assu@myserver03:/tmp/k8s-pv&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls
&lt;/span&gt;nfs_test.txt

assu@myserver03:/tmp/k8s-pv&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;exit
logout
&lt;/span&gt;Connection to myserver03 closed.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;모든 쿠버네티스 오브젝트가 사라졌음에도 불구하고, NFS 서버의 /tmp/k8s-pv 디렉터리에는 파일이 안전하게 남아있다.&lt;br /&gt;
이것이 바로 상태를 저장하는 &lt;strong&gt;Stateful&lt;/strong&gt; 애플리케이션을 위한 핵심이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;참고-사이트--함께-보면-좋은-사이트&quot;&gt;참고 사이트 &amp;amp; 함께 보면 좋은 사이트&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;본 포스트는 장철원 저자의 &lt;strong&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/strong&gt;를 기반으로 스터디하며 정리한 내용들입니다.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.yes24.com/product/goods/126115324&quot;&gt;한 권으로 배우는 도커&amp;amp;쿠버네티스&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/losskatsu/DockerKubernetes&quot;&gt;예제 코드&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Sat, 20 Dec 2025 10:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/20/kubernetes-volume-basic-emptydir-hostpath-pv/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/20/kubernetes-volume-basic-emptydir-hostpath-pv/</guid>
        
        <category>devops</category>
        
        <category>kubernetes</category>
        
        <category>k8s</category>
        
        <category>container-storage</category>
        
        <category>volume</category>
        
        <category>emptydir</category>
        
        <category>hostpath</category>
        
        <category>persistent-volume</category>
        
        <category>pv</category>
        
        <category>pvc</category>
        
        <category>nfs</category>
        
        <category>stateful-application</category>
        
        <category>infrastructure</category>
        
        
        <category>dev</category>
        
      </item>
    
      <item>
        <title>TroubleShooting - plugin type=calico failed (add): error getting ClusterInformation: connection is unauthorized: Unauthorized</title>
        <description>&lt;h1 id=&quot;내용&quot;&gt;내용&lt;/h1&gt;
&lt;p&gt;쿠버네티스 매니페스트를 통해 파드를 생성하는 중 파드의 상태가 READY 로 변경되지 않았다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;파드 실행 시 status 가 READY 로 변경되지 않으면&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl describe pod 파드명&lt;/code&gt; 을 실행하여 이벤트를 확인한다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;plugin type=&quot;calico&quot; failed (add): error getting ClusterInformation: connection is unauthorized: Unauthorized&lt;/code&gt;&lt;br /&gt;
이런 오류가 있다면 쿠버네티스 네트워크 플러그인(Calico) 인증 오류로 파드가 생성되지 못하는 것이다.&lt;/p&gt;

  &lt;p&gt;이 때는 Calico 관련 파드를 강제로 삭제하여 재시작하면 파드가 다시 생성되면서 새로운 인증 정보를 받아오게 된다.&lt;/p&gt;

  &lt;p&gt;먼저 Calico 파드가 있는 네임스페이스를 확인한다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl get pods -A | grep calico&lt;/code&gt;&lt;/p&gt;

  &lt;p&gt;이후 Calico Node 파드를 재시작한다.&lt;br /&gt;
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl delete pod -n calico-system -l k8s-app=calico-node&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;원인&quot;&gt;원인&lt;/h1&gt;

&lt;p&gt;쿠버네티스 네트워크 플러그인(Calico) 인증 오류로 파드가 생성되지 못하는 것이었다.&lt;br /&gt;
이런 현상은 주로 Calico Node Pod 의 인증 토큰이 만료되었거나 일시적인 통신 오류로 인해 발생한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;해결책&quot;&gt;해결책&lt;/h1&gt;

&lt;p&gt;Calico 파드를 강제로 삭제하여 재시작하면 된다. 그러면 파드가 다시 생성되면서 새로운 인증 정보를 받아오게 된다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Calico 파드가 있는 네임스페이스를 확인한다.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pods &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;calico

calico-apiserver   calico-apiserver-6c4c866f9f-hdvxh          1/1     Running   0            4d23h
calico-apiserver   calico-apiserver-6c4c866f9f-zl96c          1/1     Running   0            4d23h
calico-system      calico-kube-controllers-7f86844856-v785r   1/1     Running   0            3d20h
calico-system      calico-node-kq2rw                          0/1     Running   0            7s
calico-system      calico-node-t76dx                          0/1     Running   0            7s
calico-system      calico-node-wc4zx                          0/1     Running   0            7s
calico-system      calico-typha-674886f54f-dfmzp              1/1     Running   0            3d20h
calico-system      calico-typha-674886f54f-ff8ht              1/1     Running   0            3d20h
calico-system      csi-node-driver-cd58m                      2/2     Running   0            3d20h
calico-system      csi-node-driver-p6rs4                      2/2     Running   0            3d20h
calico-system      csi-node-driver-pxhnw                      2/2     Running   0            3d20h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이후 Calico 노드 파드를 재시작한다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl delete pod &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; calico-system &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; k8s-app&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;calico-node
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;잠시 후 파드 상태를 확인해보면 READY 상태로 변경된 것을 확인할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-shell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl get pods &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; wide
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
</description>
        <pubDate>Sat, 20 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://assu10.github.io/dev/2025/12/20/trouble-shooting/</link>
        <guid isPermaLink="true">https://assu10.github.io/dev/2025/12/20/trouble-shooting/</guid>
        
        <category>trouble_shooting</category>
        
        
        <category>dev</category>
        
      </item>
    
  </channel>
</rss>
