<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Jin.log</title>
        <link>https://velog.io/</link>
        <description>인천대학교 멋쟁이사자 13기 백엔드 &amp; 14기 백엔트 파트장 및 부회장</description>
        <lastBuildDate>Fri, 21 Aug 2026 02:34:23 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Jin.log</title>
            <url>https://velog.velcdn.com/images/jung_ji_in02/profile/f91d6474-74d9-4602-9e42-ac6aeae6d7b7/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Jin.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jung_ji_in02" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[CS 면접 바이블: Chapter 1.2]]></title>
            <link>https://velog.io/@jung_ji_in02/CS-%EB%A9%B4%EC%A0%91-%EB%B0%94%EC%9D%B4%EB%B8%94-Chapter-2-i5ppzz0t</link>
            <guid>https://velog.io/@jung_ji_in02/CS-%EB%A9%B4%EC%A0%91-%EB%B0%94%EC%9D%B4%EB%B8%94-Chapter-2-i5ppzz0t</guid>
            <pubDate>Fri, 21 Aug 2026 02:34:23 GMT</pubDate>
            <description><![CDATA[<h2 id="프로그래밍-패러다임">프로그래밍 패러다임</h2>
<blockquote>
<h3 id="프로그래밍-패러다임이란">프로그래밍 패러다임이란?</h3>
<p>프로그래머가 프로그램을 바라보고 설계하는 <strong>관점과 개발 방식</strong></p>
</blockquote>
<p>같은 문제를 해결하더라도 어떤 패러다임을 사용하느냐에 따라 <strong>코드를 구성하고 문제를 해결하는 방식이 달라짐</strong></p>
<p>예시:</p>
<ul>
<li>객체지향 프로그래밍 → 프로그램을 <strong>상호작용하는 객체들의 집합</strong>으로 표현</li>
<li>함수형 프로그래밍 → 프로그램을 <strong>상태 변경을 최소화한 함수들의 조합</strong>으로 표현</li>
</ul>
<hr>
<h2 id="프로그래밍-패러다임의-분류">프로그래밍 패러다임의 분류</h2>
<p><img src="https://velog.velcdn.com/images/jung_ji_in02/post/554663b3-b224-4fd7-9ed6-33d986482c3a/image.png" alt=""></p>
<h3 id="선언형-프로그래밍">선언형 프로그래밍</h3>
<ul>
<li><strong>무엇을(What) 할 것인지</strong> 중심으로 작성</li>
<li>원하는 결과를 선언하고 구체적인 처리 과정은 상대적으로 감추는 방식</li>
</ul>
<h4 id="함수형-프로그래밍">함수형 프로그래밍</h4>
<ul>
<li>선언형 프로그래밍의 한 종류</li>
<li>상태 변경과 부작용을 최소화하고 <strong>함수의 조합</strong>을 통해 프로그램 구성</li>
<li>대표 언어: Haskell</li>
<li>Java는 JDK 8부터 람다식, 메서드 참조, 생성자 참조, Stream API 등을 지원하면서 함수형 프로그래밍 방식 활용 가능</li>
</ul>
<hr>
<h3 id="명령형-프로그래밍">명령형 프로그래밍</h3>
<ul>
<li><strong>어떻게(How) 처리할 것인지</strong> 중심으로 작성</li>
<li>명령문의 순서와 상태 변화를 통해 프로그램의 동작을 표현</li>
</ul>
<h4 id="절차지향-프로그래밍">절차지향 프로그래밍</h4>
<ul>
<li>프로그램의 처리 과정을 <strong>절차와 함수의 실행 순서</strong>를 중심으로 구성</li>
<li>대표 언어: C</li>
</ul>
<h4 id="객체지향-프로그래밍">객체지향 프로그래밍</h4>
<ul>
<li>프로그램을 <strong>상태와 행동을 가진 객체들의 상호작용</strong>으로 구성</li>
<li>대표 언어: Java, C++</li>
</ul>
<hr>
<h3 id="여러-패러다임을-지원하는-언어">여러 패러다임을 지원하는 언어</h3>
<ul>
<li>하나의 언어가 반드시 하나의 프로그래밍 패러다임만 지원하는 것은 아님</li>
<li>C++, Python, JavaScript 등은 여러 프로그래밍 패러다임을 지원</li>
<li>Java는 JDK 8 이전에는 객체지향 프로그래밍을 중심으로 사용되었으며, <strong>JDK 8부터 함수형 프로그래밍을 위한 기능이 추가</strong></li>
<li>따라서 Java는 순수 함수형 언어가 아니라 <strong>객체지향을 중심으로 함수형 프로그래밍도 지원하는 다중 패러다임 언어</strong></li>
</ul>
<hr>
<h2 id="121-선언형과-함수형--프로그래밍">1.2.1 선언형과 함수형  프로그래밍</h2>
<p>선언형  프로그래밍 (declarative programming)이란 &#39;무엇을&#39; 풀어내는가에 집중하는 패러다임</p>
<p>&quot;프로그램은 함수로 이루어진 것이다.&quot;</p>
<p>함수형 프로그래밍 (functional programming) 선언형 패러다임의 일종</p>
<p>ex) 자연수로 이루어진 배열에서 최댓값을 찾기</p>
<pre><code class="language-javascript">const list = [1, 2, 3, 4, 5, 11, 12]
const ret = list.reduce((max,num) =&gt; num &gt; max ? num : max, 0)
console.log(ret)</code></pre>
<ul>
<li>reduce()는 배열만 받아서 누적한 결괏값을 반환하는 순수 함수</li>
</ul>
<p>함수형 프로그래밍은 이와 같은 작은 &#39;순수 함수&#39;들을 블록처럼 쌓아 로직을 구현하고 &#39;고차 함수&#39;를 통해 재사용성을 높인 프로그래밍 패러다임입니다. 자바스크립트는 단순하고 유연한 언어이며, 함수가 일급 객체이기 때문에 객체지향 프로그래밍보다는 함수형 프로그래밍 방식이 선호</p>
<h3 id="순수-함수">순수 함수</h3>
<ul>
<li><strong>출력값이 입력값에만 의존하는 함수</strong></li>
<li>같은 입력값을 넣으면 항상 같은 결과를 반환</li>
<li>함수 외부의 상태를 변경하거나 외부 상태에 의존하지 않음</li>
</ul>
<pre><code class="language-javascript">const pure = (a, b) =&gt; {
  return a + b
}</code></pre>
<p>위 <code>pure</code> 함수의 결과는 매개변수 <code>a</code>, <code>b</code>에만 영향을 받음.</p>
<p>만약 <code>a</code>, <code>b</code> 이외의 전역 변수 <code>c</code> 등이 결과에 영향을 준다면 순수 함수가 아님.</p>
<hr>
<h3 id="고차-함수">고차 함수</h3>
<ul>
<li><strong>함수를 매개변수로 받거나 함수를 반환하는 함수</strong></li>
<li>함수를 하나의 값처럼 활용하여 로직을 구성하는 방식</li>
</ul>
<h4 id="일급-객체">일급 객체</h4>
<p>고차 함수를 사용하려면 <strong>함수를 일급 객체로 다룰 수 있어야 함</strong></p>
<p>함수가 일급 객체라는 것은 다음 조건을 만족한다는 의미.</p>
<ul>
<li>함수를 변수에 할당 가능</li>
<li>함수를 다른 함수의 매개변수로 전달 가능</li>
<li>함수에서 다른 함수를 반환 가능</li>
</ul>
<blockquote>
<p>함수형 프로그래밍은 순수 함수와 고차 함수 외에도 <strong>불변성(Immutability), 커링(Currying), 함수 합성(Function Composition)</strong> 등의 특징을 가짐.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 13장 정리
]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-13%EC%9E%A5-%EC%A0%95%EB%A6%AC-1baxkdct</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-13%EC%9E%A5-%EC%A0%95%EB%A6%AC-1baxkdct</guid>
            <pubDate>Sun, 16 Aug 2026 07:25:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-12%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 12장 정리</a></p>
</blockquote>
<h1 id="검색어-자동완성-시스템">검색어 자동완성 시스템</h1>
<p>이번 장에서는 <strong>검색어 자동완성 시스템</strong>에 대해서 설명한다.</p>
<p>검색어 자동완성은 구글, 유튜브, 네이버 등 대부분의 검색 서비스에서 쉽게 볼 수 있는 기능이다.</p>
<p>웹페이지를 구현하면서 검색 필터는 직접 만들어본 적이 있지만, 검색어 자동완성 시스템은 구현해본 적이 없다. 평소에는 너무 자연스럽게 사용하는 기능이라 막상 &quot;이걸 어떻게 구현할까?&quot; 생각해보면 구조가 쉽게 떠오르지 않았다.</p>
<p><img src="https://velog.velcdn.com/images/jung_ji_in02/post/d5278728-d32d-4d64-921c-43e2719ef8a7/image.png" alt=""></p>
<p>이번 장에서는 위와 같이 <strong>사용자가 입력한 검색어와 연관된 인기 검색어 k개를 자동완성 결과로 제공하는 시스템</strong>을 설계해볼 것이다.</p>
<hr>
<h2 id="1-문제-이해-및-설계-범위-확정">1. 문제 이해 및 설계 범위 확정</h2>
<h3 id="요구사항">요구사항</h3>
<ul>
<li><strong>빠른 응답 속도</strong>: 자동완성 결과를 100ms 이내에 제공</li>
<li><strong>연관성</strong>: 사용자가 입력한 검색어와 관련된 결과 제공</li>
<li><strong>정렬</strong>: 인기도 등의 순위 모델을 기준으로 결과 정렬</li>
<li><strong>규모 확장성</strong>: 대규모 트래픽을 처리할 수 있는 구조</li>
<li><strong>고가용성</strong>: 일부 시스템에 장애가 발생하더라도 서비스 지속 가능</li>
</ul>
<h3 id="개략적-규모-추정">개략적 규모 추정</h3>
<ul>
<li>일간 능동 사용자(DAU): <strong>1,000만 명</strong></li>
<li>사용자 한 명당 하루 평균 검색: <strong>10건</strong></li>
<li>평균 검색어 길이: <strong>20자</strong></li>
<li>검색창에 문자를 입력할 때마다 검색어 자동완성 백엔드에 요청 전송</li>
</ul>
<p>예를 들어 <code>dinner</code>를 검색한다고 하면 다음과 같이 문자를 입력할 때마다 요청이 발생한다.</p>
<pre><code class="language-text">search?q=d
search?q=di
search?q=din
search?q=dinn
search?q=dinne
search?q=dinner</code></pre>
<p>평균 검색어 길이를 20자로 가정하면 <strong>검색 1회당 평균 20개의 자동완성 요청</strong>이 발생한다.</p>
<h3 id="qps-계산">QPS 계산</h3>
<pre><code class="language-text">10,000,000명 × 10회 × 20개 요청
────────────────────────────── ≈ 24,000 QPS
            86,400초</code></pre>
<ul>
<li>평균 QPS: 약 <strong>24,000</strong></li>
<li>최대 QPS: 평균의 2배로 가정하여 약 <strong>48,000</strong></li>
</ul>
<h3 id="신규-검색어-데이터">신규 검색어 데이터</h3>
<p>전체 검색 질의 중 약 <strong>20%가 새로운 검색어</strong>라고 가정한다.</p>
<p>검색어 하나의 크기를 약 20byte로 계산하면 다음과 같다.</p>
<pre><code class="language-text">10,000,000명 × 10회 × 20% × 20byte
= 약 0.4GB/day</code></pre>
<p>따라서 하루에 약 <strong>0.4GB의 신규 검색어 데이터가 시스템에 추가</strong>된다.</p>
<hr>
<h2 id="2-개략적-설계안-제시-및-동의-구하기">2. 개략적 설계안 제시 및 동의 구하기</h2>
<p>검색어 자동완성 시스템은 크게 <strong>데이터 수집 서비스</strong>와 <strong>질의 서비스</strong>로 나눌 수 있다.</p>
<pre><code class="language-text">사용자
  │
  ├─ 검색 질의 ───→ 데이터 수집 서비스
  │                       │
  │                       ↓
  │                 검색어 및 빈도 저장
  │
  └─ 자동완성 요청 ─→ 질의 서비스
                          │
                          ↓
                  인기 검색어 k개 반환</code></pre>
<h3 id="데이터-수집-서비스">데이터 수집 서비스</h3>
<ul>
<li>사용자가 입력한 검색 질의를 수집하는 시스템</li>
<li>검색어별 검색 빈도 저장</li>
<li>검색어의 인기도를 계산하기 위한 데이터 생성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jung_ji_in02/post/feff003c-0bcc-40d3-9612-3673b5c7198b/image.png" alt=""></p>
<h3 id="질의-서비스">질의 서비스</h3>
<ul>
<li>사용자가 입력한 문자열을 기반으로 자동완성 검색어 조회</li>
<li>검색 빈도를 기준으로 인기 있는 검색어 k개 반환</li>
<li>자동완성 기능 특성상 빠른 응답 속도가 중요</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jung_ji_in02/post/5cf4a758-22fd-4b9a-bca8-d1f346f6f99f/image.png" alt=""></p>
<p>예를들어 여기서 가장 많이 사용된 5개의 검색어(&quot;top5&quot;)는 아래의 SQL 질의문을 사용해 계산할 수 있다.</p>
<pre><code class="language-sql">SELECT *
FROM frequency_table
WHERE query LIKE &#39;prefix%&#39;
ORDER BY frequency DESC
LIMIT 5;</code></pre>
<ul>
<li>WHERE query LIKE &#39;prefix%&#39; → prefix로 시작하는 검색어 조회</li>
<li>% → 뒤에 어떤 문자열이 와도 허용하는 와일드카드</li>
<li>ORDER BY frequency DESC → 검색 빈도가 높은 순으로 내림차순 정렬</li>
<li>LIMIT 5 → 상위 5개만 반환</li>
</ul>
<hr>
<h2 id="3-상세-설계">3. 상세 설계</h2>
<p>앞서 데이터 수집 서비스와 질의 서비스로 구성된 개략적인 설계안을 살펴보았다.</p>
<p>하지만 관계형 데이터베이스에서 매번 접두어(prefix)를 기준으로 검색하고, 검색 빈도에 따라 정렬하여 상위 검색어를 찾는 방식은 데이터가 많아질수록 효율적이지 않다.</p>
<p>따라서 상세 설계에서는 검색어 자동완성 시스템의 핵심 자료구조인 <strong>트라이(Trie)</strong> 를 중심으로 시스템을 개선한다.</p>
<p>이번 절에서는 다음 내용을 순서대로 살펴본다.</p>
<ul>
<li>트라이(Trie) 자료구조</li>
<li>데이터 수집 서비스</li>
<li>질의 서비스</li>
<li>규모 확장이 가능한 저장소</li>
<li>트라이 연산</li>
</ul>
<hr>
<h3 id="트라이-자료구조">트라이 자료구조</h3>
<ul>
<li>문자열을 효율적으로 저장하고 검색하기 위한 <strong>트리 형태의 자료구조</strong></li>
<li><code>Trie</code>라는 이름은 문자열을 찾아낸다는 의미의 <code>retrieval</code>에서 유래</li>
<li>검색어 자동완성과 같이 <strong>접두어(prefix)를 기반으로 문자열을 찾는 작업</strong>에 적합</li>
</ul>
<p>트라이의 기본적인 특징은 다음과 같다.</p>
<ul>
<li>루트 노드는 빈 문자열을 의미</li>
<li>각 노드는 하나의 문자(character)를 저장</li>
<li>루트에서 특정 노드까지 이어진 경로가 하나의 문자열 또는 접두어를 의미</li>
<li>각 노드는 다음에 등장할 수 있는 문자에 해당하는 자식 노드를 가질 수 있음</li>
</ul>
<p>예를 들어 <code>tree</code>, <code>try</code>, <code>true</code>, <code>toy</code>, <code>wish</code>, <code>win</code>과 같은 검색어를 하나의 트라이에 저장할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jung_ji_in02/post/dd1b7cad-d7db-477b-97a1-92620431226e/image.png" alt=""></p>
<p>기본적인 트라이는 문자열만 저장하지만, 검색어 자동완성에서는 <strong>인기 있는 검색어를 순서대로 반환해야 하므로 검색 빈도 정보도 함께 저장할 필요가 있다.</strong></p>
<p>예를 들어 다음과 같은 검색 빈도를 가진다고 하자.</p>
<pre><code class="language-text">tree   → 10
try    → 29
true   → 35
toy    → 14
wish   → 25
win    → 50</code></pre>
<p>트라이의 각 검색어가 완성되는 노드에 해당 검색어의 빈도를 함께 저장하면, 사용자가 특정 접두어를 입력했을 때 그 접두어 아래에 존재하는 검색어들의 빈도를 비교할 수 있다.</p>
<hr>
<h4 id="인기-검색어-k개-찾기">인기 검색어 k개 찾기</h4>
<p>트라이에서 특정 접두어에 해당하는 인기 검색어 <code>k</code>개를 찾는 과정은 다음과 같다.</p>
<ol>
<li>입력된 접두어를 나타내는 노드 탐색</li>
<li>해당 노드의 하위 트리를 탐색하여 모든 유효 검색어 탐색</li>
<li>검색어를 빈도 기준으로 정렬</li>
<li>가장 빈도가 높은 <code>k</code>개의 검색어 반환</li>
</ol>
<p>이때 다음과 같이 정의한다.</p>
<ul>
<li><code>p</code>: 접두어(prefix)의 길이</li>
<li><code>n</code>: 트라이 전체 노드 수</li>
<li><code>c</code>: 해당 접두어 아래에 존재하는 노드 수</li>
</ul>
<p>각 단계의 시간 복잡도는 다음과 같다.</p>
<pre><code class="language-text">접두어 노드 탐색        → O(p)
하위 트리 탐색          → O(c)
검색어 빈도 기준 정렬   → O(c log c)</code></pre>
<p>따라서 전체 시간 복잡도는</p>
<pre><code class="language-text">O(p) + O(c) + O(c log c)</code></pre>
<p>가 된다.</p>
<p>예를 들어 사용자가 <code>be</code>를 입력했고 해당 접두어 아래에 다음과 같은 검색어가 있다고 하자.</p>
<pre><code class="language-text">beer → 10
best → 35
bet  → 29</code></pre>
<p><code>k = 2</code>라면 빈도가 가장 높은</p>
<pre><code class="language-text">best → 35
bet  → 29</code></pre>
<p>를 자동완성 결과로 반환한다.</p>
<hr>
<h4 id="기본-트라이-방식의-문제점">기본 트라이 방식의 문제점</h4>
<p>위 방식은 동작 자체는 단순하지만, 최악의 경우 인기 검색어 <code>k</code>개를 찾기 위해 <strong>접두어 아래의 하위 트리를 모두 탐색해야 할 수 있다.</strong></p>
<p>검색어의 수가 매우 많아지면 자동완성 요청마다 많은 노드를 탐색하고 정렬해야 하기 때문에 빠른 응답 시간을 보장하기 어렵다.</p>
<p>이를 개선하기 위해 다음 두 가지 최적화 방법을 사용할 수 있다.</p>
<ol>
<li>접두어의 최대 길이 제한</li>
<li>각 노드에 인기 검색어 캐시</li>
</ol>
<hr>
<h4 id="접두어-최대-길이-제한">접두어 최대 길이 제한</h4>
<p>사용자가 입력하는 검색어의 길이는 일반적으로 무한히 길지 않다.</p>
<p>따라서 검색어의 최대 길이를 일정한 값으로 제한하면 <code>p</code>를 작은 상수값으로 볼 수 있다.</p>
<pre><code class="language-text">O(p) → O(1)</code></pre>
<p>즉, 접두어에 해당하는 노드를 찾는 비용을 사실상 상수 시간으로 제한할 수 있다.</p>
<blockquote>
<h4 id="op-≈-o1의-의미">O(p) ≈ O(1)의 의미</h4>
<p>책에서는 검색어의 최대 길이를 제한하면 접두어 탐색을 O(1)로 볼 수 있다고 설명한다. 이는 트라이 탐색 자체의 시간복잡도가 O(p)에서 실제로 O(1)로 변하는 것은 아니다.
p ≤ 50처럼 입력 길이에 상한을 두어 p가 더 이상 무한히 증가하지 않으므로, 시스템 설계 관점에서 상수 시간으로 취급하는 것이다.</p>
</blockquote>
<hr>
<h4 id="각-노드에-인기-검색어-캐시">각 노드에 인기 검색어 캐시</h4>
<p>각 노드에 해당 접두어로 시작하는 <strong>상위 k개의 인기 검색어를 미리 저장</strong>하는 방식이다.</p>
<p>예를 들어 <code>be</code> 노드에 다음 결과를 미리 저장해둘 수 있다.</p>
<pre><code class="language-text">best → 35
bet  → 29
bee  → 20
be   → 15
beer → 10</code></pre>
<p>그러면 사용자가 <code>be</code>를 입력했을 때 하위 트리를 다시 탐색하거나 정렬할 필요 없이 해당 노드에 저장된 결과를 바로 반환할 수 있다.</p>
<p>따라서</p>
<pre><code class="language-text">하위 트리 탐색      → O(1)
Top k 검색어 조회   → O(1)</code></pre>
<p>로 줄일 수 있다.</p>
<blockquote>
<p>단점은 각 노드마다 인기 검색어를 저장해야 하므로 추가적인 저장 공간이 필요하다는 점이다.</p>
</blockquote>
<p>하지만 검색어 자동완성 시스템에서는 <strong>응답 속도가 매우 중요하기 때문에 저장 공간을 일부 희생하더라도 빠른 조회 성능을 얻는 것이 더 유리하다.</strong></p>
<p>두 가지 최적화를 모두 적용하면 자동완성 질의의 시간 복잡도는 사실상</p>
<pre><code class="language-text">O(1)</code></pre>
<p>수준으로 줄일 수 있다.</p>
<hr>
<h3 id="데이터-수집-서비스-1">데이터 수집 서비스</h3>
<p>앞서 살펴본 트라이는 검색어와 빈도 정보를 기반으로 동작한다.</p>
<p>단순하게 생각하면 사용자가 검색할 때마다 실시간으로 트라이의 빈도를 갱신할 수도 있지만, 실제 대규모 시스템에서는 이러한 방식이 적절하지 않다.</p>
<p>그 이유는 다음과 같다.</p>
<ul>
<li>하루에도 수천만 건 이상의 검색 요청 발생 가능</li>
<li>검색 요청마다 트라이를 갱신할 경우 질의 서비스 성능 저하</li>
<li>인기 검색어의 순위는 매 요청마다 즉시 갱신할 필요가 없는 경우가 많음</li>
</ul>
<p>따라서 사용자 검색 데이터를 먼저 수집한 뒤, 일정한 주기로 가공하여 트라이를 갱신하는 구조를 사용한다.</p>
<pre><code class="language-text">데이터 분석 서비스 로그
        ↓
로그 취합 서버
        ↓
취합된 데이터
        ↓
작업 서버
        ↓
트라이 데이터베이스
        ↓
트라이 캐시</code></pre>
<h4 id="데이터-분석-서비스-로그">데이터 분석 서비스 로그</h4>
<p>사용자가 검색창에 입력한 질의와 시간 등의 원본 데이터를 저장한다.</p>
<p>예를 들면 다음과 같다.</p>
<pre><code class="language-text">query   time
tree    22:01:01
try     22:01:05
tree    22:01:30
toy     22:02:22
tree    22:02:42
try     22:03:03</code></pre>
<p>원본 로그는 새로운 검색 요청이 발생할 때마다 추가되며, 이후 인기 검색어를 계산하기 위한 기초 데이터로 사용된다.</p>
<h4 id="로그-취합-서버">로그 취합 서버</h4>
<p>데이터 분석 서비스에서 생성되는 로그는 양이 매우 크기 때문에 그대로 사용하는 것은 비효율적이다.</p>
<p>따라서 로그 취합 서버에서 데이터를 일정한 기준으로 집계하여 시스템에서 사용하기 쉬운 형태로 변환한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">tree → 3
try  → 2
toy  → 1</code></pre>
<p>와 같이 검색어별 빈도를 계산할 수 있다.</p>
<p>실시간성이 매우 중요한 서비스라면 짧은 간격으로 집계할 수 있고, 일반적인 검색 서비스처럼 순위 변화가 비교적 느리다면 하루나 일주일 단위로 집계할 수도 있다.</p>
<hr>
<h3 id="취합된-데이터">취합된 데이터</h3>
<ul>
<li>로그 취합 서버에서 원본 검색 로그를 <strong>일정한 시간 단위로 집계한 데이터</strong></li>
<li>본 설계에서는 검색어의 인기도가 빠르게 변하지 않는다고 가정하여 <strong>일주일 단위로 취합</strong></li>
<li><code>time</code>: 해당 주가 시작된 날짜</li>
<li><code>frequency</code>: 해당 주에 검색어가 사용된 횟수의 합</li>
</ul>
<p>예를 들어 다음과 같은 형태로 저장</p>
<pre><code class="language-text">query    time          frequency
tree     2019-10-01    12000
tree     2019-10-08    15000
tree     2019-10-15     9000
toy      2019-10-01     8500
...</code></pre>
<blockquote>
<p>데이터 취합 주기는 서비스 특성에 따라 결정. 검색어 순위의 실시간성이 중요하다면 짧은 주기를 사용하고, 그렇지 않다면 일주일 정도의 주기로도 충분</p>
</blockquote>
<hr>
<h3 id="작업-서버">작업 서버</h3>
<ul>
<li>주기적으로 <strong>비동기 작업(job)을 수행하는 서버</strong></li>
<li>취합된 데이터를 기반으로 새로운 트라이 자료구조 생성</li>
<li>생성된 트라이를 <strong>트라이 데이터베이스에 저장</strong></li>
<li>검색 요청을 직접 처리하는 서버와 트라이 생성 작업을 분리하여 요청 처리에 미치는 영향 최소화</li>
</ul>
<hr>
<h3 id="트라이-캐시">트라이 캐시</h3>
<ul>
<li>트라이 데이터를 메모리에 저장하는 <strong>분산 캐시 시스템</strong></li>
<li>데이터베이스보다 빠른 읽기를 통해 자동완성 요청의 응답 속도 향상</li>
<li>주기적으로 <strong>트라이 데이터베이스의 스냅샷을 가져와 갱신</strong></li>
<li>자동완성 질의가 발생하면 데이터베이스보다 트라이 캐시를 우선 조회</li>
</ul>
<hr>
<h3 id="트라이-데이터베이스">트라이 데이터베이스</h3>
<ul>
<li>생성된 트라이 데이터를 보관하는 <strong>지속성 저장소</strong></li>
<li>트라이를 저장하는 방법으로 다음 두 가지 방식 고려 가능</li>
</ul>
<h4 id="1-문서-저장소">1. 문서 저장소</h4>
<ul>
<li>새로운 트라이를 주기적으로 생성하므로 <strong>트라이 전체를 직렬화하여 하나의 문서 형태로 저장</strong></li>
<li>MongoDB와 같은 문서 저장소 활용 가능</li>
</ul>
<h4 id="2-키-값-저장소">2. 키-값 저장소</h4>
<p>트라이의 각 노드를 <code>&lt;키, 값&gt;</code> 형태로 변환하여 저장</p>
<ul>
<li><strong>키</strong>: 해당 트라이 노드가 나타내는 접두어</li>
<li><strong>값</strong>: 해당 노드에 캐시된 인기 검색어 목록</li>
</ul>
<p>예를 들어 <code>be</code> 노드가 다음 데이터를 가지고 있다면</p>
<pre><code class="language-text">[be: 15, bee: 20, beer: 10, best: 35]</code></pre>
<p>키-값 저장소에서는 다음과 같이 표현 가능</p>
<pre><code class="language-text">Key   → Value

b     → [be:15, bee:20, beer:10, best:35]
be    → [be:15, bee:20, beer:10, best:35]
bee   → [bee:20, beer:10]
bes   → [best:35]
beer  → [beer:10]
best  → [best:35]</code></pre>
<p>즉, 트라이의 계층 구조를 그대로 저장하지 않고 <strong>접두어를 키로 사용하여 해당 접두어의 자동완성 결과를 바로 조회할 수 있는 형태로 변환</strong>하는 방식이다.</p>
<hr>
<h2 id="질의-서비스-1">질의 서비스</h2>
<p>질의 서비스는 사용자가 검색창에 문자를 입력할 때 <strong>자동완성 검색어를 실제로 반환하는 영역</strong>이다.</p>
<p>전체적인 요청 흐름은 다음과 같다.</p>
<pre><code class="language-text">사용자
  ↓
로드밸런서
  ↓
API 서버
  ↓
트라이 캐시
  ↓ Cache Miss
트라이 데이터베이스</code></pre>
<ol>
<li>사용자가 입력한 검색 질의를 로드밸런서로 전송</li>
<li>로드밸런서가 요청을 여러 API 서버 중 하나로 전달</li>
<li>API 서버가 <strong>트라이 캐시에서 해당 접두어의 자동완성 결과 조회</strong></li>
<li>캐시에 데이터가 없다면 트라이 데이터베이스에서 조회한 뒤 캐시에 저장</li>
<li>조회한 인기 검색어 목록을 사용자에게 반환</li>
</ol>
<blockquote>
<p><strong>Cache Miss</strong></p>
<p>요청한 데이터가 캐시에 존재하지 않아 데이터베이스와 같은 원본 저장소에서 데이터를 가져와야 하는 상황</p>
</blockquote>
<p>한 번 데이터베이스에서 가져온 데이터는 캐시에 저장하기 때문에, 이후 같은 접두어 요청은 캐시에서 바로 처리 가능</p>
<hr>
<h3 id="질의-서비스-성능-최적화">질의 서비스 성능 최적화</h3>
<p>검색어 자동완성은 사용자가 문자를 입력할 때마다 요청이 발생할 수 있기 때문에 <strong>매우 빠른 응답 속도</strong>가 중요하다.</p>
<p>이를 위해 다음과 같은 최적화 방법 사용 가능</p>
<h4 id="ajax-요청">AJAX 요청</h4>
<ul>
<li>페이지 전체를 새로고침하지 않고 <strong>자동완성 결과만 비동기적으로 요청</strong></li>
<li>사용자가 문자를 입력할 때 서버로 질의를 보내고 결과만 갱신</li>
<li>불필요한 페이지 재로딩 방지</li>
</ul>
<h4 id="브라우저-캐싱">브라우저 캐싱</h4>
<ul>
<li>자동완성 결과는 짧은 시간 동안 크게 변하지 않으므로 <strong>클라이언트의 브라우저에도 결과를 캐싱</strong></li>
<li>동일하거나 관련된 요청에 대해 서버 요청을 줄이고 브라우저 캐시에서 바로 결과 사용 가능</li>
</ul>
<p>예를 들어 HTTP 응답에 다음과 같은 캐시 정책 설정 가능</p>
<pre><code class="language-text">Cache-Control: private, max-age=3600</code></pre>
<ul>
<li><code>private</code>: 해당 사용자의 브라우저와 같은 <strong>개인 캐시에만 저장 허용</strong></li>
<li><code>max-age=3600</code>: 응답을 <strong>3600초(1시간) 동안 유효한 캐시로 사용</strong></li>
</ul>
<hr>
<h3 id="데이터-샘플링">데이터 샘플링</h3>
<ul>
<li>대규모 시스템에서 모든 검색 질의를 로그로 저장하면 <strong>CPU와 저장 공간을 과도하게 사용</strong></li>
<li>모든 요청을 저장하는 대신 일부 요청만 선택적으로 기록하는 방식</li>
<li>예를 들어 <code>N</code>개의 요청 중 <code>1</code>개만 로깅하는 방식 적용 가능</li>
</ul>
<pre><code class="language-text">전체 검색 요청
     ↓
일부 요청만 샘플링
     ↓
데이터 분석 로그 저장</code></pre>
<blockquote>
<p>모든 요청을 반드시 분석할 필요가 없다면 데이터 샘플링을 통해 로그 저장 비용과 처리 비용 절감 가능</p>
</blockquote>
<hr>
<h2 id="트라이-연산">트라이 연산</h2>
<p>트라이 기반 자동완성 시스템에서는 트라이를 조회하는 것뿐만 아니라 <strong>생성·갱신·검색어 삭제</strong> 등의 연산도 필요하다.</p>
<h3 id="트라이-생성">트라이 생성</h3>
<ul>
<li><strong>작업 서버가 트라이 생성 담당</strong></li>
<li>데이터 분석 서비스의 로그 또는 취합된 데이터를 기반으로 트라이 생성</li>
<li>생성된 트라이를 트라이 데이터베이스에 저장</li>
<li>이후 트라이 캐시에 반영하여 질의 서비스에서 사용</li>
</ul>
<pre><code class="language-text">로그
 ↓
취합된 데이터
 ↓
작업 서버
 ↓
트라이 생성
 ↓
트라이 데이터베이스
 ↓
트라이 캐시</code></pre>
<hr>
<h3 id="트라이-갱신">트라이 갱신</h3>
<p>트라이를 갱신하는 방법은 크게 두 가지</p>
<h4 id="1-트라이-전체-교체">1. 트라이 전체 교체</h4>
<ul>
<li>일정한 주기마다 새로운 트라이 생성</li>
<li>생성이 완료되면 기존 트라이를 새로운 트라이로 교체</li>
<li>본 설계에서 사용하는 방식</li>
</ul>
<pre><code class="language-text">기존 Trie
    ↓
새로운 Trie 생성
    ↓
완성 후 기존 Trie 교체</code></pre>
<p>검색 요청을 처리하면서 트라이를 계속 수정하지 않아도 되므로 대규모 환경에서 관리하기 유리</p>
<h4 id="2-개별-노드-갱신">2. 개별 노드 갱신</h4>
<ul>
<li>검색 빈도가 변경될 때 해당 검색어가 위치한 노드를 직접 수정</li>
<li>검색어 빈도가 변경되면 <strong>상위 노드에 캐시된 인기 검색어 정보도 함께 갱신 필요</strong></li>
</ul>
<p>예를 들어</p>
<pre><code class="language-text">beer: 10 → beer: 30</code></pre>
<p>으로 변경되었다면 <code>beer</code> 노드만 수정하는 것이 아니라</p>
<pre><code class="language-text">beer
 ↑
bee
 ↑
be
 ↑
b</code></pre>
<p>와 같이 상위 노드에 저장된 인기 검색어 목록에도 <code>beer: 30</code>을 반영해야 한다.</p>
<p>따라서 개별 노드 갱신은 트라이가 작을 때는 고려할 수 있지만, <strong>대규모 트라이에서는 갱신 비용이 커질 수 있음</strong></p>
<hr>
<h3 id="검색어-삭제">검색어 삭제</h3>
<p>자동완성 결과에는 다음과 같은 부적절한 검색어가 포함될 수 있다.</p>
<ul>
<li>혐오성 질의</li>
<li>폭력적인 질의</li>
<li>성적으로 노골적인 질의</li>
<li>기타 서비스 정책상 노출하면 안 되는 질의</li>
</ul>
<p>이러한 검색어를 트라이에서 즉시 물리적으로 삭제하는 대신 <strong>트라이 캐시와 API 서버 사이에 필터 계층(Filter Layer)을 배치</strong>할 수 있다.</p>
<pre><code class="language-text">트라이 캐시
    ↓
필터 계층
    ↓
API 서버
    ↓
사용자</code></pre>
<ul>
<li>트라이에서 검색된 결과를 필터 규칙에 따라 검사</li>
<li>부적절한 검색어는 사용자에게 반환하지 않음</li>
<li>필터 규칙을 변경하는 것만으로 빠르게 노출 정책 변경 가능</li>
<li>실제 트라이 데이터에서의 물리적 삭제는 다음 업데이트 주기에 비동기적으로 처리 가능</li>
</ul>
<p>즉, <strong>즉각적인 노출 차단은 필터 계층</strong>, <strong>실제 데이터 삭제는 이후 트라이 갱신 과정</strong>에서 담당하는 구조</p>
<hr>
<h2 id="저장소-규모-확장">저장소 규모 확장</h2>
<p>트라이의 크기가 커져 하나의 서버에 모두 저장하기 어려워지면 <strong>트라이 데이터를 여러 서버에 분산 저장하는 샤딩(Sharding)</strong>이 필요하다.</p>
<p>가장 단순한 방법은 검색어의 <strong>첫 글자를 기준으로 샤딩</strong>하는 것이다.</p>
<p>예를 들어 서버가 두 대라면</p>
<pre><code class="language-text">Server 1 → a ~ m
Server 2 → n ~ z</code></pre>
<p>세 대라면</p>
<pre><code class="language-text">Server 1 → a ~ i
Server 2 → j ~ r
Server 3 → s ~ z</code></pre>
<p>처럼 검색어를 분산할 수 있다.</p>
<p>하지만 이 방식에는 문제가 있다.</p>
<h3 id="첫-글자-기반-샤딩의-문제점">첫 글자 기반 샤딩의 문제점</h3>
<ul>
<li>알파벳 기준으로 단순 분할하면 최대 26개 영역으로만 구분</li>
<li>검색어의 첫 글자별 데이터 양이 서로 다르기 때문에 <strong>서버별 부하 불균형 발생 가능</strong></li>
<li>특정 문자로 시작하는 검색어가 많으면 해당 서버에 데이터와 요청 집중</li>
</ul>
<p>예를 들어 <code>s</code>로 시작하는 검색어가 <code>x</code>, <code>y</code>, <code>z</code>로 시작하는 검색어보다 훨씬 많다면 단순히 알파벳 개수만 동일하게 나누는 것은 균등한 샤딩이 아니다.</p>
<hr>
<h3 id="검색어-분포를-고려한-샤딩">검색어 분포를 고려한 샤딩</h3>
<p>단순히 알파벳 범위를 동일하게 나누는 대신 <strong>과거 검색어 데이터의 분포를 분석하여 각 샤드가 비슷한 양의 데이터를 가지도록 분할</strong>하는 방식</p>
<p>이를 위해 <strong>샤드 관리자(Shard Map Manager)</strong>가 어떤 검색어가 어느 저장소 서버에 저장되는지를 관리한다.</p>
<p>예를 들어 <code>s</code>로 시작하는 검색어가 매우 많다면</p>
<pre><code class="language-text">Shard 1 → s로 시작하는 검색어

Shard 2 → u ~ z로 시작하는 검색어</code></pre>
<p>처럼 데이터 양을 기준으로 범위를 다르게 설정할 수 있다.</p>
<blockquote>
<p>핵심은 <strong>문자의 개수를 동일하게 나누는 것이 아니라 실제 검색어의 분포를 기준으로 데이터를 최대한 균등하게 분산하는 것</strong>이다.</p>
</blockquote>
<h3 id="샤드-관리자">샤드 관리자</h3>
<p>검색어를 여러 저장소 서버에 분산했다면, <strong>어떤 검색어가 어느 샤드에 저장되어 있는지 관리하는 정보</strong>가 필요하다.</p>
<p>이를 담당하는 것이 <strong>검색어 대응 샤드 관리자(Shard Map Manager)</strong>다.</p>
<pre><code class="language-text">웹 서버
   ↓
검색어가 어느 샤드에 있는지 조회
   ↓
샤드 관리자
   ↓
해당 샤드 정보 반환
   ↓
웹 서버
   ↓
해당 데이터베이스 샤드에서 데이터 조회</code></pre>
<ul>
<li>웹 서버는 먼저 샤드 관리자에게 검색어가 저장된 위치 확인</li>
<li>샤드 관리자는 해당 검색어가 저장된 샤드 정보 반환</li>
<li>웹 서버는 반환받은 샤드에서 실제 데이터 조회</li>
<li>검색어 분포가 변경되면 샤드 매핑 정보도 조정 가능</li>
</ul>
<blockquote>
<p>즉, 샤드 관리자는 <strong>검색어와 실제 저장소 위치를 연결해주는 역할</strong></p>
</blockquote>
<hr>
<h2 id="4-마무리">4. 마무리</h2>
<p>상세 설계를 마친 이후에는 기본 요구사항 외에도 다음과 같은 확장 질문을 고려할 수 있다.</p>
<h3 id="다국어-지원">다국어 지원</h3>
<p>비영어권 국가까지 지원하려면 영어 알파벳만을 기준으로 트라이를 구성할 수 없다.</p>
<ul>
<li>트라이에 <strong>유니코드(Unicode) 문자 저장</strong></li>
<li>여러 언어의 문자 체계 지원 가능</li>
<li>국가 및 언어별 검색어 처리 가능</li>
</ul>
<blockquote>
<p>유니코드는 다양한 언어의 문자를 하나의 표준 체계로 표현하기 위한 문자 인코딩 표준</p>
</blockquote>
<hr>
<h3 id="국가별-인기-검색어-차이">국가별 인기 검색어 차이</h3>
<p>국가마다 많이 검색되는 검색어와 검색 순위가 다를 수 있다.</p>
<p>따라서</p>
<ul>
<li>국가별로 별도의 트라이 구성</li>
<li>각 지역 사용자에게 해당 국가의 인기 검색어 제공</li>
<li>트라이를 CDN과 가까운 위치에 배치하여 응답 속도 개선 가능</li>
</ul>
<p>즉,</p>
<pre><code class="language-text">한국 사용자 → 한국 검색어 Trie
미국 사용자 → 미국 검색어 Trie
일본 사용자 → 일본 검색어 Trie</code></pre>
<p>처럼 지역별 자동완성 데이터를 분리하여 관리하는 방식이다.</p>
<hr>
<h3 id="실시간-검색어-추세-반영">실시간 검색어 추세 반영</h3>
<p>앞서 설계한 방식은 일정 주기로 검색 데이터를 취합하고 트라이를 갱신한다.</p>
<p>하지만 뉴스나 특정 이벤트가 발생하면 어떤 검색어의 인기가 <strong>짧은 시간 안에 급격하게 증가</strong>할 수 있다.</p>
<p>현재 설계에서는 다음과 같은 한계가 있다.</p>
<ul>
<li>작업 서버가 일주일에 한 번 정도 실행</li>
<li>트라이 생성 자체에도 많은 시간 필요</li>
<li>급격하게 증가한 검색어를 즉시 반영하기 어려움</li>
</ul>
<p>따라서 실시간성을 높이려면 다음과 같은 방법을 고려할 수 있다.</p>
<h4 id="샤딩">샤딩</h4>
<p>전체 검색 데이터를 한 번에 처리하지 않고 여러 샤드로 나누어 처리</p>
<pre><code class="language-text">전체 검색 데이터
      ↓
Shard 1
Shard 2
Shard 3
...</code></pre>
<p>처리해야 하는 데이터의 범위를 줄여 갱신 속도 향상 가능</p>
<h4 id="순위-모델-개선">순위 모델 개선</h4>
<p>단순히 전체 검색 빈도만 사용하는 것이 아니라 <strong>최근 검색 데이터에 더 높은 가중치</strong>를 부여</p>
<p>예를 들어</p>
<pre><code class="language-text">인기도 점수
= 과거 검색 빈도
+ 최근 검색 빈도 × 높은 가중치</code></pre>
<p>와 같은 방식으로 최근 급격하게 증가한 검색어를 더 빠르게 상위에 노출 가능</p>
<h4 id="스트림-처리">스트림 처리</h4>
<p>검색 로그를 일정 기간 모았다가 한꺼번에 처리하는 대신 <strong>검색 데이터가 들어오는 즉시 지속적으로 처리하는 방식</strong></p>
<pre><code class="language-text">검색 요청
   ↓
스트림
   ↓
실시간 집계
   ↓
인기 검색어 갱신</code></pre>
<p>이를 위해 Kafka, Spark Streaming, Storm 등의 스트림 처리 시스템을 활용할 수 있다.</p>
<blockquote>
<p>기존 설계는 <strong>배치 처리 중심</strong>, 실시간 트렌드를 반영하려면 <strong>스트림 처리 방식 추가 필요</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 12장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-12%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-12%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 16 Aug 2026 07:24:16 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-11%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 11장 정리</a>
다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-13%EC%9E%A5-%EC%A0%95%EB%A6%AC-1baxkdct">[ 대규모 시스템 설계 스터디 ] 13장 정리</a></p>
</blockquote>
<hr>
<h1 id="12장-채팅-시스템-설계">12장 채팅 시스템 설계</h1>
<p>사실 채팅 시스템은 내가 다루는 웹 / 앱에서 정말 중요한 파트를 담당하는 부분이다.
실제로 작년 해커톤에서 채팅 시스템을 도입할려다가 시간과 경험의 한계에 부딪혀 직접 도입하지 못한 경험도 있다. </p>
<p>채팅 시스템이라면 대표적으로</p>
<p>왓츠앱, 어릴 때 자주 쓰던 페메, 그리고 위챗, 라인, 디스코드 등등 많은 채팅 시스템이 존재한다.</p>
<p>그만큼 다양한 채팅 앱이 있기 때문에 요구사항을 확실하게 정해두는 것이 중요하다!</p>
<hr>
<h2 id="1-문제-이해-및-설계-범위-확정">1. 문제 이해 및 설계 범위 확정</h2>
<p>아까 도입부에서 말했듯이, 채팅 앱을 어떻게 설계하려는지 확실하게 해 두는 것이 중요하다.</p>
<p>시장에 나와있는 앱들은 다 같은 채팅앱이 아니다. 각 앱의 특징들이 조끔식 따르기 때문에
어디에 집중하는 지를 잘 보고, 초반에 던저야 하는 질문들이 면접관이 원하는 앱이 정확히 무엇인 지를 알아내야한다.</p>
<p>예를 들어, 1대1 채팅인지, 그룹 채팅인지 아니면 둘 다 지원할 수 있어야하는 지, 모바일 앱인지 웹인지 자세히 물어봐야한다.</p>
<p>책에서 나온 예시에서 요구 사항은</p>
<ul>
<li>응답 지연이 낮은 일대일 채팅</li>
<li>최대 100명까지 참여할 수 있는 그룹 채팅 기능</li>
<li>사용자의 접속 상태 표시 기능</li>
<li>다양한 단말 지원, 하나의 계정으로 여러 단말에 동시 접속 지원</li>
<li>푸시 알람</li>
<li>5000만 DAU 처리</li>
</ul>
<p>즉, 페이스북 메신저와 유사한 채팅 앱을 설계해 보려고한다.</p>
<hr>
<h2 id="2-개략적-설계안-제시-및-동의-구하기">2. 개략적 설계안 제시 및 동의 구하기</h2>
<p>채팅 시스템은 클라이언트는 모바일 앱이거나 웹 애플리케이션이다.</p>
<p>클라이언트는 서로 직접 통신하는 것이 아닌, 채팅 서비스와 통신한다.</p>
<ul>
<li>클라이언트들로부터 메시지 수신</li>
<li>메시지 수신자 결정 및 전달</li>
<li>수신자가 접속 상태가 아닌 경우에는 접속할 때까지 해당 메시지 보관</li>
</ul>
<blockquote>
<h3 id="그렇다면-채팅-서비스는-옵저버-패턴인가">그렇다면 채팅 서비스는 옵저버 패턴인가?</h3>
<p>전에 옵저버 패턴에 대해서 배웠을 때, 특정 객체의 상태가 변하면 이를 구독하고 있는 객체들에게 알림을 전달하는 구조라는 점이 채팅 시스템과 굉장히 유사하다고 생각해서 한 번 찾아봤다.
결론적으로 채팅 서비스 자체가 옵저버 패턴인 것은 아니지만, 메시지 발생 시 관련 사용자에게 전달하는 구조는 옵저버 패턴과 유사함.
실제 대규모 채팅 시스템에서는 Pub/Sub(발행-구독) 구조로 이해하는 것이 더 적절하다고 한다.</p>
</blockquote>
<h3 id="클라이언트와-채팅-서비스-사이의-관계">클라이언트와 채팅 서비스 사이의 관계</h3>
<pre><code>메시지 송신 클라이언트
        ↓
      메시지
        ↓
채팅 서비스
1. 메시지 저장
2. 메시지 전달
        ↓
      메시지
        ↓
메시지 수신 클라이언트</code></pre><h4 id="메시지-송신">메시지 송신</h4>
<p>먼저 메시지 송신은 클라이언트가 채팅 서비스에 메시지를 보내는 경우 HTTP 사용 가능하다. 
클라이언트가 먼저 서버에 HTTP 요청을 보내고, 서버가 해당 메시지를 수신자에게 전달하는 방식이다.
그리고 keep-alive 헤더를 사용하면 클라이언트와 서버의 연결을 계속 유지할 수 있있다. 이 방식을 사용하면 TCP 연결을 반복적으로 할 필요가 없어져 비용 감소가 가능하다. 
그리고 실제로 페이스북 같은 초기 채팅 서비스에서도 HTTP를 사용하고 있다.</p>
<h4 id="메시지-수신">메시지 수신</h4>
<p>메시지 수신에서의 문제는 서버에서 클라이언트로 메시지를 전달하는 경우이다. 
HTTP는 기본적으로 클라이언트가 먼저 요청을 보내야 서버가 응답하는 구조이기 때문에 서버가 원하는 시점에 클라이언트에게 바로 메시지를 보내기가 어렵다.
그래서 이를 해결하기 위해 여러 방식을 사용하는데 그 예시는 </p>
<ul>
<li>폴링(Polling)</li>
<li>롱 폴링(Long Polling)</li>
<li>웹소켓(WebSocket)</li>
</ul>
<p>등등이 있다. </p>
<p>즉, 메시지를 보내는 것은 HTTP로 비교적 쉽게 처리할 수 있지만, 실시간으로 메시지를 받기 위해서는 별도의 통신 방식이 필요하다는 것이다. 나도 작년에 해커톤에서 도입하려고 시도했던건 웹 소켓 버전이다.</p>
<h3 id="폴링">폴링</h3>
<p>클라이언트가 주기적으로 서버에게 새 메세지가 있냐고 물어보는 방법이다.
폴링을 자주할 수록 비용이 증가하고, 단점으론 답해줄 메시지가 없는 경우에는 서버 자원이 불필요하게 낭비된다는 문제가 있다.</p>
<pre><code>클라이언트 → 서버 : 새 메시지가 있나?
서버 → 클라이언트 : 아니요
연결 종료

클라이언트 → 서버 : 새 메시지가 있나?
서버 → 클라이언트 : 아니요
연결 종료

클라이언트 → 서버 : 새 메시지가 있나?
서버 → 클라이언트 : 예, 새 메시지 반환
연결 종료

클라이언트 → 서버 : 새 메시지가 있나?
서버 → 클라이언트 : 아니요
연결 종료
...</code></pre><p>클라이언트가 서버에 새 메시지가 있는지 주기적으로 요청하고 메시지가 있으면 서버가 반환하고, 없으면 없다는 응답을 보낸 뒤 연결 종료한다. </p>
<p>새로운 메시지가 없어도 계속 요청을 보내기 때문에 불필요한 서버 자원과 네트워크 비용이 발생할 수 있다.</p>
<h3 id="롱-폴링">롱 폴링</h3>
<p>폴링은 여러 가지로 비효율적이기 때문에 나온 기법이 롱 폴링이다.</p>
<pre><code>클라이언트 → 서버 : 새 메시지가 있나?
서버 : 새 메시지가 올 때까지 연결 유지

새 메시지 발생
서버 → 클라이언트 : 새 메시지 반환
연결 종료

클라이언트 → 서버 : 새 메시지가 있나?
서버 : 일정 시간 동안 메시지가 없으면 대기
서버 → 클라이언트 : 타임아웃
연결 종료
...</code></pre><p>롱 폴링은 클라이언트가 서버에 요청을 보내면 새 메시지가 생기거나 타임아웃될 때까지 연결 유지한다.
새 메시지가 생기면 서버가 바로 반환하고 기존 연결 종료하고 이후 클라이언트가 다시 요청을 보내면서 같은 과정 반복한다.</p>
<p>롱 폴링의 장점은 폴링처럼 계속 요청을 보내지 않아도 되기 때문에 불필요한 요청을 줄일 수 있다.
다만 문제점은 송신 클라이언트와 수신 클라이언트가 같은 채팅 서버에 연결된다는 보장이 없다.</p>
<ul>
<li>HTTP 서버는 보통 무상태이고, 로드밸런서가 요청을 여러 서버에 분산하기 때문이다.</li>
<li>메시지를 받은 서버가 해당 수신자의 롱 폴링 연결을 가지고 있지 않을 수도 있다.</li>
<li>서버 입장에서는 클라이언트가 연결을 해제했는지 확인하기 어렵다.</li>
<li>메시지가 거의 없는 경우에도 타임아웃이 발생할 때마다 다시 연결해야 하기 때문에 여전히 비효율적이다.</li>
</ul>
<hr>
<h3 id="웹소켓websocket">웹소켓(WebSocket)</h3>
<p>앞서 롱 폴링은 폴링보다 불필요한 요청을 줄일 수 있었지만, 연결을 반복해서 맺어야 하고 서버 간 연결 관리가 복잡하다는 문제점이 존재한다.
이를 해결하기 위해 채팅 시스템에서는 웹소켓(WebSocket)을 주로 사용한다.</p>
<p>웹 소켓은 서버가 클라이언트에게 비동기적으로 메시지를 전달할 때 널리 사용되는 방식이다.</p>
<p>처음에는 클라이언트가 HTTP 요청을 통해 연결 시작하고 HTTP 핸드셰이크 이후 연결을 웹소켓 연결로 업그레이드하는 방식이다.</p>
<p>한 번 연결되면 해당 연결을 영구적으로 유지하며 클라이언트 ↔ 서버 양방향 통신 가능하고 서버도 비동기적으로 클라이언트의 요청을 기다리지 않고 원하는 시점에 메시지 전송 가능하다.</p>
<p>웹소켓은 일반적으로 방화벽이 있는 환경에서도 잘 동작한다고 한다. 80이나 443처럼 HTTP, HTTPS 프로토콜의 기본 포트를 그대로 사용하기 때문이다.</p>
<pre><code class="language-text">클라이언트 → 서버 : HTTP 핸드셰이크
클라이언트 ← 서버 : ACK

        ↓ WebSocket 연결

클라이언트 ↔ 서버 : 메시지 양방향 전송</code></pre>
<blockquote>
<p>즉, <strong>롱 폴링은 메시지를 받을 때마다 연결을 다시 만들어야 하지만, 웹소켓은 한 번 연결한 뒤 계속 유지하면서 양방향으로 메시지를 주고받는 방식</strong>.</p>
</blockquote>
<h3 id="채팅-시스템에서-웹소켓-사용">채팅 시스템에서 웹소켓 사용</h3>
<p>웹소켓을 사용하면 메시지를 보내는 경우와 받는 경우 모두 동일한 프로토콜 사용 가능하다.
따라서 HTTP로 송신하고 롱 폴링으로 수신하는 것보다 구조가 단순하고 직관적이며, 송신 클라이언트와 수신 클라이언트 모두 채팅 서비스와 웹소켓 연결 유지할 수 있다.</p>
<pre><code>메시지 전송 클라이언트
        ↕ WebSocket
      채팅 서비스
        ↕ WebSocket
메시지 수신 클라이언트</code></pre><p>즉, 웹소켓은 연결을 계속 유지해야 하기 때문에 서버 측에서 많은 연결을 효율적으로 관리하는 것이 중요하다.</p>
<hr>
<h3 id="개략적-설계안">개략적 설계안</h3>
<p>앞에서 웹 소켓의 장점을 말했지만 그렇다고 채팅 시스템의 모든 기능을 웹소켓으로 구현할 필요는 없다.</p>
<p>예를 들어</p>
<ul>
<li>실시간 메시지 송수신 → WebSocket</li>
<li>회원가입, 로그인, 사용자 프로필 등 일반적인 기능 → HTTP</li>
</ul>
<p>같이 기능 별로 나누어서 설계하는 방법이 있다.</p>
<p>따라서 전체 채팅 시스템은 크게 다음 세 부분으로 나누어 설계할 수 있다.</p>
<ol>
<li>무상태(Stateless) 서비스</li>
<li>상태 유지(Stateful) 서비스</li>
<li>제3자 서비스 연동</li>
</ol>
<h4 id="무상태-서비스">무상태 서비스</h4>
<p>무상태 서비스는 로그인, 회원가입, 사용자 프로필 표시 등 일반적인 요청/응답을 처리하는 서비스다.</p>
<p>우리가 일반적인 웹사이트나 앱에서 자주 사용하는 기능들이 대부분 여기에 포함된다고 보면 된다.</p>
<p>무상태 서비스는 <strong>로드밸런서 뒤에 위치</strong>하고, 로드밸런서는 사용자의 요청을 적절한 서비스로 전달하는 역할을 한다.</p>
<p>예를 들어</p>
<ul>
<li>서비스 탐색</li>
<li>인증 서비스</li>
<li>그룹 관리</li>
<li>사용자 프로필</li>
</ul>
<p>등이 무상태 서비스에 해당한다.</p>
<p>이러한 서비스는 하나의 서버에 모든 기능을 넣는 <strong>모놀리식 구조</strong>로 구성할 수도 있고, 기능별로 나누는 <strong>마이크로서비스 구조</strong>로 구성할 수도 있다.</p>
<p>여기서 조금 생소한 것이 <strong>서비스 탐색(Service Discovery)</strong>이다.</p>
<blockquote>
<h4 id="서비스-탐색service-discovery">서비스 탐색(Service Discovery)</h4>
<p>클라이언트가 접속할 <strong>채팅 서버의 위치를 찾아주는 역할</strong>을 하는 서비스.
현재 서버의 부하 등을 고려하여 클라이언트가 접속할 채팅 서버의 DNS 호스트명을 알려준다.</p>
</blockquote>
<hr>
<h4 id="상태-유지-서비스">상태 유지 서비스</h4>
<p>앞에서 말한 무상태 서비스와 달리, 이번 설계에서 대표적인 <strong>상태 유지 서비스는 채팅 서비스</strong>다.</p>
<p>채팅 서비스를 상태 유지 서비스로 사용하는 이유는 앞에서 배운 <strong>웹소켓 연결</strong> 때문이다.</p>
<p>각 클라이언트는 채팅 서버와 <strong>독립적인 웹소켓 연결을 계속 유지</strong>해야 한다.</p>
<pre><code class="language-text">사용자 1 ↔ WebSocket ↔ 채팅 서버
사용자 2 ↔ WebSocket ↔ 채팅 서버</code></pre>
<p>한 번 특정 채팅 서버와 연결된 클라이언트는 보통 해당 서버가 정상적으로 동작하는 동안에는 <strong>다른 채팅 서버로 연결을 변경하지 않는다.</strong></p>
<p>따라서 앞에서 나온 <strong>서비스 탐색 서비스가 채팅 서비스와 협력하여 특정 채팅 서버에 연결이 몰리지 않도록 적절한 서버를 선택</strong>하는 역할도 한다.</p>
<blockquote>
<p>즉, 일반적인 기능은 요청이 들어올 때마다 처리하면 되기 때문에 무상태 서비스로 처리하고,
<strong>채팅은 클라이언트와 서버 사이의 연결 자체를 계속 유지해야 하기 때문에 상태 유지 서비스로 처리하는 것</strong>.</p>
</blockquote>
<hr>
<h4 id="제3자-서비스-연동">제3자 서비스 연동</h4>
<p>채팅 시스템에서 중요한 제3자 서비스 중 하나가 <strong>푸시 알림 서비스</strong>다.</p>
<p>새로운 메시지가 도착했을 때 사용자가 채팅 앱을 실행하고 있지 않더라도 메시지가 왔다는 사실을 알려줘야 하기 때문이다.</p>
<pre><code class="language-text">새 메시지 발생
      ↓
   알림 서버
      ↓
  푸시 알림
      ↓
  사용자 단말</code></pre>
<p>따라서 채팅 시스템은 실시간 채팅 기능뿐만 아니라 <strong>외부 푸시 알림 서비스와의 연동도 필요</strong>하다.</p>
<hr>
<h4 id="규모-확장성">규모 확장성</h4>
<p>트래픽이 많지 않다면 지금까지 설명한 기능들을 <strong>서버 한 대에 모두 구현하는 것도 가능</strong>하다.</p>
<p>실제로 중요한 것은 서버의 개수 자체보다 <strong>한 서버가 동시에 얼마나 많은 연결을 처리할 수 있는가</strong>이다.</p>
<p>예를 들어 동시 접속자가 100만 명이고, 하나의 연결을 유지하는 데 약 10KB의 메모리가 필요하다고 가정하면</p>
<pre><code class="language-text">1,000,000 × 10KB = 약 10GB</code></pre>
<p>정도의 메모리로 연결 자체는 처리할 수 있다.</p>
<p>하지만 그렇다고 실제 대규모 시스템을 <strong>서버 한 대로 설계하는 것은 적절하지 않다.</strong></p>
<p>가장 대표적인 이유가 <strong>SPOF(Single Point Of Failure)</strong> 문제다.</p>
<blockquote>
<h4 id="spof">SPOF</h4>
<p>특정 서버 하나가 장애를 일으켰을 때 전체 시스템이 함께 중단될 수 있는 단일 장애 지점.</p>
</blockquote>
<p>다만 시스템 설계 면접에서는 처음부터 복잡한 구조를 만드는 것보다 <strong>서버 한 대에서 시작한 뒤 트래픽이 증가함에 따라 점차 확장하는 방식</strong>으로 설명하는 것도 괜찮다.</p>
<p>규모 확장성을 고려하여 지금까지 나온 요소들을 하나로 묶으면 아래와 같은 구조가 된다.</p>
<p>사용자 단말은 일반적인 기능을 사용할 때는 <strong>HTTP</strong>, 실시간 메시지를 주고받을 때는 <strong>웹소켓</strong>을 사용한다.</p>
<p>각 서버의 역할은 다음과 같다.</p>
<ul>
<li><strong>채팅 서버</strong> → 클라이언트 사이의 메시지를 중계</li>
<li><strong>접속 상태 서버(Presence Server)</strong> → 사용자의 접속 여부 관리</li>
<li><strong>API 서버</strong> → 로그인, 회원가입, 프로필 변경 등 나머지 기능 처리</li>
<li><strong>알림 서버</strong> → 푸시 알림 전송</li>
<li><strong>키-값 저장소(Key-Value Store)</strong> → 채팅 이력(Chat History) 보관</li>
</ul>
<p>여기서 실시간으로 메시지를 주고받기 위해 클라이언트는 <strong>채팅 서버와 웹소켓 연결을 끊지 않고 유지</strong>한다.</p>
<hr>
<h3 id="저장소">저장소</h3>
<p>이제 서버를 구성했으니 실제 데이터를 어디에 저장할지도 결정해야 한다.</p>
<p>관계형 데이터베이스를 사용할 것인지, NoSQL을 사용할 것인지는 단순히 하나가 더 좋다고 결정할 수 있는 것이 아니라 <strong>데이터의 유형과 읽기/쓰기 패턴</strong>을 살펴봐야 한다.</p>
<p>채팅 시스템에서 다루는 데이터는 크게 두 가지로 나눌 수 있다.</p>
<h4 id="일반적인-데이터">일반적인 데이터</h4>
<ul>
<li>사용자 프로필</li>
<li>설정</li>
<li>친구 목록</li>
</ul>
<p>같은 일반적인 데이터는 안정성을 보장하는 <strong>관계형 데이터베이스</strong>에 보관할 수 있다.</p>
<p>데이터의 가용성과 규모 확장성을 위해 <strong>다중화(Replication)</strong>와 <strong>샤딩(Sharding)</strong>도 사용할 수 있다.</p>
<h4 id="채팅-이력-데이터">채팅 이력 데이터</h4>
<p>두 번째는 채팅 시스템의 핵심 데이터인 <strong>채팅 이력(Chat History)</strong>이다.</p>
<p>채팅 이력에 사용할 저장소를 결정하려면 채팅 데이터가 어떤 특징을 가지는지 살펴봐야 한다.</p>
<ul>
<li>채팅 이력 데이터의 양이 굉장히 많음</li>
<li>대부분의 사용자는 오래된 메시지보다 <strong>최근에 주고받은 메시지를 자주 조회</strong></li>
<li>하지만 검색, 멘션 확인, 특정 메시지로 이동 등의 기능 때문에 <strong>과거 메시지에 대한 무작위 접근(Random Access)</strong>도 필요</li>
<li>1:1 채팅에서는 읽기와 쓰기의 비율이 대략 <strong>1:1</strong></li>
</ul>
<p>이러한 특징을 고려하여 이 설계에서는 채팅 이력을 저장하기 위한 데이터베이스로 <strong>키-값 저장소(Key-Value Store)</strong>를 사용한다.</p>
<p>그 이유는</p>
<ul>
<li><strong>수평적 규모 확장(Horizontal Scaling)이 쉬움</strong></li>
<li>데이터 접근 <strong>지연시간(Latency)이 낮음</strong></li>
<li>데이터가 커질수록 관계형 데이터베이스에서 비용이 커질 수 있는 <strong>무작위 접근 처리에 적합</strong></li>
<li>이미 많은 대규모 채팅 시스템에서 사용된 사례 존재</li>
</ul>
<p>책에서는 페이스북 메신저의 <strong>HBase</strong>, 디스코드의 <strong>Cassandra</strong>를 키-값 계열 저장소를 사용한 사례로 소개한다.</p>
<hr>
<h3 id="데이터-모델">데이터 모델</h3>
<p>키-값 저장소를 사용하기로 했으니 이제 실제 메시지 데이터를 어떤 형태로 저장할 것인지 살펴본다.</p>
<h4 id="11-채팅을-위한-메시지-테이블">1:1 채팅을 위한 메시지 테이블</h4>
<p>1:1 채팅에서는 다음과 같은 형태로 메시지를 저장할 수 있다.</p>
<pre><code class="language-text">message

message_id      bigint
message_from    bigint
message_to      bigint
content         text
created_at      timestamp</code></pre>
<p>여기서 가장 중요한 값은 <strong>message_id</strong>다.</p>
<p><code>message_id</code>를 기본 키(Primary Key)로 사용하며, 메시지의 순서를 판단하는 역할도 한다.</p>
<p>단순히 <code>created_at</code>을 이용해서 메시지 순서를 결정하기는 어렵다.</p>
<p>서로 다른 메시지가 <strong>동일한 시각에 생성될 수도 있기 때문</strong>이다.</p>
<hr>
<h4 id="그룹-채팅을-위한-메시지-테이블">그룹 채팅을 위한 메시지 테이블</h4>
<p>그룹 채팅에서는 특정 그룹, 즉 <strong>채널(Channel)</strong> 단위로 메시지를 관리해야 한다.</p>
<pre><code class="language-text">group message

channel_id      bigint
message_id      bigint
message_to      bigint
content         text
created_at      timestamp</code></pre>
<p>여기서는 <code>(channel_id, message_id)</code>를 <strong>복합 키(Composite Key)</strong>로 사용한다.</p>
<p><code>channel_id</code>는 <strong>파티션 키(Partition Key)</strong>로 사용한다.</p>
<p>그룹 채팅에서 발생하는 대부분의 조회가 특정 채널의 메시지를 대상으로 하기 때문에 같은 채널의 메시지를 묶어서 관리하기 위한 방식이다.</p>
<hr>
<h3 id="메시지-id">메시지 ID</h3>
<p>앞에서 <code>message_id</code>가 메시지의 순서를 결정한다고 했는데, 그렇다면 메시지 ID를 어떻게 만들 것인지도 중요하다.</p>
<p><code>message_id</code>는 다음 두 조건을 만족해야 한다.</p>
<ul>
<li><p><strong>고유한 값(Unique)</strong>이어야 함</p>
</li>
<li><p><strong>시간 순서대로 정렬 가능</strong>해야 함</p>
<ul>
<li>새로운 메시지의 ID가 이전 메시지의 ID보다 큰 값</li>
</ul>
</li>
</ul>
<p>RDBMS라면 <code>auto_increment</code>를 사용할 수 있지만, NoSQL에서는 일반적으로 이러한 기능을 바로 사용할 수 없다.</p>
<p>따라서 몇 가지 방법을 생각해볼 수 있다.</p>
<h4 id="전역적-id-생성기">전역적 ID 생성기</h4>
<p>스노플레이크(Snowflake)와 같은 <strong>전역적 64-bit 순서 번호 생성기</strong>를 사용하는 방법이다.</p>
<p>전체 시스템에서 중복되지 않는 ID를 생성할 수 있지만 별도의 전역 ID 생성 구조가 필요하다.</p>
<h4 id="지역적-순서-번호-생성기">지역적 순서 번호 생성기</h4>
<p>또 다른 방법은 <strong>지역적 순서 번호 생성기(Local Sequence Number Generator)</strong>를 사용하는 것이다.</p>
<p>여기서 지역적이라는 것은 시스템 전체에서 ID의 유일성을 보장하는 것이 아니라</p>
<ul>
<li>같은 1:1 채팅 세션</li>
<li>같은 그룹 채팅 채널</li>
</ul>
<p>안에서만 메시지의 순서와 유일성을 보장하는 방식이다.</p>
<p>채팅에서는 메시지 사이의 순서가 <strong>같은 채널이나 같은 채팅 세션 안에서만 유지되면 충분</strong>하기 때문에 전역적 ID 생성기보다 구현하기 쉬운 방법이 될 수 있다.</p>
<hr>
<h2 id="3단계-상세-설계">3단계 상세 설계</h2>
<p>개략적 설계까지 끝냈으니 이제 몇 가지 중요한 컴포넌트를 조금 더 자세히 살펴본다.</p>
<p>채팅 시스템에서는 크게</p>
<ul>
<li>서비스 탐색(Service Discovery)</li>
<li>메시지 전달 흐름</li>
<li>여러 단말 사이의 메시지 동기화</li>
<li>그룹 채팅에서의 메시지 처리</li>
</ul>
<p>등을 조금 더 자세하게 살펴볼 필요가 있다.</p>
<hr>
<h3 id="서비스-탐색service-discovery-1">서비스 탐색(Service Discovery)</h3>
<p>서비스 탐색의 가장 중요한 역할은 <strong>클라이언트에게 가장 적합한 채팅 서버를 찾아주는 것</strong>이다.</p>
<p>모든 사용자를 하나의 채팅 서버에 연결할 수는 없기 때문에 여러 채팅 서버 중 어떤 서버와 연결할지를 결정해야 한다.</p>
<p>이때 고려할 수 있는 기준은</p>
<ul>
<li>클라이언트의 위치(Geographical Location)</li>
<li>서버의 용량(Capacity)</li>
<li>서버의 현재 부하</li>
</ul>
<p>등이 있다.</p>
<p>책에서는 서비스 탐색을 구현하는 대표적인 오픈소스 솔루션으로 <strong>Apache ZooKeeper</strong>를 사용한다.</p>
<p>ZooKeeper에는 현재 사용 가능한 채팅 서버들을 등록해두고, 사용자가 접속할 때 조건에 맞는 채팅 서버를 선택하는 방식이다.</p>
<h4 id="서비스-탐색-과정">서비스 탐색 과정</h4>
<pre><code class="language-text">사용자 A
   ↓ 1. 로그인
로드밸런서
   ↓ 2
API 서버
   ↓ 3
서비스 탐색(ZooKeeper)
   ↓
최적의 채팅 서버 선택
   ↓
사용자 A ↔ 채팅 서버 2
         WebSocket</code></pre>
<ol>
<li>사용자 A가 시스템에 로그인 요청</li>
<li>로드밸런서가 로그인 요청을 API 서버 중 하나로 전달</li>
<li>API 서버가 사용자 인증을 처리한 뒤 <strong>서비스 탐색을 통해 가장 적절한 채팅 서버 탐색</strong></li>
<li>선택된 채팅 서버 정보를 사용자에게 반환하고, 사용자는 해당 서버와 <strong>웹소켓 연결</strong></li>
</ol>
<p>즉, 서비스 탐색은 단순히 서버를 찾는 것이 아니라 <strong>여러 채팅 서버 중 사용자가 연결할 최적의 서버를 선택해주는 역할</strong>을 한다.</p>
<hr>
<h3 id="메시지-흐름">메시지 흐름</h3>
<p>이제 실제로 사용자가 메시지를 보냈을 때 어떤 과정을 거쳐 상대방에게 전달되는지를 살펴본다.</p>
<p>여기서는</p>
<ul>
<li>1:1 채팅</li>
<li>여러 단말 사이의 메시지 동기화</li>
<li>그룹 채팅</li>
</ul>
<p>순으로 살펴본다.</p>
<hr>
<h3 id="11-채팅-메시지-처리-흐름">1:1 채팅 메시지 처리 흐름</h3>
<p>사용자 A가 사용자 B에게 메시지를 보내는 상황을 생각해보자.</p>
<pre><code class="language-text">사용자 A
   ↓
채팅 서버 1
   ↓
ID 생성기
   ↓
메시지 동기화 큐
   ├→ 키-값 저장소
   │
   ├→ B 접속 중 → 채팅 서버 2 → 사용자 B
   │
   └→ B 미접속 → 푸시 알림 서버</code></pre>
<p>처리 과정은 다음과 같다.</p>
<ol>
<li><p><strong>사용자 A가 채팅 서버 1로 메시지 전송</strong></p>
</li>
<li><p>채팅 서버 1이 <strong>ID 생성기를 통해 메시지 ID 생성</strong></p>
</li>
<li><p>메시지를 <strong>메시지 동기화 큐(Message Sync Queue)</strong>로 전송</p>
</li>
<li><p>메시지를 <strong>키-값 저장소에 저장</strong></p>
</li>
<li><p>사용자 B의 접속 상태에 따라 처리</p>
<ul>
<li><strong>B가 접속 중</strong> → B가 연결되어 있는 채팅 서버로 메시지 전달</li>
<li><strong>B가 미접속</strong> → 푸시 알림 서버로 알림 전송</li>
</ul>
</li>
<li><p>B가 접속 중이라면 채팅 서버가 기존 <strong>웹소켓 연결을 이용해 메시지 전달</strong></p>
</li>
</ol>
<p>즉, 메시지를 바로 상대방에게만 보내는 것이 아니라 <strong>저장과 전달을 함께 처리</strong>하고, 상대방의 접속 여부에 따라 전달 방법이 달라지는 구조다.</p>
<hr>
<h3 id="여러-단말-사이의-메시지-동기화">여러 단말 사이의 메시지 동기화</h3>
<p>요즘에는 하나의 계정을 스마트폰, 노트북 등 <strong>여러 단말에서 동시에 사용하는 경우가 많다.</strong></p>
<p>예를 들어 사용자 A가</p>
<ul>
<li>스마트폰</li>
<li>노트북</li>
</ul>
<p>두 개의 단말에서 동시에 로그인했다고 해보자.</p>
<p>각 단말은 채팅 서버와 <strong>각각 별도의 웹소켓 연결</strong>을 유지한다.</p>
<p>그리고 각 단말은 <code>cur_max_message_id</code>라는 값을 관리한다.</p>
<blockquote>
<h4 id="cur_max_message_id">cur_max_message_id</h4>
<p>해당 단말에서 지금까지 확인한 <strong>가장 최신 메시지의 ID</strong></p>
</blockquote>
<p>예를 들어</p>
<pre><code class="language-text">스마트폰
cur_max_message_id = 653

노트북
cur_max_message_id = 842</code></pre>
<p>처럼 단말마다 서로 다른 값을 가질 수 있다.</p>
<p>다음 두 조건을 만족하는 메시지를 새로운 메시지라고 판단한다.</p>
<ul>
<li>메시지의 수신자 ID가 현재 로그인한 사용자의 ID와 같음</li>
<li><code>message_id &gt; cur_max_message_id</code></li>
</ul>
<p>즉, 각 단말은 <strong>자신이 마지막으로 확인한 메시지 ID 이후의 메시지만 가져오면 되기 때문에</strong> 여러 단말 사이의 메시지 동기화를 비교적 간단하게 구현할 수 있다.</p>
<hr>
<h3 id="소규모-그룹-채팅에서의-메시지-흐름">소규모 그룹 채팅에서의 메시지 흐름</h3>
<p>1:1 채팅보다 그룹 채팅에서는 메시지를 여러 사용자에게 전달해야 하기 때문에 조금 더 복잡하다.</p>
<p>예를 들어 사용자 A, B, C가 같은 그룹 채팅방에 있다고 해보자.</p>
<p>사용자 A가 메시지를 보내면 해당 메시지를</p>
<pre><code class="language-text">사용자 A
   ↓
채팅 서버
   ├→ 사용자 B의 메시지 동기화 큐
   └→ 사용자 C의 메시지 동기화 큐</code></pre>
<p>처럼 <strong>각 수신자의 메시지 동기화 큐에 복사</strong>한다.</p>
<p>즉, 각 사용자에게 <strong>자신만의 메시지 수신함을 하나씩 만들어주는 방식</strong>으로 생각할 수 있다.</p>
<p>이 방식의 장점은</p>
<ul>
<li>사용자는 자신의 큐만 확인하면 되기 때문에 <strong>메시지 동기화 구조가 단순</strong></li>
<li>그룹의 크기가 작다면 동일한 메시지를 여러 큐에 복사하는 비용도 크지 않음</li>
</ul>
<p>책에서는 이러한 방식의 사례로 위챗(WeChat)을 언급하며, 책 기준으로 그룹 크기를 500명으로 제한하고 있다고 설명한다.</p>
<p>하지만 그룹의 사용자가 매우 많아진다면 문제가 발생한다.</p>
<p>예를 들어 수만 명이 있는 그룹에서 메시지 하나가 발생할 때마다 <strong>모든 사용자의 큐에 같은 메시지를 복사하는 것은 비용이 너무 커질 수 있기 때문</strong>이다.</p>
<p>또한 수신자 입장에서 보면 하나의 사용자에게 메시지를 보내는 사람이 여러 명일 수 있기 때문에,</p>
<pre><code class="language-text">사용자 A → 채팅 서버 1 ─┐
                         ├→ 사용자 C의 메시지 동기화 큐 → 사용자 C
사용자 B → 채팅 서버 2 ─┘</code></pre>
<p>처럼 <strong>하나의 메시지 동기화 큐가 여러 사용자로부터 들어오는 메시지를 받을 수 있어야 한다.</strong></p>
<hr>
<h3 id="접속상태-표시">접속상태 표시</h3>
<p>채팅 애플리케이션에서는 사용자가 현재 <strong>접속 중인지 아닌지를 표시하는 기능</strong>도 중요하다.</p>
<p>이를 위해 앞에서 살펴본 <strong>접속상태 서버(Presence Server)</strong>를 사용한다.</p>
<p>접속상태 서버는 클라이언트와 웹소켓으로 연결되어 있으며 사용자의 상태가 변경될 때마다 상태 정보를 관리한다.</p>
<hr>
<h4 id="사용자-로그인">사용자 로그인</h4>
<p>사용자가 로그인하여 클라이언트와 실시간 서비스 사이에 웹소켓 연결이 만들어지면 접속상태 서버는 해당 사용자의 상태를 <strong>online</strong>으로 변경한다.</p>
<p>이때 사용자의 마지막 활동 시점을 나타내는 <code>last_active_at</code>도 함께 키-값 저장소에 저장한다.</p>
<pre><code class="language-text">사용자 A
   ↕ WebSocket
접속상태 서버
   ↓
키-값 저장소

사용자 A {
    status: online,
    last_active_at: timestamp
}</code></pre>
<p>즉, 로그인 이후 웹소켓 연결이 만들어지면 해당 사용자를 <strong>접속 중인 사용자로 표시</strong>하는 방식이다.</p>
<hr>
<h4 id="로그아웃">로그아웃</h4>
<p>사용자가 정상적으로 로그아웃하면 API 서버를 통해 접속상태 서버에 로그아웃 사실을 전달한다.</p>
<p>이후 키-값 저장소에 저장되어 있던 사용자의 상태를</p>
<pre><code class="language-text">online → offline</code></pre>
<p>으로 변경한다.</p>
<pre><code class="language-text">사용자 A
   ↓ 로그아웃
API 서버
   ↓
접속상태 서버
   ↓
키-값 저장소

사용자 A { status: offline }</code></pre>
<hr>
<h4 id="접속-장애">접속 장애</h4>
<p>문제는 사용자가 항상 정상적으로 로그아웃하는 것은 아니라는 점이다.</p>
<p>인터넷 연결이 끊기면 클라이언트와 서버 사이의 <strong>웹소켓 연결도 끊어질 수 있다.</strong></p>
<p>그렇다고 웹소켓 연결이 잠깐 끊어질 때마다 바로 사용자를 <code>offline</code>으로 변경하는 것은 좋은 방법이 아니다.</p>
<p>예를 들어 지하철이나 터널을 지나면서 잠깐 인터넷이 끊겼다가 다시 연결되는 경우도 있기 때문이다.</p>
<p>그래서 <strong>박동(Heartbeat)</strong>을 사용한다.</p>
<p>온라인 상태의 클라이언트는 일정한 주기로 접속상태 서버에 박동 이벤트를 전송한다.</p>
<pre><code class="language-text">클라이언트 → 서버 : heartbeat
       5초
클라이언트 → 서버 : heartbeat
       5초
클라이언트 → 서버 : heartbeat</code></pre>
<p>서버는 마지막 박동 이벤트를 받은 뒤 일정 시간 <code>x</code> 안에 새로운 박동 이벤트가 들어오면 사용자를 계속 <strong>online 상태로 유지</strong>한다.</p>
<p>반대로 일정 시간 동안 아무런 박동 이벤트를 받지 못하면 사용자를 <strong>offline 상태로 변경</strong>한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">5초마다 Heartbeat 전송
        ↓
서버가 계속 수신
        ↓
Online 유지

30초 동안 Heartbeat 없음
        ↓
Offline 변경</code></pre>
<blockquote>
<p>즉, 웹소켓 연결이 잠깐 끊겼다고 바로 오프라인으로 판단하는 것이 아니라 <strong>일정 시간 동안 Heartbeat가 들어오는지 확인한 후 접속 상태를 변경하는 방식</strong>이다.</p>
</blockquote>
<hr>
<h3 id="상태-정보의-전송">상태 정보의 전송</h3>
<p>그렇다면 사용자 A의 상태가 <code>online → offline</code>처럼 변경되었을 때 <strong>A의 친구들은 이 사실을 어떻게 알 수 있을까?</strong></p>
<p>여기서는 <strong>발행-구독(Publish-Subscribe) 모델</strong>을 사용할 수 있다.</p>
<p>예를 들어 사용자 A가 B, C, D와 친구라고 한다면 각각의 관계마다 하나의 채널을 만든다.</p>
<pre><code class="language-text">사용자 A
   ├→ 채널 A-B ← 사용자 B 구독
   ├→ 채널 A-C ← 사용자 C 구독
   └→ 채널 A-D ← 사용자 D 구독</code></pre>
<p>사용자 A의 접속 상태가 변경되면 해당 상태 변화를 각각의 채널에 <strong>발행(Publish)</strong>한다.</p>
<p>그리고 B, C, D는 각 채널을 <strong>구독(Subscribe)</strong>하고 있기 때문에 A의 상태 변경을 실시간으로 전달받을 수 있다.</p>
<p>클라이언트와 서버 사이의 상태 정보 전달에는 앞에서 사용한 <strong>웹소켓을 그대로 사용</strong>한다.</p>
<p>이 방식은 친구나 그룹의 규모가 작을 때는 효과적이지만, 사용자가 많아질수록 문제가 발생한다.</p>
<p>예를 들어 한 그룹에 100,000명의 사용자가 있다면 상태가 한 번 변경될 때마다 <strong>100,000개의 이벤트가 발생할 수 있기 때문</strong>이다.</p>
<p>따라서 대규모 그룹에서는</p>
<ul>
<li>사용자가 그룹 채팅방에 들어왔을 때만 상태 정보 조회</li>
<li>사용자가 친구 목록을 확인할 때 필요한 상태 정보만 갱신</li>
</ul>
<p>등의 방식으로 이벤트 수를 줄일 수 있다.</p>
<hr>
<h2 id="4단계-마무리">4단계 마무리</h2>
<p>지금까지</p>
<ul>
<li>1:1 채팅</li>
<li>그룹 채팅</li>
<li>웹소켓을 이용한 실시간 통신</li>
<li>채팅 서버</li>
<li>접속상태 서버</li>
<li>푸시 알림 서버</li>
<li>키-값 저장소</li>
<li>API 서버</li>
</ul>
<p>등을 이용하여 채팅 시스템을 설계했다.</p>
<p>시간이 더 있다면 다음과 같은 부분도 추가로 생각해볼 수 있다.</p>
<h3 id="미디어-지원">미디어 지원</h3>
<p>사진이나 동영상은 텍스트 메시지보다 파일 크기가 훨씬 크다.</p>
<p>따라서</p>
<ul>
<li>이미지/동영상 압축</li>
<li>클라우드 저장소</li>
<li>썸네일(Thumbnail) 생성</li>
</ul>
<p>등을 추가로 고려할 수 있다.</p>
<h3 id="종단-간-암호화">종단 간 암호화</h3>
<p>왓츠앱과 같은 채팅 서비스에서는 <strong>종단 간 암호화(End-to-End Encryption)</strong>를 지원한다.</p>
<p>메시지를 보내는 사용자와 받는 사용자만 메시지 내용을 확인할 수 있도록 하는 방식이다.</p>
<h3 id="캐시">캐시</h3>
<p>클라이언트가 이미 읽은 메시지를 로컬에 캐시하면 서버에서 같은 데이터를 반복해서 가져올 필요가 없기 때문에 <strong>서버와 주고받는 데이터 양을 줄일 수 있다.</strong></p>
<h3 id="로딩-속도-개선">로딩 속도 개선</h3>
<p>사용자 데이터나 채널 데이터를 지역적으로 분산해 사용자와 가까운 곳에서 제공하면 <strong>애플리케이션의 로딩 속도를 개선</strong>할 수 있다.</p>
<h3 id="오류-처리">오류 처리</h3>
<p>대규모 시스템에서는 서버나 네트워크에 장애가 발생할 수 있기 때문에 이에 대한 처리도 필요하다.</p>
<ul>
<li><p><strong>채팅 서버 장애</strong></p>
<ul>
<li>현재 연결된 채팅 서버가 죽으면 서비스 탐색 기능을 통해 새로운 서버를 찾아 다시 연결</li>
</ul>
</li>
<li><p><strong>메시지 재전송</strong></p>
<ul>
<li>메시지 전달에 실패한 경우 <code>retry</code>나 <code>queue</code>를 이용하여 안정적으로 다시 전송</li>
</ul>
</li>
</ul>
<p>즉, 기본적인 채팅 기능을 설계한 이후에는 <strong>보안, 성능, 장애 복구와 같은 부분까지 고려하면서 시스템을 확장할 수 있다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AEReport] 당신은 AI를 얼마나 잘 활용하고 있는가]]></title>
            <link>https://velog.io/@jung_ji_in02/AEReport-%EB%8B%B9%EC%8B%A0%EC%9D%80-AI%EB%A5%BC-%EC%96%BC%EB%A7%88%EB%82%98-%EC%9E%98-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B3%A0-%EC%9E%88%EB%8A%94%EA%B0%80</link>
            <guid>https://velog.io/@jung_ji_in02/AEReport-%EB%8B%B9%EC%8B%A0%EC%9D%80-AI%EB%A5%BC-%EC%96%BC%EB%A7%88%EB%82%98-%EC%9E%98-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B3%A0-%EC%9E%88%EB%8A%94%EA%B0%80</guid>
            <pubDate>Thu, 13 Aug 2026 06:23:16 GMT</pubDate>
            <description><![CDATA[<h1 id="ai-fluency-index-나는-ai를-얼마나-잘-활용하고-있을까">[AI Fluency Index] 나는 AI를 얼마나 잘 활용하고 있을까?!</h1>
<p>요즘은 공부부터 개발, 문서 작성, 프로젝트 관리까지 생각보다 많은 일을 AI와 함께하고 있다.</p>
<p>그러다 문득 이런 생각이 들었다.</p>
<blockquote>
<p><strong>나는 AI를 많이 사용하고 있는 걸까, 아니면 잘 사용하고 있는 걸까?</strong></p>
</blockquote>
<p>AI를 사용하는 빈도가 높다고 해서 반드시 AI를 잘 활용한다고 볼 수는 없다. 원하는 답을 한 번에 받아내는 것도 중요하지만, 내가 필요한 것을 명확하게 설명하고, 결과를 검증하고, 부족한 부분을 다시 질문하는 능력 역시 AI 활용 능력의 일부이기 때문이다.</p>
<p>마침 Anthropic에서 <strong>「Anthropic Education Report: The AI Fluency Index」</strong>라는 흥미로운 보고서를 공개했다.</p>
<p>이 보고서는 사람들이 Claude와 실제로 어떻게 대화하는지를 분석하여, <strong>AI를 효과적으로 활용하는 사람들에게 어떤 행동이 나타나는지</strong> 살펴본 연구다.</p>
<p>보고서를 읽는 것에서 끝내지 않고, Anthropic에서 제공하는 AI Fluency 자기진단 프롬프트를 이용해 <strong>내가 실제로 Claude를 사용하는 방식도 직접 분석해봤다.</strong></p>
<hr>
<h2 id="1-ai-fluency란-무엇인가">1. AI Fluency란 무엇인가?</h2>
<p>Anthropic은 AI 활용 능력을 단순히 &#39;프롬프트를 잘 작성하는 능력&#39;으로 바라보지 않는다.</p>
<p>보고서에서는 Rick Dakan과 Joseph Feller가 Anthropic과 함께 개발한 <strong>4D AI Fluency Framework</strong>를 기준으로 AI와 협업할 때 나타나는 행동을 분석한다.</p>
<p>4D는 다음 네 가지 영역으로 구성된다.</p>
<h3 id="delegation">Delegation</h3>
<p><strong>AI에게 어떤 일을 맡길지 판단하는 능력</strong></p>
<p>단순히 작업을 요청하는 것이 아니라,</p>
<ul>
<li>내가 해결하려는 문제가 무엇인지</li>
<li>AI가 어떤 부분을 맡아야 하는지</li>
<li>어떤 접근 방법을 사용하는 것이 좋은지</li>
</ul>
<p>등을 판단하는 영역이다.</p>
<h3 id="description">Description</h3>
<p><strong>AI에게 필요한 내용을 명확하게 전달하는 능력</strong></p>
<p>예를 들어,</p>
<ul>
<li>결과물을 누가 사용할 것인지</li>
<li>어떤 형식으로 만들어야 하는지</li>
<li>어떤 말투와 스타일을 원하는지</li>
<li>참고할 만한 예시는 무엇인지</li>
</ul>
<p>등을 AI에게 설명하는 능력이다.</p>
<h3 id="discernment">Discernment</h3>
<p><strong>AI가 만들어낸 결과를 평가하는 능력</strong></p>
<p>AI가 답변했다고 해서 그대로 받아들이는 것이 아니라,</p>
<ul>
<li>사실이 맞는지</li>
<li>논리에 문제가 없는지</li>
<li>빠진 맥락은 없는지</li>
</ul>
<p>확인하는 행동이 여기에 해당한다.</p>
<h3 id="diligence">Diligence</h3>
<p><strong>AI를 책임감 있게 활용하는 능력</strong></p>
<p>AI가 만들어낸 결과물을 어디까지 사용할지, AI의 역할을 어떻게 공개할지, 결과물이 다른 사람에게 어떤 영향을 줄지 등을 고려하는 영역이다.</p>
<p>4D Framework에는 총 24개의 행동 지표가 존재하지만, Anthropic의 연구에서는 그중 <strong>대화 내용에서 직접 확인할 수 있는 11개 행동</strong>만을 분석했다. 책임 있는 AI 활용과 같이 실제 행동이 대화 밖에서 일어나는 일부 지표는 분석하기 어렵기 때문이다.</p>
<hr>
<h1 id="2-anthropic은-실제-claude-사용자들을-어떻게-분석했을까">2. Anthropic은 실제 Claude 사용자들을 어떻게 분석했을까?</h1>
<p>Anthropic은 Claude.ai에서 이루어진 <strong>9,830개의 익명화된 대화</strong>를 분석했다.</p>
<p>단순한 인사나 한두 단어의 대화가 아니라 Claude와 여러 차례 메시지를 주고받은 대화를 대상으로, 각 대화에서 11개의 AI Fluency 행동이 나타나는지 확인했다.</p>
<p>이 연구에서 개인적으로 가장 흥미로웠던 결과는 두 가지였다.</p>
<hr>
<h2 id="3-가장-중요한-행동은-좋은-첫-질문이-아니라-대화를-이어가는-것이었다">3. 가장 중요한 행동은 &#39;좋은 첫 질문&#39;이 아니라 대화를 이어가는 것이었다</h2>
<p>연구 대상 대화의 <strong>85.7%에서 iteration and refinement, 즉 반복과 개선 행동이 나타났다.</strong></p>
<p>여기서 반복과 개선은 AI의 첫 번째 답변을 그대로 받아들이지 않고,</p>
<blockquote>
<p>&quot;이 부분은 조금 이상한데?&quot;
&quot;다시 설명해줘.&quot;
&quot;이 조건을 추가해서 수정해줘.&quot;
&quot;방금 답변보다 조금 더 간단하게 정리해줘.&quot;</p>
</blockquote>
<p>와 같이 이전 답변을 기반으로 대화를 계속 발전시키는 행동을 의미한다.</p>
<p>더 흥미로운 것은 반복적인 대화를 하는 사용자에게서 다른 AI 활용 행동 역시 훨씬 많이 나타났다는 점이다.</p>
<p>반복·개선이 나타난 대화에서는 다른 AI Fluency 행동이 평균 <strong>2.67개 추가로 나타났고</strong>, 그렇지 않은 대화의 평균은 1.33개였다.</p>
<p>또한 반복적인 대화를 하는 사용자는 Claude의 추론에 의문을 제기할 가능성이 <strong>5.6배</strong>, 빠진 맥락을 찾아낼 가능성이 <strong>4배</strong> 높았다.</p>
<p>이 부분을 보면서 내가 가지고 있던 &#39;AI를 잘 쓰는 사람&#39;에 대한 생각이 조금 바뀌었다.</p>
<p>예전에는 좋은 프롬프트를 한 번 작성해서 원하는 답을 바로 얻는 것이 AI 활용 능력이라고 생각했다.</p>
<p>하지만 실제로는 <strong>첫 질문을 완벽하게 작성하는 것보다, 나온 결과를 보고 다시 질문할 수 있는 능력이 더 중요할 수도 있다.</strong></p>
<p>AI와의 대화 역시 결국 하나의 협업 과정에 가깝다는 생각이 들었다.</p>
<hr>
<h1 id="4-결과물이-그럴듯할수록-오히려-검증을-덜-한다">4. 결과물이 그럴듯할수록 오히려 검증을 덜 한다</h1>
<p>두 번째 결과는 조금 더 인상 깊었다.</p>
<p>전체 대화 중 <strong>12.3%는 코드, 문서, 앱, 인터랙티브 도구와 같은 결과물(Artifact)을 만드는 대화</strong>였다.</p>
<p>이러한 대화에서는 사용자들이 AI에게 일을 훨씬 구체적으로 지시했다.</p>
<p>일반 대화와 비교했을 때,</p>
<ul>
<li>목표 명확화: <strong>+14.7%p</strong></li>
<li>형식 지정: <strong>+14.5%p</strong></li>
<li>예시 제공: <strong>+13.4%p</strong></li>
<li>반복·개선: <strong>+9.7%p</strong></li>
</ul>
<p>가 증가했다.</p>
<p>즉, 실제 결과물을 만들어야 하는 상황에서는 사람들이 AI에게 훨씬 많은 정보를 제공하고 있었다.</p>
<p>그런데 반대편에서는 흥미로운 현상이 나타났다.</p>
<p>Artifact를 만드는 대화에서는 오히려</p>
<ul>
<li>빠진 맥락 확인: <strong>-5.2%p</strong></li>
<li>사실 확인: <strong>-3.7%p</strong></li>
<li>AI 추론 검증: <strong>-3.1%p</strong></li>
</ul>
<p>로 감소했다.</p>
<p>처음에는 조금 의외였다.</p>
<p>중요한 결과물을 만들수록 더 꼼꼼하게 검증할 것이라고 생각했기 때문이다.</p>
<p>Anthropic은 이에 대해 AI가 만들어낸 결과물이 <strong>완성된 것처럼 그럴듯하게 보일수록 사용자가 그것을 다시 검증하지 않을 가능성</strong>을 하나의 설명으로 제시한다.</p>
<p>물론 이것만이 원인이라고 단정할 수는 없다.</p>
<p>사용자가 생성된 코드를 직접 실행하거나, 앱을 테스트하거나, 문서를 다른 사람에게 검토받는 것처럼 <strong>대화 밖에서 검증했을 가능성</strong>도 있기 때문이다. 연구 역시 이 부분을 한계로 명시하고 있다.</p>
<p>그래도 이 결과를 보며 한 가지는 확실히 생각하게 됐다.</p>
<blockquote>
<p><strong>AI 결과물이 허술해 보일 때보다, 완벽해 보일 때 오히려 한 번 더 확인해야 한다.</strong></p>
</blockquote>
<hr>
<h1 id="5-그렇다면-나는-ai를-어떻게-사용하고-있을까">5. 그렇다면 나는 AI를 어떻게 사용하고 있을까?</h1>
<p><img src="https://velog.velcdn.com/images/jung_ji_in02/post/66a8d15b-ffe2-4242-b785-71f9b03314d3/image.png" alt=""></p>
<p>보고서와 함께 제공되는 자기진단 프롬프트를 Claude에서 직접 실행해봤다.</p>
<p>Anthropic 역시 이 결과를 정식 평가가 아닌 <strong>자신의 AI 활용 패턴을 살펴보기 위한 탐색적 진단</strong>으로 활용할 것을 권장한다.</p>
<p>Claude가 최근 대화를 분석한 결과, 나에게 가장 뚜렷하게 나타난 특징은 <strong>AI가 알 수 없는 맥락을 먼저 전달하는 것</strong>이었다.</p>
<p>평가 결과는 다음과 같았다.</p>
<h3 id="delegation-1">Delegation</h3>
<table>
<thead>
<tr>
<th>지표</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>Clarifies goals</td>
<td>✅ Demonstrated</td>
</tr>
<tr>
<td>Consults on approach</td>
<td>✅ Demonstrated</td>
</tr>
</tbody></table>
<p>나는 단순히 작업을 요청하기보다 무엇을 하려는지 설명하고, 필요할 경우 어떤 방향으로 진행하는 것이 좋은지도 먼저 질문하는 편이었다.</p>
<p>실제 대화에서도 다음과 같은 표현이 발견됐다.</p>
<blockquote>
<p>&quot;운영체제 공부를 시작하려고함. 지금 현재 파일들은 순서대로 들어가있지 않음&quot;</p>
</blockquote>
<p>그리고 바로 작업을 요청하기보다,</p>
<blockquote>
<p>&quot;어디서 부터 시작하면 좋을 지 판단해줘&quot;</p>
</blockquote>
<p>처럼 AI에게 접근 방법을 먼저 판단하도록 요청하는 행동도 확인됐다.</p>
<p>이 부분은 내가 AI를 단순한 &#39;답변 생성기&#39;라기보다 <strong>어떤 방식으로 문제를 해결할지 함께 판단하는 도구</strong>로 사용하고 있다는 의미로 받아들였다.</p>
<hr>
<h2 id="description-1">Description</h2>
<table>
<thead>
<tr>
<th>지표</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>Defines audience</td>
<td>❌ Not observed</td>
</tr>
<tr>
<td>Specifies format</td>
<td>✅ Demonstrated</td>
</tr>
<tr>
<td>Communicates tone</td>
<td>❌ Not observed</td>
</tr>
<tr>
<td>Builds iteratively</td>
<td>✅ Demonstrated</td>
</tr>
<tr>
<td>Provides examples</td>
<td>❌ Not observed</td>
</tr>
<tr>
<td>Sets interaction</td>
<td>❌ Not observed</td>
</tr>
</tbody></table>
<p>Description 영역에서는 장단점이 비교적 명확하게 나타났다.</p>
<p>형식에 대한 요구는 꽤 구체적인 편이었다.</p>
<p>예를 들어,</p>
<blockquote>
<p>&quot;다음 기획안에 대해서 내가 구현할 api를 점검해주고, 표로 작성해서 기능명세서 뽑아줘.&quot;</p>
</blockquote>
<p>처럼 원하는 결과물이 무엇이고 어떤 형식으로 정리되어야 하는지 지정하고 있었다.</p>
<p>또한 한 번의 대화로 작업을 끝내기보다는 추가 자료를 전달하거나 이전 결과물을 기반으로 계속 작업하는 패턴도 나타났다.</p>
<blockquote>
<p>&quot;PDF의 페이지수 초과로 2번 PDF만 보냄. 아직 8개의 파일 남아있음&quot;</p>
</blockquote>
<p>반면 내가 상대적으로 잘 사용하지 않고 있던 것도 명확했다.</p>
<p><strong>이 결과물을 누가 읽는지, 어떤 말투로 작성해야 하는지, AI가 나와 어떤 방식으로 상호작용해야 하는지</strong>에 대한 지시는 거의 없었다.</p>
<p>특히 이 부분이 의외였다.</p>
<p>나는 평소 AI에게 꽤 구체적으로 요청한다고 생각했다.</p>
<p>하지만 다시 생각해보니 대부분의 구체성은</p>
<blockquote>
<p>무엇을 만들 것인가</p>
</blockquote>
<p>에 집중되어 있었고,</p>
<blockquote>
<p>누가 볼 것인가
어떤 느낌이어야 하는가
AI가 나에게 어떻게 대응해야 하는가</p>
</blockquote>
<p>까지 정의하는 경우는 많지 않았다.</p>
<hr>
<h1 id="6-내가-가장-잘하고-있던-것은-맥락-전달이었다">6. 내가 가장 잘하고 있던 것은 &#39;맥락 전달&#39;이었다</h1>
<p>Discernment 영역에서는 다음과 같은 결과가 나타났다.</p>
<table>
<thead>
<tr>
<th>지표</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>Checks facts</td>
<td>✅ Demonstrated</td>
</tr>
<tr>
<td>Notices reasoning</td>
<td>❌ Not observed</td>
</tr>
<tr>
<td>Recognizes context</td>
<td>✅ Demonstrated</td>
</tr>
</tbody></table>
<p>Claude가 나의 가장 강한 행동으로 평가한 것은 <strong>Recognizes context</strong>, 즉 AI가 스스로 알 수 없는 상황을 먼저 전달하는 습관이었다.</p>
<p>대표적으로 다음과 같은 요청이 있었다.</p>
<blockquote>
<p>&quot;내 github 주소를 확인하여 내 코드를 스캔 후에 API 명세서를 노션의 정지인의 개인 페이지 - API 명세서 페이지에 작성.&quot;</p>
</blockquote>
<p>나는 AI에게 작업을 요청할 때 저장소, 문서 위치, 작업 대상 등을 꽤 구체적으로 전달하고 있었다.</p>
<p>개발 작업을 하다 보면 AI가 현재 프로젝트 구조나 내가 이전에 어떤 결정을 했는지 알지 못하는 경우가 많다.</p>
<p>그래서 자연스럽게</p>
<ul>
<li>어떤 프로젝트인지</li>
<li>어느 저장소인지</li>
<li>어느 페이지에 작성해야 하는지</li>
<li>이전에 무엇을 했는지</li>
</ul>
<p>를 함께 전달하는 습관이 생긴 것 같다.</p>
<p>이 결과는 개인적으로 꽤 만족스러웠다.</p>
<p>AI가 부족한 정보를 추측하도록 두기보다, <strong>내가 알고 있는 맥락을 최대한 AI에게 넘겨주는 것이 협업의 시작</strong>이라고 생각하기 때문이다.</p>
<hr>
<h1 id="7-하지만-검증은-생각보다-부족했다">7. 하지만 &#39;검증&#39;은 생각보다 부족했다</h1>
<p>반대로 가장 생각할 거리가 많았던 부분은 <strong>Notices reasoning</strong>이었다.</p>
<p>사실 확인은 어느 정도 하고 있었다.</p>
<p>예를 들어 AI가 수정한 결과가 실제로 반영되지 않았을 때,</p>
<blockquote>
<p>&quot;아직 그대로야&quot;</p>
</blockquote>
<p>라고 문제를 다시 확인하는 행동은 나타났다.</p>
<p>하지만 AI가 어떤 논리를 통해 결론을 내렸는지 확인하거나,</p>
<blockquote>
<p>&quot;그 결론이 왜 나오는 거지?&quot;
&quot;방금 설명의 전제와 결론이 맞지 않는 것 같은데?&quot;
&quot;내 가정이 틀렸다면 반박해줘.&quot;</p>
</blockquote>
<p>처럼 <strong>추론 과정 자체를 검토하는 행동은 발견되지 않았다.</strong></p>
<p>여기서 보고서의 두 번째 연구 결과가 다시 떠올랐다.</p>
<p>나는 AI에게 API 명세서, 문서, 코드 등 꽤 많은 Artifact 생성을 요청한다.</p>
<p>그런데 바로 이런 상황에서 사용자들이 AI 결과를 덜 검증하는 경향이 나타났다는 것이다.</p>
<p>생각해보면 나 역시 AI가 원하는 형태의 코드나 문서를 만들어주면,</p>
<p><strong>&quot;잘 만들어졌는가?&quot;</strong></p>
<p>를 먼저 봤지,</p>
<p><strong>&quot;이 결과가 어떤 근거와 판단을 통해 만들어졌는가?&quot;</strong></p>
<p>까지 확인하는 경우는 많지 않았던 것 같다.</p>
<hr>
<h1 id="8-ai를-잘-활용한다는-것은-좋은-명령을-내리는-것만이-아니었다">8. AI를 잘 활용한다는 것은 &#39;좋은 명령을 내리는 것&#39;만이 아니었다</h1>
<p>이번 보고서를 읽고 나에게 직접 적용해보면서 가장 크게 바뀐 생각은 이것이다.</p>
<p>기존에는 AI 활용 능력을 주로 <strong>프롬프트 작성 능력</strong>이라고 생각했다.</p>
<p>즉,</p>
<blockquote>
<p>내가 원하는 것을 얼마나 구체적으로 설명할 수 있는가?</p>
</blockquote>
<p>가 중요하다고 생각했다.</p>
<p>물론 이것도 중요하다.</p>
<p>하지만 AI Fluency Framework에서 말하는 활용 능력은 여기서 한 단계 더 나아간다.</p>
<p>내가 정리한 AI 활용 과정은 다음과 같다.</p>
<p><strong>목표 설정</strong></p>
<p>→ 무엇을 해결하려는지 설명</p>
<p><strong>맥락 전달</strong></p>
<p>→ AI가 모르는 정보를 제공</p>
<p><strong>작업 방식 결정</strong></p>
<p>→ AI와 접근 방법 논의</p>
<p><strong>결과 생성</strong></p>
<p>→ 원하는 형식과 조건 지정</p>
<p><strong>반복 개선</strong></p>
<p>→ 결과를 기반으로 추가 질문</p>
<p><strong>검증</strong></p>
<p>→ 사실·맥락·논리를 다시 확인</p>
<p>결국 AI를 잘 활용한다는 것은 <strong>프롬프트 한 번을 잘 작성하는 능력이 아니라 이 전체 과정을 잘 운영하는 능력</strong>에 더 가까웠다.</p>
<hr>
<h1 id="9-앞으로-바꾸고-싶은-나의-ai-사용-습관">9. 앞으로 바꾸고 싶은 나의 AI 사용 습관</h1>
<p>Anthropic은 보고서 마지막에서 AI 활용 능력을 높이기 위한 세 가지 행동을 제안한다.</p>
<p>첫 번째는 <strong>대화를 계속 이어가는 것</strong>이다.</p>
<p>첫 번째 답변을 완성본으로 생각하지 않고 출발점으로 보고, 후속 질문과 반박을 통해 결과를 개선하는 것이다.</p>
<p>두 번째는 <strong>완성도가 높아 보이는 결과물을 오히려 의심하는 것</strong>이다.</p>
<p>결과가 잘 만들어졌다면 그 순간,</p>
<ul>
<li>이 내용이 사실인가?</li>
<li>빠진 내용은 없는가?</li>
<li>이 결론의 근거는 무엇인가?</li>
<li>다른 방법은 없는가?</li>
</ul>
<p>를 한 번 더 묻는 것이다.</p>
<p>세 번째는 <strong>AI와의 상호작용 방법을 먼저 정의하는 것</strong>이다.</p>
<p>Anthropic의 분석에서는 사용자가 AI에게 어떻게 상호작용하기를 원하는지 직접 지정한 대화가 약 <strong>30%</strong>에 불과했다.</p>
<p>나 역시 이 부분은 거의 사용하고 있지 않았다.</p>
<p>그래서 앞으로는 중요한 작업을 시작할 때 다음과 같은 지시를 조금씩 추가해보려고 한다.</p>
<blockquote>
<p>내가 잘못된 가정을 하고 있다면 그대로 따르지 말고 먼저 지적해줘.</p>
</blockquote>
<p>또는,</p>
<blockquote>
<p>답변을 만들기 전에 내가 놓치고 있는 조건이 있는지 먼저 확인해줘.</p>
</blockquote>
<p>그리고 결과물을 받은 뒤에는,</p>
<blockquote>
<p>이 결과에서 사실과 추론을 구분하고, 검증이 필요한 부분을 알려줘.</p>
</blockquote>
<p>와 같은 질문도 습관적으로 사용해볼 생각이다.</p>
<hr>
<h1 id="10-마무리">10. 마무리</h1>
<p>이번 AI Fluency Index를 통해 확인한 내 강점은 명확했다.</p>
<p>나는 <strong>목표와 작업 맥락을 전달하고, AI와 대화를 이어가며 결과물을 만드는 것</strong>에는 비교적 익숙했다.</p>
<p>반면 부족한 부분도 분명했다.</p>
<p><strong>대상 독자와 톤을 정의하는 것, AI와의 상호작용 규칙을 미리 설정하는 것, 그리고 AI의 추론 자체를 검증하는 것</strong>은 앞으로 더 의식적으로 사용할 필요가 있었다.</p>
<p>특히 이번 보고서를 읽으며 가장 기억에 남은 것은 <strong>반복과 개선</strong>이었다.</p>
<p>AI가 발전할수록 처음부터 꽤 그럴듯한 답변을 만들어낼 가능성은 계속 높아질 것이다.</p>
<p>그렇다면 앞으로 중요한 능력은 단순히 좋은 결과를 빠르게 얻는 것에서 끝나지 않을 것 같다.</p>
<blockquote>
<p><strong>AI에게 답을 얻는 능력보다, 그 답을 다시 질문할 수 있는 능력.</strong></p>
</blockquote>
<p>결국 AI Fluency는 AI에게 얼마나 많은 일을 맡길 수 있는지를 평가하는 개념이라기보다, <strong>AI와 함께 생각하면서도 최종적인 판단을 놓치지 않는 능력</strong>에 가깝다고 느꼈다.</p>
<p>이번 결과는 현재의 AI 활용 방식을 보여주는 하나의 스냅샷일 뿐이다.</p>
<p>몇 달 뒤 같은 방식으로 다시 평가했을 때 지금 <code>Not observed</code>였던 행동들이 어떻게 바뀌어 있을지도 궁금하다.</p>
<p>앞으로는 AI에게 더 많은 일을 시키는 것보다,</p>
<p><strong>AI와 더 잘 협업하는 방법을 익히는 것</strong></p>
<p>을 하나의 목표로 삼아보려고 한다.</p>
<hr>
<h2 id="참고-자료">참고 자료</h2>
<ul>
<li>Anthropic, <em>Anthropic Education Report: The AI Fluency Index</em></li>
<li>Claude, <em>The 4 Ds of AI Fluency — Behavioral Indicators</em></li>
</ul>
<blockquote>
<p><strong>참고:</strong> AI Fluency 자기진단은 공식적인 역량 시험이 아니라 최근 대화에서 관찰된 행동을 기반으로 자신의 AI 활용 패턴을 탐색하기 위한 도구다. 또한 원 연구 역시 단일 기간의 Claude.ai 사용자 표본, 대화에서 관찰되지 않는 행동의 누락, 상관관계 분석 등의 한계를 가진다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 11장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-11%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-11%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 12 Aug 2026 00:58:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-10%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 10장 정리</a></p>
</blockquote>
<h1 id="11장-뉴스-피드-시스템-설계">11장 뉴스 피드 시스템 설계</h1>
<p>뉴스 피드(News Feed)는 사용자의 홈 화면에 지속적으로 업데이트되는 콘텐츠 목록을 의미한다.</p>
<p>페이스북, 인스타그램, 트위터와 같은 SNS에서 친구나 팔로우한 사용자의 게시물, 사진, 영상 등의 콘텐츠가 표시되는 화면이 대표적인 예이다.</p>
<p>이번 장에서는 이러한 <strong>뉴스 피드를 대규모 사용자에게 효율적으로 제공하는 시스템</strong>을 설계한다.</p>
<hr>
<h2 id="1단계-문제-이해-및-설계-범위-확정">1단계. 문제 이해 및 설계 범위 확정</h2>
<h3 id="요구사항">요구사항</h3>
<ul>
<li>모바일 앱과 웹 모두 지원</li>
<li>새로운 게시물 작성 가능</li>
<li>친구가 작성한 게시물을 뉴스 피드에서 조회 가능</li>
<li>게시물은 <strong>시간 역순(Reverse Chronological Order)</strong>으로 정렬</li>
<li>사용자당 최대 <strong>5,000명의 친구</strong></li>
<li><strong>DAU 1,000만 명</strong></li>
<li>이미지와 비디오 등의 미디어 지원</li>
</ul>
<blockquote>
<p>실제 서비스에서는 친밀도나 관심도 등을 이용하여 피드 순위를 결정할 수 있지만, 이번 설계에서는 단순히 최신 게시물부터 보여준다고 가정</p>
</blockquote>
<hr>
<h2 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계. 개략적 설계안 제시 및 동의 구하기</h2>
<p>뉴스 피드 시스템은 크게 두 가지 흐름으로 나뉜다.</p>
<h3 id="피드-발행feed-publishing">피드 발행(Feed Publishing)</h3>
<p>사용자가 새로운 게시물을 작성했을 때의 처리 과정</p>
<ul>
<li>게시물 저장</li>
<li>친구들의 뉴스 피드에 게시물 전달</li>
<li>필요 시 알림 전송</li>
</ul>
<h3 id="피드-읽기news-feed-building">피드 읽기(News Feed Building)</h3>
<p>사용자가 자신의 뉴스 피드를 조회할 때의 처리 과정</p>
<ul>
<li>뉴스 피드에 포함될 게시물 조회</li>
<li>게시물 및 사용자 정보 조합</li>
<li>클라이언트에 피드 반환</li>
</ul>
<hr>
<h2 id="뉴스-피드-api">뉴스 피드 API</h2>
<h3 id="피드-발행-api">피드 발행 API</h3>
<p>새로운 게시물을 작성하기 위한 API</p>
<pre><code class="language-http">POST /v1/me/feed</code></pre>
<ul>
<li><code>body</code>: 게시물 내용</li>
<li><code>Authorization</code>: 사용자 인증 정보</li>
</ul>
<h3 id="피드-읽기-api">피드 읽기 API</h3>
<p>뉴스 피드를 조회하기 위한 API</p>
<pre><code class="language-http">GET /v1/me/feed</code></pre>
<ul>
<li><code>Authorization</code>: 사용자 인증 정보</li>
</ul>
<hr>
<h2 id="피드-발행">피드 발행</h2>
<pre><code class="language-text">사용자
  ↓
로드밸런서
  ↓
웹 서버
 ├─ 포스팅 저장 서비스
 ├─ 포스팅 전송 서비스
 └─ 알림 서비스</code></pre>
<h3 id="사용자">사용자</h3>
<ul>
<li>모바일 앱 또는 웹 브라우저에서 게시물 작성</li>
<li><code>POST /v1/me/feed</code> API 호출</li>
</ul>
<h3 id="로드밸런서">로드밸런서</h3>
<ul>
<li>요청을 여러 웹 서버로 분산</li>
<li>특정 서버에 트래픽이 집중되는 현상 방지</li>
</ul>
<h3 id="웹-서버">웹 서버</h3>
<ul>
<li>HTTP 요청을 내부 서비스로 전달</li>
</ul>
<h3 id="포스팅-저장-서비스post-service">포스팅 저장 서비스(Post Service)</h3>
<p>새로운 게시물을 저장하는 역할</p>
<pre><code class="language-text">포스팅 저장 서비스
        ↓
    포스팅 캐시
        ↓
 포스팅 데이터베이스</code></pre>
<ul>
<li>게시물을 캐시에 저장</li>
<li>게시물을 데이터베이스에 영구 저장</li>
</ul>
<h3 id="포스팅-전송-서비스fanout-service">포스팅 전송 서비스(Fanout Service)</h3>
<p>새로운 게시물을 작성자의 친구들에게 전달하는 역할</p>
<ul>
<li>친구들의 뉴스 피드에 게시물 추가</li>
<li>뉴스 피드 정보는 <strong>뉴스 피드 캐시</strong>에 저장</li>
<li>사용자가 빠르게 피드를 조회할 수 있도록 지원</li>
</ul>
<blockquote>
<p>새로운 게시물을 다른 사용자들의 뉴스 피드로 전달하는 과정을 <strong>팬아웃(Fanout)</strong>이라고 한다.</p>
</blockquote>
<hr>
<h3 id="여기서-말하는-팬아웃은-fan-in--fan-out에서의-fan-out과-같은-개념일까"><em>여기서 말하는 팬아웃은 Fan-in / Fan-out에서의 Fan-out과 같은 개념일까?</em></h3>
<ul>
<li><strong>같은 개념에서 출발한 Fan-out</strong></li>
<li>Fan-out의 기본 의미 → <strong>하나의 데이터나 요청을 여러 대상으로 분산·전파하는 구조</strong></li>
<li>뉴스 피드에서의 Fan-out → <strong>한 사용자의 게시물을 여러 친구 또는 팔로워의 피드로 전파하는 과정</strong></li>
</ul>
<pre><code class="language-text">게시물 1개
    ↓
 Fan-out
 ↙  ↓  ↘
사용자 A  사용자 B  사용자 C</code></pre>
<ul>
<li><strong>Fan-out = 1 → N 방향의 확산</strong></li>
<li><strong>Fan-in = N → 1 방향의 수집</strong></li>
<li>뉴스 피드에서는 Fan-out을 <strong>게시물 전파 과정에 적용한 개념</strong></li>
<li><code>Fanout-on-write</code> → 게시물 <strong>작성 시점</strong>에 전파</li>
<li><code>Fanout-on-read</code> → 피드 <strong>조회 시점</strong>에 필요한 게시물을 가져와 구성</li>
</ul>
<hr>
<h3 id="알림-서비스notification-service">알림 서비스(Notification Service)</h3>
<ul>
<li>새로운 게시물이 등록되었음을 친구에게 알림</li>
<li>필요에 따라 푸시 알림 전송</li>
</ul>
<hr>
<h2 id="피드-읽기">피드 읽기</h2>
<pre><code class="language-text">사용자
  ↓
로드밸런서
  ↓
웹 서버
  ↓
뉴스 피드 서비스
  ↓
뉴스 피드 캐시</code></pre>
<h3 id="뉴스-피드-서비스news-feed-service">뉴스 피드 서비스(News Feed Service)</h3>
<ul>
<li>사용자의 뉴스 피드 조회 담당</li>
<li>뉴스 피드 캐시에서 필요한 게시물 ID 목록 조회</li>
</ul>
<h3 id="뉴스-피드-캐시news-feed-cache">뉴스 피드 캐시(News Feed Cache)</h3>
<ul>
<li>사용자의 뉴스 피드에 포함될 <strong>게시물 ID 목록 저장</strong></li>
<li>게시물 전체를 저장하지 않고 필요한 식별 정보만 저장</li>
</ul>
<pre><code class="language-text">user_id = 100

Feed Cache
→ post_id 302
→ post_id 299
→ post_id 281</code></pre>
<p>실제 피드 조회 시 해당 <code>post_id</code>를 이용해 게시물 정보를 가져온다.</p>
<hr>
<h2 id="3단계-상세-설계">3단계. 상세 설계</h2>
<p>상세 설계에서는 다음 두 부분을 중심으로 살펴본다.</p>
<ul>
<li>피드 발행</li>
<li>피드 읽기</li>
</ul>
<hr>
<h1 id="피드-발행-흐름-상세-설계">피드 발행 흐름 상세 설계</h1>
<h2 id="웹-서버-1">웹 서버</h2>
<p>웹 서버는 요청 전달 외에도 다음 기능을 수행한다.</p>
<h3 id="인증">인증</h3>
<ul>
<li>Authorization 헤더의 인증 토큰 확인</li>
<li>인증된 사용자만 게시물 작성 가능</li>
</ul>
<h3 id="처리율-제한rate-limiting">처리율 제한(Rate Limiting)</h3>
<ul>
<li>일정 시간 동안 사용자가 작성할 수 있는 게시물 수 제한</li>
<li>스팸 및 유해 콘텐츠의 반복 등록 방지</li>
</ul>
<hr>
<h2 id="포스팅-전송-서비스">포스팅 전송 서비스</h2>
<p>팬아웃은 새로운 게시물을 작성자와 관계가 있는 사용자들의 뉴스 피드에 전달하는 과정이다.</p>
<p>팬아웃 방식은 크게 두 가지가 있다.</p>
<ul>
<li><strong>Fanout-on-write</strong></li>
<li><strong>Fanout-on-read</strong></li>
</ul>
<hr>
<h3 id="쓰기-시점-팬아웃fanout-on-write">쓰기 시점 팬아웃(Fanout-on-write)</h3>
<p><strong>Push Model</strong></p>
<p>게시물이 작성되는 순간 친구들의 뉴스 피드를 미리 갱신하는 방식</p>
<pre><code class="language-text">사용자 A 게시물 작성
        ↓
A의 친구 목록 조회
        ↓
친구들의 Feed Cache 갱신</code></pre>
<p>즉, 사용자가 피드를 요청하기 전에 뉴스 피드를 미리 만들어 두는 <strong>Pre-computed 방식</strong></p>
<h4 id="장점">장점</h4>
<ul>
<li>뉴스 피드의 빠른 갱신</li>
<li>피드 조회 시 이미 데이터가 준비되어 있어 빠른 읽기 가능</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>친구가 많은 사용자의 경우 모든 친구의 피드를 갱신하는 데 많은 자원 필요</li>
<li>비활성 사용자의 피드까지 갱신하므로 자원 낭비 가능</li>
</ul>
<h3 id="핫키-문제hot-key-problem">핫키 문제(Hot Key Problem)</h3>
<p>팔로워가 매우 많은 사용자가 게시물을 작성하면 한 번의 게시물 작성으로 엄청난 수의 뉴스 피드를 갱신해야 하는 문제</p>
<pre><code class="language-text">일반 사용자
→ 친구 수백 명의 피드 갱신

유명 사용자
→ 팔로워 수천만 명의 피드 갱신</code></pre>
<hr>
<h3 id="읽기-시점-팬아웃fanout-on-read">읽기 시점 팬아웃(Fanout-on-read)</h3>
<p><strong>Pull Model</strong></p>
<p>게시물을 작성할 때는 저장만 하고, 사용자가 피드를 조회할 때 필요한 게시물을 가져오는 방식</p>
<pre><code class="language-text">게시물 작성
   ↓
게시물 저장

사용자 피드 조회
   ↓
친구들의 최신 게시물 조회
   ↓
뉴스 피드 생성</code></pre>
<p>사용자가 요청했을 때 데이터를 가져오는 <strong>On-demand 방식</strong></p>
<h4 id="장점-1">장점</h4>
<ul>
<li>비활성 사용자의 피드를 미리 만들 필요 없음</li>
<li>불필요한 컴퓨팅 자원 사용 감소</li>
<li>모든 팔로워에게 미리 Push하지 않으므로 핫키 문제 완화</li>
</ul>
<h4 id="단점-1">단점</h4>
<ul>
<li>피드를 조회하는 순간 여러 게시물을 가져와야 하므로 읽기 지연 증가 가능</li>
</ul>
<hr>
<h3 id="하이브리드-팬아웃">하이브리드 팬아웃</h3>
<p>두 방식의 장점을 활용하기 위해 혼합하여 사용한다.</p>
<ul>
<li>대부분의 사용자 → <strong>Push Model</strong></li>
<li>팔로워가 매우 많은 사용자 → <strong>Pull Model</strong></li>
</ul>
<pre><code class="language-text">일반 사용자
→ 게시물 작성 시 Push

유명 사용자
→ 게시물 저장
→ 사용자가 피드를 읽을 때 Pull</code></pre>
<p>이를 통해 대부분의 사용자는 빠른 피드 조회가 가능하고, 유명 사용자 때문에 발생하는 대규모 팬아웃 부하도 줄일 수 있다.</p>
<hr>
<h2 id="팬아웃-서비스-동작-과정">팬아웃 서비스 동작 과정</h2>
<h3 id="1-친구-id-목록-조회">1. 친구 ID 목록 조회</h3>
<ul>
<li>그래프 데이터베이스에서 작성자의 친구 ID 목록 조회</li>
<li>그래프 DB는 사용자 간 관계 관리에 적합</li>
</ul>
<pre><code class="language-text">사용자 A
   ↓
Graph DB
   ↓
[B, C, D, E ...]</code></pre>
<h3 id="2-친구-정보-조회-및-필터링">2. 친구 정보 조회 및 필터링</h3>
<p>사용자 정보 캐시에서 친구들의 정보를 가져온 뒤 사용자 설정에 따라 제외할 대상을 필터링</p>
<p>예)</p>
<ul>
<li>음소거(Mute)</li>
<li>차단</li>
<li>게시물 공개 범위 제한</li>
</ul>
<h3 id="3-메시지-큐-삽입">3. 메시지 큐 삽입</h3>
<p>다음 정보를 메시지 큐에 저장</p>
<ul>
<li>게시물 ID</li>
<li>게시물을 전달받을 사용자 목록</li>
</ul>
<pre><code class="language-text">Fanout Service
      ↓
Message Queue</code></pre>
<h3 id="4-팬아웃-작업-서버-처리">4. 팬아웃 작업 서버 처리</h3>
<ul>
<li>메시지 큐에서 팬아웃 작업 조회</li>
<li>각 사용자의 뉴스 피드 캐시 갱신</li>
</ul>
<h3 id="5-뉴스-피드-캐시-저장">5. 뉴스 피드 캐시 저장</h3>
<p>뉴스 피드 캐시에는 게시물 전체가 아닌 <strong>게시물 ID 중심의 최소 정보</strong> 저장</p>
<pre><code class="language-text">user_id
→ [post_id, post_id, post_id ...]</code></pre>
<p>게시물 전체를 저장하지 않는 이유</p>
<ul>
<li>메모리 사용량 절감</li>
<li>동일 게시물 중복 저장 방지</li>
<li>최신 게시물 위주의 빠른 조회 가능</li>
</ul>
<p>또한 대부분의 사용자는 최신 게시물만 조회하므로 캐시에 저장할 게시물 개수를 제한할 수 있다.</p>
<hr>
<h1 id="피드-읽기-흐름-상세-설계">피드 읽기 흐름 상세 설계</h1>
<pre><code class="language-text">사용자
  ↓
로드밸런서
  ↓
웹 서버
  ↓
뉴스 피드 서비스
  ↓
뉴스 피드 캐시
  ↓
사용자 / 게시물 캐시
  ↓
클라이언트</code></pre>
<p>이미지나 동영상과 같은 미디어 파일은 <strong>CDN</strong>을 통해 제공한다.</p>
<h3 id="동작-과정">동작 과정</h3>
<ol>
<li>사용자가 <code>/v1/me/feed</code>로 뉴스 피드 조회 요청</li>
<li>로드밸런서가 요청을 웹 서버로 전달</li>
<li>웹 서버가 뉴스 피드 서비스 호출</li>
<li>뉴스 피드 서비스가 뉴스 피드 캐시에서 <code>post_id</code> 목록 조회</li>
<li>사용자 캐시와 포스팅 캐시에서 필요한 정보 조회</li>
<li>완성된 뉴스 피드를 JSON 형태로 클라이언트에 반환</li>
<li>클라이언트가 뉴스 피드 렌더링</li>
</ol>
<pre><code class="language-text">News Feed Cache
→ post_id 목록

Post Cache
→ 게시물 내용

User Cache
→ 작성자 정보

CDN
→ 이미지 / 동영상</code></pre>
<hr>
<h2 id="캐시-구조">캐시 구조</h2>
<p>뉴스 피드 시스템에서는 캐시를 데이터 특성에 따라 여러 계층으로 구분할 수 있다.</p>
<h3 id="뉴스-피드-캐시">뉴스 피드 캐시</h3>
<ul>
<li>사용자별 뉴스 피드의 게시물 ID 저장</li>
</ul>
<h3 id="콘텐츠-캐시">콘텐츠 캐시</h3>
<ul>
<li>게시물 데이터 저장</li>
<li>인기 콘텐츠와 일반 콘텐츠 분리 가능</li>
</ul>
<h3 id="소셜-그래프-캐시">소셜 그래프 캐시</h3>
<ul>
<li>사용자 간 관계 정보 저장</li>
<li>팔로워</li>
<li>팔로잉</li>
</ul>
<h3 id="행동-캐시action-cache">행동 캐시(Action Cache)</h3>
<p>게시물에 대한 사용자 행동 저장</p>
<ul>
<li>좋아요</li>
<li>댓글</li>
<li>기타 행동</li>
</ul>
<h3 id="횟수-캐시counter-cache">횟수 캐시(Counter Cache)</h3>
<p>각종 횟수 정보 저장</p>
<ul>
<li>좋아요 수</li>
<li>댓글 수</li>
<li>팔로워 수</li>
<li>팔로잉 수</li>
</ul>
<pre><code class="language-text">Cache

├─ News Feed
├─ Content
├─ Social Graph
├─ Action
└─ Counter</code></pre>
<p>데이터 성격에 따라 캐시를 분리하면 각각의 특성에 맞게 독립적으로 확장 및 최적화할 수 있다.</p>
<hr>
<h2 id="4단계-마무리">4단계. 마무리</h2>
<p>이번 장에서는 뉴스 피드 시스템을 크게 다음 두 부분으로 나누어 설계하였다.</p>
<pre><code class="language-text">피드 발행
→ 게시물 작성
→ 게시물 저장
→ Fanout
→ 친구들의 Feed Cache 갱신

피드 읽기
→ 뉴스 피드 요청
→ Feed Cache 조회
→ 게시물 / 사용자 정보 조합
→ 클라이언트에 반환</code></pre>
<p>뉴스 피드 시스템의 핵심은 <strong>어느 시점에 사용자의 피드를 만들어 둘 것인지 결정하는 것</strong></p>
<ul>
<li>Push → 읽기는 빠르지만 쓰기 비용 증가</li>
<li>Pull → 쓰기 비용은 작지만 읽기 비용 증가</li>
<li>Hybrid → 사용자 특성에 따라 Push와 Pull을 함께 사용</li>
</ul>
<hr>
<h3 id="추가-고려사항">추가 고려사항</h3>
<h4 id="데이터베이스-확장">데이터베이스 확장</h4>
<ul>
<li>수직적 확장 vs 수평적 확장</li>
<li>SQL vs NoSQL</li>
<li>주-복제본 구조</li>
<li>읽기 복제본 활용</li>
<li>일관성 모델</li>
<li>데이터베이스 샤딩</li>
</ul>
<h4 id="시스템-확장성">시스템 확장성</h4>
<ul>
<li>웹 계층의 무상태(Stateless) 구성</li>
<li>다중 데이터 센터 지원</li>
<li>적극적인 캐시 활용</li>
<li>메시지 큐를 통한 컴포넌트 간 결합도 감소</li>
</ul>
<h4 id="모니터링">모니터링</h4>
<p>주요 지표</p>
<ul>
<li>트래픽 집중 시간대의 QPS</li>
<li>뉴스 피드 조회 지연시간</li>
<li>캐시 적중률(Cache Hit Rate)</li>
<li>팬아웃 처리 시간</li>
<li>메시지 큐 적재량</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 10장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-10%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-10%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 11 Aug 2026 05:07:18 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-9%EC%9E%A5-%EC%A0%95%EB%A6%AC-u2nl6iya">[ 대규모 시스템 설계 스터디 ] 9장 정리</a>
다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-11%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 11장 정리</a></p>
</blockquote>
<h1 id="10장-알림-시스템-설계">10장 알림 시스템 설계</h1>
<ul>
<li>최근 많은 프로그램이 채택한 인기 있는 기능</li>
<li>최신 뉴스, 제품 업데이트, 이벤트, 선물 등 고객에게 중요할 만한 정보를 비동기적으로 제공</li>
<li>알림 시스템은 단순히 모바일 푸시 알림에 한정되지 않음</li>
</ul>
<ol>
<li>모바일 푸시 알림</li>
<li>SMS 메시지</li>
<li>이메일</li>
</ol>
<p>3가지로 분류</p>
<p><img src="https://velog.velcdn.com/images/jung_ji_in02/post/996208c7-5041-4acc-9b1b-287c4bce3ca0/image.png" alt=""></p>
<h2 id="1단계-문제-이해-및-설계-범위-확정">1단계. 문제 이해 및 설계 범위 확정</h2>
<ul>
<li>알림 시스템 설계 문제는 <strong>정해진 하나의 정답이 없는 개방형 문제</strong></li>
<li>면접관과의 질의를 통해 <strong>요구사항과 설계 범위를 먼저 명확히 정의</strong></li>
<li>대규모 알림 처리뿐 아니라 <strong>어떤 알림을, 언제, 어디로, 누가 보낼 것인지</strong> 확인 필요</li>
</ul>
<h3 id="알림-종류">알림 종류</h3>
<ul>
<li><strong>푸시 알림(Push Notification)</strong></li>
<li><strong>SMS 메시지</strong></li>
<li><strong>이메일</strong></li>
</ul>
<h3 id="실시간성">실시간성</h3>
<ul>
<li><strong>연성 실시간(Soft Real-Time) 시스템</strong></li>
<li>알림은 가능한 빠르게 전달</li>
<li>시스템 부하가 높은 경우 <strong>약간의 지연 허용</strong></li>
</ul>
<blockquote>
<p>즉, 정해진 시간 안에 반드시 전달되어야 하는 Hard Real-Time 시스템은 아님</p>
</blockquote>
<blockquote>
<h4 id="soft-real-time-시스템">Soft Real-Time 시스템:</h4>
<p>정해진 시간 내 처리가 중요하지만, 일부 지연이 발생해도 시스템의 정상 동작이 가능한 시스템</p>
</blockquote>
<h4 id="hard-real-time-시스템">Hard Real-Time 시스템:</h4>
<p>정해진 시간 내 처리가 반드시 보장되어야 하며, 시간 초과를 시스템 실패로 간주하는 시스템</p>
<h3 id="지원-단말">지원 단말</h3>
<ul>
<li>iOS</li>
<li>Android</li>
<li>랩톱 / 데스크톱</li>
</ul>
<h3 id="알림-생성-주체">알림 생성 주체</h3>
<p>알림은 크게 두 곳에서 생성 가능</p>
<ul>
<li><p><strong>클라이언트 애플리케이션</strong></p>
<ul>
<li>사용자의 행동이나 이벤트에 의해 알림 생성</li>
</ul>
</li>
<li><p><strong>서버</strong></p>
<ul>
<li>서버에서 일정에 맞춰 알림 스케줄링 및 생성</li>
</ul>
</li>
</ul>
<h3 id="알림-수신-거부">알림 수신 거부</h3>
<ul>
<li>사용자가 <strong>Opt-out 설정 가능</strong></li>
<li>수신 거부한 사용자는 이후 해당 알림을 받지 않음</li>
</ul>
<blockquote>
<p>Opt-out: 기본적으로 제공되는 서비스나 알림에 대해 사용자가 수신 거부를 선택하는 방식</p>
</blockquote>
<h3 id="하루-처리-규모">하루 처리 규모</h3>
<ul>
<li>모바일 푸시 알림: <strong>1,000만 건/일</strong></li>
<li>SMS: <strong>100만 건/일</strong></li>
<li>이메일: <strong>500만 건/일</strong></li>
</ul>
<p>→ 하루 최대 <strong>1,600만 건 규모의 알림 처리</strong></p>
<hr>
<h2 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h2>
<p>개략적 설계에서는 다음 세 가지를 중심으로 알림 시스템의 동작 방식을 살펴본다.</p>
<ul>
<li><strong>알림 유형별 지원 방안</strong></li>
<li><strong>연락처 정보 수집 절차</strong></li>
<li><strong>알림 전송 및 수신 절차</strong></li>
</ul>
<h3 id="알림-유형별-지원-방안">알림 유형별 지원 방안</h3>
<p>알림 유형에 따라 사용자에게 알림을 전달하는 방식과 사용하는 외부 서비스가 다르다.</p>
<hr>
<h4 id="ios-푸시-알림">iOS 푸시 알림</h4>
<p>iOS에서 푸시 알림을 보내기 위해서는 다음 세 가지 컴포넌트가 필요하다.</p>
<ul>
<li><p><strong>알림 제공자(Provider)</strong></p>
<ul>
<li>알림 요청(Notification Request)을 생성하여 APNS로 전달하는 주체</li>
<li>알림 요청 생성 시 <strong>단말 토큰과 페이로드</strong> 필요</li>
</ul>
</li>
<li><p><strong>단말 토큰(Device Token)</strong></p>
<ul>
<li>알림을 전송할 특정 iOS 단말을 식별하기 위한 <strong>고유 식별자</strong></li>
</ul>
</li>
<li><p><strong>페이로드(Payload)</strong></p>
<ul>
<li>실제 알림 내용을 담은 데이터</li>
<li>일반적으로 <code>Key : Value</code> 형태의 <strong>JSON 객체(JSON Object)</strong>로 구성</li>
</ul>
</li>
</ul>
<pre><code class="language-json">{
  &quot;aps&quot;: {
    &quot;alert&quot;: {
      &quot;title&quot;: &quot;Game Request&quot;,
      &quot;body&quot;: &quot;Bob wants to play chess&quot;,
      &quot;action-loc-key&quot;: &quot;PLAY&quot;
    },
    &quot;badge&quot;: 5
  }
}</code></pre>
<ul>
<li><p><strong>APNS(Apple Push Notification Service)</strong></p>
<ul>
<li>애플이 제공하는 원격 푸시 알림 서비스</li>
<li>알림 제공자가 전달한 푸시 알림을 해당 iOS 단말로 전달</li>
</ul>
</li>
<li><p><strong>iOS 단말</strong></p>
<ul>
<li>APNS를 통해 푸시 알림을 최종적으로 수신하는 사용자 단말</li>
</ul>
</li>
</ul>
<pre><code class="language-text">알림 제공자 → APNS → iOS 단말</code></pre>
<hr>
<h4 id="android-푸시-알림">Android 푸시 알림</h4>
<ul>
<li>iOS 푸시 알림과 거의 동일한 방식으로 동작</li>
<li>APNS 대신 <strong>FCM(Firebase Cloud Messaging)</strong> 사용</li>
<li>FCM이 알림 제공자로부터 알림을 받아 Android 단말로 전달</li>
</ul>
<pre><code class="language-text">알림 제공자 → FCM → Android 단말</code></pre>
<blockquote>
<p>iOS는 <strong>APNS</strong>, Android는 <strong>FCM</strong>을 통해 푸시 알림 전달</p>
</blockquote>
<h4 id="sms-메시지">SMS 메시지</h4>
<ul>
<li>SMS 전송 시 일반적으로 <strong>제3자 SMS 서비스</strong> 이용</li>
<li>대표적으로 Twilio, Nexmo 등의 서비스 존재</li>
<li>대부분 상용 서비스이므로 사용량에 따른 비용 발생</li>
</ul>
<pre><code class="language-text">알림 제공자 → SMS 서비스 → SMS 수신 단말</code></pre>
<h4 id="이메일">이메일</h4>
<ul>
<li>자체 이메일 서버 구축도 가능하지만, 일반적으로 <strong>상용 이메일 서비스</strong> 활용</li>
<li>대표적으로 SendGrid, Mailchimp 등의 서비스 존재</li>
<li>이메일 전송뿐만 아니라 높은 전송 성공률 및 데이터 분석 기능 제공</li>
</ul>
<pre><code class="language-text">알림 제공자 → 이메일 서비스 → 이메일 수신 단말</code></pre>
<blockquote>
<p>결국 알림 제공자가 사용자 단말에 직접 알림을 보내는 것이 아니라, <strong>알림 유형에 따라 APNS, FCM, SMS 서비스, 이메일 서비스 등의 외부 서비스를 거쳐 전달하는 구조</strong>이다.</p>
</blockquote>
<hr>
<h3 id="연락처-정보-수집-절차">연락처 정보 수집 절차</h3>
<p>알림을 전송하기 위해서는 사용자에 대한 다음 정보가 필요하다.</p>
<ul>
<li>모바일 <strong>단말 토큰(Device Token)</strong></li>
<li><strong>전화번호</strong></li>
<li><strong>이메일 주소</strong></li>
</ul>
<p>사용자가 앱을 설치하거나 처음 계정을 등록하면 API 서버가 해당 정보를 수집하여 데이터베이스에 저장한다.</p>
<pre><code class="language-text">사용자 단말
    ↓ 앱 설치 / 계정 등록
로드밸런서
    ↓
API 서버
    ↓ 연락처 정보 저장
데이터베이스</code></pre>
<h4 id="연락처-정보-저장-구조">연락처 정보 저장 구조</h4>
<p>사용자 정보와 단말 정보는 별도의 테이블에 저장한다.</p>
<p><strong>User 테이블</strong></p>
<ul>
<li><code>user_id</code></li>
<li><code>email</code></li>
<li><code>country_code</code></li>
<li><code>phone_number</code></li>
<li><code>created_at</code></li>
</ul>
<p><strong>Device 테이블</strong></p>
<ul>
<li><code>id</code></li>
<li><code>device_token</code></li>
<li><code>user_id</code></li>
<li><code>last_logged_in_at</code></li>
</ul>
<pre><code class="language-text">User 1 ───── N Device</code></pre>
<ul>
<li>한 명의 사용자가 <strong>여러 개의 단말을 보유할 수 있으므로 1:N 관계</strong></li>
<li>이메일 주소와 전화번호 → <code>User</code> 테이블에 저장</li>
<li>단말 토큰 → <code>Device</code> 테이블에 저장</li>
<li>한 사용자가 여러 단말을 가지고 있다면 <strong>해당 사용자의 모든 단말에 알림 전송 가능</strong></li>
</ul>
<hr>
<h3 id="알림-전송-및-수신-절차">알림 전송 및 수신 절차</h3>
<p>우선 단순한 형태의 알림 시스템을 설계한 후 발생하는 문제점을 확인하고 개선한다.</p>
<h3 id="개략적-설계안---초안">개략적 설계안 - 초안</h3>
<pre><code class="language-text">서비스 1 ─┐
서비스 2 ─┼─→ 알림 시스템 → 제3자 서비스 → 사용자 단말
   ...    │
서비스 N ─┘</code></pre>
<h4 id="서비스-1--n">서비스 1 ~ N</h4>
<ul>
<li>알림을 발생시키는 여러 서비스</li>
<li>각각 마이크로서비스, 크론잡, 분산 시스템 컴포넌트 등이 될 수 있음</li>
<li>알림 시스템의 API를 호출하여 알림 전송 요청</li>
</ul>
<p>예)</p>
<ul>
<li>과금 서비스 → 납기일 알림</li>
<li>쇼핑몰 서비스 → 배송 알림</li>
<li>크론잡 → 특정 시간에 예약 알림</li>
</ul>
<h4 id="알림-시스템">알림 시스템</h4>
<p>알림 전송과 수신 처리를 담당하는 핵심 컴포넌트</p>
<ul>
<li>서비스 1~N에서 사용할 <strong>알림 전송 API 제공</strong></li>
<li>사용자에게 전달할 <strong>알림 페이로드 생성</strong></li>
<li>사용자 연락처 및 단말 정보 조회</li>
<li>알림 유형에 따라 적절한 제3자 서비스로 전달</li>
</ul>
<h4 id="제3자-서비스">제3자 서비스</h4>
<p>실제로 사용자 단말까지 알림을 전달하는 역할</p>
<pre><code class="language-text">iOS     → APNS
Android → FCM
SMS     → SMS 서비스
Email   → 이메일 서비스</code></pre>
<p>제3자 서비스를 선택할 때는 <strong>확장성(Extensibility)</strong>도 고려해야 한다.</p>
<ul>
<li>새로운 서비스를 쉽게 추가하거나 제거할 수 있는 구조 필요</li>
<li>국가나 시장에 따라 사용할 수 있는 서비스가 다를 수 있음</li>
<li>특정 서비스에 강하게 종속되지 않도록 설계</li>
</ul>
<hr>
<h3 id="초안의-문제점">초안의 문제점</h3>
<p>초기 설계에서는 하나의 알림 서버가 대부분의 역할을 담당하기 때문에 다음과 같은 문제가 발생한다.</p>
<h4 id="spofsingle-point-of-failure">SPOF(Single Point Of Failure)</h4>
<ul>
<li>알림 서버가 하나뿐인 구조</li>
<li>해당 서버에 장애가 발생하면 <strong>전체 알림 서비스 중단</strong></li>
</ul>
<h4 id="규모-확장성-문제">규모 확장성 문제</h4>
<ul>
<li>알림 관련 처리를 하나의 서버가 모두 담당</li>
<li>데이터베이스, 캐시 등의 컴포넌트를 <strong>독립적으로 확장하기 어려움</strong></li>
</ul>
<h4 id="성능-병목">성능 병목</h4>
<ul>
<li><p>알림 처리에는 상대적으로 오래 걸리는 작업 존재</p>
<ul>
<li>알림 콘텐츠 생성</li>
<li>HTML 생성</li>
<li>외부 서비스 응답 대기 등</li>
</ul>
</li>
<li><p>하나의 서버에서 모든 작업을 처리하면 트래픽이 증가할 때 <strong>과부하 발생 가능</strong></p>
</li>
</ul>
<hr>
<h3 id="개략적-설계안---개선된-버전">개략적 설계안 - 개선된 버전</h3>
<p>초안의 문제를 해결하기 위해 다음과 같이 개선한다.</p>
<ul>
<li><strong>데이터베이스와 캐시를 알림 서버에서 분리</strong></li>
<li><strong>알림 서버를 여러 대로 증설하여 수평적 확장</strong></li>
<li><strong>메시지 큐를 도입하여 시스템 컴포넌트 간 결합도 감소</strong></li>
<li>알림 종류별로 별도의 메시지 큐 사용</li>
</ul>
<pre><code class="language-text">서비스 1~N
    ↓
알림 서버
 ├─ 캐시
 ├─ 데이터베이스
 ↓
메시지 큐
 ├─ iOS Push Queue
 ├─ Android Push Queue
 ├─ SMS Queue
 └─ Email Queue
 ↓
작업 서버(Worker)
 ↓
제3자 서비스
 ↓
사용자 단말</code></pre>
<h3 id="알림-서버notification-server">알림 서버(Notification Server)</h3>
<p>알림 서버는 다음 기능을 담당한다.</p>
<h4 id="알림-전송-api">알림 전송 API</h4>
<ul>
<li>다른 서비스가 알림을 요청할 수 있도록 API 제공</li>
<li>스팸 방지 등을 위해 일반적으로 <strong>내부 서비스 또는 인증된 클라이언트만 접근 허용</strong></li>
</ul>
<h4 id="알림-검증validation">알림 검증(Validation)</h4>
<ul>
<li>이메일 주소</li>
<li>전화번호</li>
<li>사용자 정보</li>
</ul>
<p>등 알림 전송에 필요한 기본 정보 검증</p>
<h4 id="데이터베이스--캐시-질의">데이터베이스 / 캐시 질의</h4>
<p>알림을 생성하는 데 필요한 정보를 조회한다.</p>
<ul>
<li>사용자 정보</li>
<li>단말 토큰</li>
<li>알림 설정</li>
<li>연락처 정보 등</li>
</ul>
<h4 id="알림-전송">알림 전송</h4>
<ul>
<li>알림 데이터를 적절한 <strong>메시지 큐에 삽입</strong></li>
<li>알림 유형별로 큐를 분리하여 병렬 처리 가능</li>
</ul>
<hr>
<h3 id="데이터베이스">데이터베이스</h3>
<ul>
<li>사용자 정보 저장</li>
<li>단말 정보 저장</li>
<li>알림 설정 등 다양한 정보 저장</li>
</ul>
<h3 id="캐시">캐시</h3>
<ul>
<li>자주 사용하는 사용자 정보나 알림 관련 데이터를 빠르게 조회하기 위해 사용</li>
<li>데이터베이스 접근 횟수 감소</li>
</ul>
<hr>
<h3 id="메시지-큐message-queue">메시지 큐(Message Queue)</h3>
<p>알림 서버와 작업 서버 사이에 위치하여 두 컴포넌트 간의 <strong>강한 의존성을 제거</strong></p>
<pre><code class="language-text">알림 서버 → 메시지 큐 → 작업 서버</code></pre>
<p>주요 역할</p>
<ul>
<li>대량의 알림 요청을 임시로 저장하는 <strong>버퍼 역할</strong></li>
<li>생산 속도와 처리 속도의 차이 완화</li>
<li>알림 종류별 독립적인 처리 가능</li>
</ul>
<p>예를 들어 SMS 서비스에 문제가 생기더라도</p>
<pre><code class="language-text">SMS 알림        → 지연 또는 실패
iOS 알림        → 정상
Android 알림    → 정상
Email 알림      → 정상</code></pre>
<p>처럼 다른 종류의 알림에 미치는 영향을 줄일 수 있다.</p>
<hr>
<h3 id="작업-서버worker">작업 서버(Worker)</h3>
<ul>
<li>메시지 큐에서 알림 이벤트를 가져옴</li>
<li>가져온 알림을 적절한 제3자 서비스로 전달</li>
<li>필요에 따라 여러 작업 서버를 두어 병렬 처리 가능</li>
</ul>
<pre><code class="language-text">메시지 큐
   ↓
작업 서버
   ↓
APNS / FCM / SMS / Email 서비스</code></pre>
<blockquote>
<h4 id="작업-서버가-왜-필요할까">작업 서버가 왜 필요할까?</h4>
<p>알림 서버가 모든 전송 작업까지 직접 처리하지 않게 하기 위해서.
작업 서버(Worker)는 메시지 큐에 쌓인 알림을 실제로 꺼내서 제3자 서비스로 전송하는 실행 담당 서버 알림 서버가 외부 서비스의 응답을 계속 기다려야 해서 요청 처리가 느려지고, 대량의 알림이 들어오면 병목이 발생할 수 있음.</p>
</blockquote>
<hr>
<h3 id="전체-알림-전송-과정">전체 알림 전송 과정</h3>
<ol>
<li>서비스가 <strong>알림 전송 API를 호출하여 알림 서버에 요청</strong></li>
<li>알림 서버가 캐시 또는 데이터베이스에서 <strong>사용자 정보, 단말 토큰, 알림 설정 등의 메타데이터 조회</strong></li>
<li>알림 서버가 알림 유형에 맞는 <strong>이벤트를 생성하여 해당 메시지 큐에 삽입</strong></li>
<li>작업 서버가 <strong>메시지 큐에서 알림 이벤트를 가져옴</strong></li>
<li>작업 서버가 알림을 <strong>제3자 서비스로 전달</strong></li>
<li>제3자 서비스가 <strong>사용자 단말로 최종 알림 전송</strong></li>
</ol>
<pre><code class="language-text">서비스
  ↓ ① 알림 요청
알림 서버
  ↓ ② 사용자 정보 조회
DB / Cache
  ↓
알림 서버
  ↓ ③ 메시지 삽입
Message Queue
  ↓ ④ 이벤트 조회
Worker
  ↓ ⑤ 전송
APNS / FCM / SMS / Email
  ↓ ⑥ 최종 전달
사용자 단말</code></pre>
<blockquote>
<p><strong>핵심:</strong> 알림 서버가 모든 작업을 직접 처리하지 않고, <strong>메시지 큐를 중간에 두어 비동기적으로 처리</strong>함으로써 서버 간 결합도를 낮추고 대량의 알림 요청에도 확장 가능한 구조를 구성한다.</p>
</blockquote>
<hr>
<h2 id="3단계-상세-설계">3단계. 상세 설계</h2>
<p>개략적 설계안을 바탕으로 실제 알림 시스템을 운영하기 위해 필요한 세부 요소를 추가한다.</p>
<p>주요 고려사항은 다음과 같다.</p>
<ul>
<li><strong>안정성(Reliability)</strong></li>
<li><strong>알림 템플릿</strong></li>
<li><strong>알림 설정</strong></li>
<li><strong>전송률 제한(Rate Limiting)</strong></li>
<li><strong>재시도 메커니즘(Retry Mechanism)</strong></li>
<li><strong>보안(Security)</strong></li>
<li><strong>큐 모니터링</strong></li>
<li><strong>이벤트 추적</strong></li>
</ul>
<hr>
<h3 id="안정성">안정성</h3>
<p>분산 환경에서 알림 시스템을 운영할 때는 <strong>알림이 유실되거나 불필요하게 중복 전송되는 문제</strong>를 고려해야 한다.</p>
<h4 id="데이터-손실-방지">데이터 손실 방지</h4>
<ul>
<li>알림 시스템의 중요한 요구사항 중 하나는 <strong>어떤 상황에서도 알림이 유실되지 않도록 하는 것</strong></li>
<li>알림이 조금 늦거나 순서가 바뀌는 것은 허용할 수 있지만, <strong>알림 자체가 사라지는 것은 방지</strong></li>
<li>이를 위해 전송할 알림 데이터를 저장하고 <strong>재시도 메커니즘</strong> 구성</li>
<li>알림의 전송 상태를 추적하기 위해 <strong>알림 로그(Notification Log) 데이터베이스</strong> 유지</li>
</ul>
<pre><code class="language-text">메시지 큐
    ↓
작업 서버 ──→ 제3자 서비스
    ↓
알림 로그 DB</code></pre>
<blockquote>
<p>알림 로그를 통해 어떤 알림이 생성·전송되었는지 기록하고, 장애 발생 시 유실된 알림을 확인하거나 재처리할 수 있음</p>
</blockquote>
<hr>
<h4 id="알림-중복-전송-방지">알림 중복 전송 방지</h4>
<p>분산 시스템의 특성상 같은 알림이 여러 번 전송되는 것을 <strong>100% 완전히 방지하는 것은 어려움</strong></p>
<p>따라서 중복 가능성을 줄이기 위한 별도의 중복 탐지 로직 사용</p>
<ul>
<li>각 알림에 <strong>고유한 이벤트 ID(Event ID)</strong> 부여</li>
<li>알림 처리 전에 해당 이벤트 ID가 이전에 처리된 적이 있는지 확인</li>
<li>이미 처리된 이벤트라면 폐기</li>
<li>처음 처리하는 이벤트라면 정상적으로 알림 전송</li>
</ul>
<pre><code class="language-text">알림 이벤트 도착
      ↓
Event ID 확인
      ↓
이미 처리함? ── Yes → 폐기
      │
      No
      ↓
알림 전송</code></pre>
<blockquote>
<p>즉, <strong>고유 ID를 이용한 중복 제거(Deduplication)</strong> 방식</p>
</blockquote>
<hr>
<h3 id="추가로-필요한-컴포넌트-및-고려사항">추가로 필요한 컴포넌트 및 고려사항</h3>
<p>실제 대규모 알림 시스템에는 단순히 알림을 전달하는 기능 외에도 여러 기능이 필요하다.</p>
<hr>
<h3 id="알림-템플릿">알림 템플릿</h3>
<p>대규모 알림 시스템에서는 하루에도 수백만 건 이상의 알림이 생성되지만, 실제 메시지의 형식은 비슷한 경우가 많다.</p>
<p>따라서 <strong>미리 정의한 알림 템플릿</strong>을 이용해 메시지를 생성한다.</p>
<p>예를 들어:</p>
<pre><code class="language-text">여러분이 꿈꿔온 그 상품을 준비했습니다.
[item_name]이 다시 입고되었습니다!
[date]까지만 주문 가능합니다!</code></pre>
<p>실제 전송 시:</p>
<pre><code class="language-text">item_name = MacBook
date = 8월 20일</code></pre>
<p>처럼 필요한 값만 넣어 메시지를 완성한다.</p>
<h4 id="장점">장점</h4>
<ul>
<li>알림 작성 시간 절감</li>
<li>메시지 형식의 일관성 유지</li>
<li>메시지 생성 과정 단순화</li>
<li>작성 오류 감소</li>
</ul>
<blockquote>
<p><strong>템플릿 + 파라미터 → 실제 알림 메시지 생성</strong></p>
</blockquote>
<hr>
<h3 id="알림-설정">알림 설정</h3>
<p>사용자는 지나치게 많은 알림을 원하지 않을 수 있으므로 <strong>알림 수신 여부를 직접 설정할 수 있어야 한다.</strong></p>
<p>알림 설정 테이블의 예:</p>
<pre><code class="language-text">user_id     bigint
channel     varchar
opt_in      boolean</code></pre>
<ul>
<li><code>user_id</code>: 사용자 식별자</li>
<li><code>channel</code>: Push, SMS, Email 등 알림 채널</li>
<li><code>opt_in</code>: 해당 채널의 알림 수신 여부</li>
</ul>
<p>따라서 알림을 전송하기 전에 반드시:</p>
<pre><code class="language-text">알림 생성
   ↓
사용자 알림 설정 조회
   ↓
수신 허용?
 ├─ Yes → 알림 전송
 └─ No  → 전송하지 않음</code></pre>
<blockquote>
<p>1단계에서 정했던 <strong>Opt-out 요구사항을 실제 시스템에 반영하는 부분</strong></p>
</blockquote>
<hr>
<h3 id="전송률-제한">전송률 제한</h3>
<p>한 사용자에게 너무 많은 알림이 전달되지 않도록 <strong>전송 빈도에 제한</strong>을 둔다.</p>
<p>예를 들어:</p>
<pre><code class="language-text">사용자 A
→ 1시간 동안 푸시 알림 최대 5건</code></pre>
<p>처럼 제한할 수 있다.</p>
<p>전송률 제한이 필요한 이유:</p>
<ul>
<li>과도한 알림으로 인한 사용자 피로도 감소</li>
<li>사용자가 알림 기능 자체를 꺼버리는 상황 방지</li>
<li>불필요한 외부 서비스 호출 및 비용 감소</li>
</ul>
<blockquote>
<p><strong>Rate Limiting = 사용자에게 일정 시간 동안 보낼 수 있는 알림 수 제한</strong></p>
</blockquote>
<hr>
<h3 id="재시도-방법">재시도 방법</h3>
<p>APNS, FCM, SMS 서비스 등의 <strong>제3자 서비스에서 알림 전송에 실패할 수 있음</strong></p>
<p>이 경우 실패한 알림을 바로 버리지 않고 <strong>재시도 전용 큐에 다시 넣어 처리</strong></p>
<pre><code class="language-text">작업 서버
   ↓
제3자 서비스 전송
   ↓
전송 실패
   ↓
재시도 큐
   ↓
다시 작업 서버
   ↓
재전송</code></pre>
<ul>
<li>지정된 횟수만큼 재시도</li>
<li>동일한 문제가 계속 발생하면 개발자에게 <strong>Alert 발생</strong></li>
</ul>
<blockquote>
<p>재시도 메커니즘을 통해 일시적인 네트워크 장애나 외부 서비스 장애로 인한 <strong>알림 유실 가능성 감소</strong></p>
</blockquote>
<hr>
<h3 id="푸시-알림과-보안">푸시 알림과 보안</h3>
<p>iOS와 Android의 알림 전송 API는 아무 사용자가 호출할 수 있게 두면 안 된다.</p>
<p>따라서 다음과 같은 인증 정보를 이용한다.</p>
<ul>
<li><code>appKey</code></li>
<li><code>appSecret</code></li>
</ul>
<p>이를 통해 <strong>인증되거나 승인된 클라이언트만 알림 전송 API 사용 가능</strong></p>
<pre><code class="language-text">클라이언트
   ↓ appKey / appSecret 확인
알림 서버
   ↓
인증 성공 시 알림 전송</code></pre>
<blockquote>
<p>목적: 악의적인 사용자가 임의로 대량의 알림을 보내는 문제 방지</p>
</blockquote>
<hr>
<h3 id="큐-모니터링">큐 모니터링</h3>
<p>메시지 큐에서 중요한 모니터링 지표 중 하나는 <strong>큐에 쌓여 있는 메시지의 개수</strong>이다.</p>
<pre><code class="language-text">알림 생성 속도 &gt; 작업 서버 처리 속도

        ↓

메시지 큐에 알림 계속 누적</code></pre>
<p>큐의 크기가 지속적으로 증가한다면:</p>
<ul>
<li>작업 서버가 알림을 충분히 빠르게 처리하지 못하고 있다는 의미</li>
<li>필요에 따라 <strong>작업 서버를 추가하여 처리량 증가</strong></li>
</ul>
<pre><code class="language-text">Queue

Worker 1
Worker 2
Worker 3
   ↓

처리량 증가</code></pre>
<blockquote>
<p><strong>큐의 길이를 모니터링하여 시스템의 병목 여부를 파악하고 Worker 수를 조절</strong></p>
</blockquote>
<hr>
<h3 id="이벤트-추적">이벤트 추적</h3>
<p>알림을 단순히 전송하는 것만으로 끝나는 것이 아니라 <strong>사용자가 실제로 알림에 어떻게 반응했는지 추적</strong>할 필요가 있다.</p>
<p>주요 지표:</p>
<ul>
<li>알림 전송 여부</li>
<li>알림 수신 여부</li>
<li>알림 확인율</li>
<li>클릭률</li>
<li>실제 앱 사용으로 이어진 비율</li>
<li>알림 수신 거부 여부</li>
<li>전송 오류</li>
</ul>
<p>예를 들면 다음과 같은 이벤트 흐름을 추적할 수 있다.</p>
<pre><code class="language-text">시작
 ↓
발송 대기
 ↓
알림 발송
 ↓
사용자 수신
 ├─→ 클릭
 └─→ 알림 수신 거부 설정

발송 대기 / 발송 과정
 └─→ 오류</code></pre>
<p>이러한 이벤트는 <strong>데이터 분석 서비스(Analytics)</strong>와 연동하여 수집한다.</p>
<blockquote>
<p>이를 통해 어떤 알림이 효과적인지 분석하고 향후 알림 시스템 개선에 활용</p>
</blockquote>
<hr>
<h3 id="수정된-설계안">수정된 설계안</h3>
<p>지금까지의 상세 설계를 반영하면 기존 구조에 여러 컴포넌트가 추가된다.</p>
<pre><code class="language-text">서비스 1 ~ N
      ↓
   알림 서버
 ├─ 인증
 ├─ 전송률 제한
 ├─ 사용자 설정 확인
 └─ 사용자 정보 조회
      ↓
 메시지 큐
      ↓
   작업 서버
 ├─ 알림 템플릿
 ├─ 알림 로그
 └─ 실패 시 재시도
      ↓
 제3자 서비스
(APNS / FCM / SMS / Email)
      ↓
   사용자 단말
      ↓
이벤트 추적 / 데이터 분석</code></pre>
<h4 id="기존-설계에서-추가된-핵심-기능">기존 설계에서 추가된 핵심 기능</h4>
<ul>
<li>알림 서버에 <strong>인증(Authentication)</strong> 기능 추가</li>
<li>알림 서버에 <strong>전송률 제한(Rate Limiting)</strong> 기능 추가</li>
<li>사용자별 <strong>알림 설정 확인</strong></li>
<li>실패한 알림을 다시 처리하는 <strong>재시도 메커니즘</strong></li>
<li><strong>알림 템플릿</strong>을 이용한 메시지 생성</li>
<li><strong>알림 로그 DB</strong>를 통한 전송 기록 및 데이터 손실 방지</li>
<li><strong>큐 모니터링</strong>을 통한 Worker 확장</li>
<li>알림 발송부터 클릭까지의 <strong>이벤트 추적 및 분석</strong></li>
</ul>
<hr>
<h3 id="3단계-핵심-정리">3단계 핵심 정리</h3>
<pre><code class="language-text">안정성
→ 알림 로그 + 재시도로 데이터 유실 방지
→ Event ID로 중복 전송 최소화

알림 템플릿
→ 반복되는 메시지 형식을 미리 정의

알림 설정
→ 사용자별 Push / SMS / Email 수신 여부 확인

전송률 제한
→ 사용자에게 너무 많은 알림이 전송되는 것 방지

재시도
→ 전송 실패 시 재시도 큐를 통해 다시 처리

보안
→ 인증된 서비스만 알림 API 사용 가능

큐 모니터링
→ 큐의 메시지 수를 확인하여 Worker 증설 판단

이벤트 추적
→ 발송·수신·클릭·오류 등의 사용자 행동 추적</code></pre>
<blockquote>
<p><strong>핵심:</strong> 2단계에서 만든 확장 가능한 알림 전송 구조에 <strong>신뢰성, 사용자 설정, 보안, 재시도, 모니터링, 분석 기능을 추가하여 실제 운영 가능한 알림 시스템으로 발전시키는 단계</strong>이다.</p>
</blockquote>
<hr>
<h2 id="4단계-마무리">4단계. 마무리</h2>
<p>알림 시스템은 사용자에게 중요한 정보를 지속적으로 전달하기 위해 필요한 시스템이다.</p>
<p>예를 들어 다음과 같은 서비스에서 활용된다.</p>
<ul>
<li>넷플릭스의 신작 영화 출시 알림</li>
<li>신규 상품 할인 쿠폰 이메일</li>
<li>온라인 쇼핑 결제 확정 메시지</li>
<li>배송 상태 알림</li>
</ul>
<p>이번 설계에서는 <strong>푸시 알림, SMS, 이메일 등 여러 채널을 지원하면서 대규모 트래픽에도 확장 가능한 알림 시스템</strong>을 구성하였다.</p>
<h3 id="전체-설계-핵심">전체 설계 핵심</h3>
<ul>
<li><p>다양한 알림 채널 지원</p>
<ul>
<li>iOS Push</li>
<li>Android Push</li>
<li>SMS</li>
<li>Email</li>
</ul>
</li>
<li><p>메시지 큐를 통한 <strong>컴포넌트 간 결합도 감소</strong></p>
</li>
<li><p>알림 서버와 작업 서버의 <strong>수평적 확장</strong></p>
</li>
<li><p>사용자 및 단말 정보를 별도의 데이터베이스에 저장</p>
</li>
<li><p>캐시를 통한 데이터 조회 성능 개선</p>
</li>
</ul>
<hr>
<h3 id="안정성-1">안정성</h3>
<ul>
<li>알림 전송 실패율을 낮추기 위해 <strong>재시도 메커니즘</strong> 적용</li>
<li>실패한 알림을 다시 큐에 넣어 지정된 횟수만큼 재전송</li>
<li>알림 로그를 저장하여 전송 상태 및 실패 여부 추적</li>
</ul>
<pre><code class="language-text">전송 실패
   ↓
재시도 큐
   ↓
작업 서버
   ↓
재전송</code></pre>
<ul>
<li>이벤트 ID를 이용해 중복 알림 탐지</li>
<li>완벽한 중복 제거는 어렵지만 <strong>중복 전송 가능성을 최소화</strong></li>
</ul>
<hr>
<h3 id="보안">보안</h3>
<ul>
<li>인증되지 않은 사용자가 임의로 알림을 보내는 것을 방지</li>
<li><code>appKey</code>, <code>appSecret</code> 등을 이용한 인증 적용</li>
<li><strong>인증된 클라이언트만 알림 전송 API 사용 가능</strong></li>
</ul>
<pre><code class="language-text">클라이언트
   ↓ 인증
알림 서버
   ↓
알림 처리</code></pre>
<hr>
<h3 id="사용자-설정">사용자 설정</h3>
<ul>
<li>사용자가 자신에게 전달되는 알림의 종류 및 채널을 설정할 수 있도록 지원</li>
<li>알림 전송 전에 사용자의 설정 확인</li>
</ul>
<p>예)</p>
<pre><code class="language-text">user_id
channel
opt_in</code></pre>
<ul>
<li><code>channel</code> → Push / SMS / Email 등</li>
<li><code>opt_in</code> → 해당 채널의 알림 수신 여부</li>
</ul>
<pre><code class="language-text">알림 전송 요청
      ↓
사용자 설정 확인
   ├─ ON  → 전송
   └─ OFF → 전송하지 않음</code></pre>
<hr>
<h3 id="전송률-제한-1">전송률 제한</h3>
<ul>
<li>한 사용자에게 너무 많은 알림이 전송되지 않도록 <strong>알림 빈도 제한</strong></li>
<li>사용자 피로도 및 알림 기능 비활성화 가능성 감소</li>
</ul>
<p>예)</p>
<pre><code class="language-text">사용자 A
→ 일정 시간 동안 최대 N개의 알림 허용</code></pre>
<blockquote>
<p><strong>전송률 제한 = 일정 시간 동안 사용자에게 전달할 수 있는 알림 수 제한</strong></p>
</blockquote>
<hr>
<h3 id="알림-템플릿-1">알림 템플릿</h3>
<ul>
<li>비슷한 형태의 알림을 매번 새로 생성하지 않고 <strong>미리 정의된 템플릿 사용</strong></li>
<li>파라미터만 변경하여 실제 메시지 생성</li>
</ul>
<pre><code class="language-text">[item_name] 상품이 다시 입고되었습니다.
[date]까지만 주문 가능합니다.</code></pre>
<h4 id="장점-1">장점</h4>
<ul>
<li>알림 생성 과정 단순화</li>
<li>메시지 형식 일관성 유지</li>
<li>작성 시간 감소</li>
<li>오류 가능성 감소</li>
</ul>
<hr>
<h3 id="이벤트-추적-및-모니터링">이벤트 추적 및 모니터링</h3>
<p>알림이 생성된 이후 실제 사용자에게 전달되기까지의 과정을 추적한다.</p>
<pre><code class="language-text">알림 생성
   ↓
발송 대기
   ↓
알림 발송
   ↓
사용자 수신
   ↓
클릭</code></pre>
<p>추적 가능한 주요 이벤트:</p>
<ul>
<li>발송 대기</li>
<li>전송 성공</li>
<li>전송 실패</li>
<li>사용자 수신</li>
<li>알림 클릭</li>
<li>알림 수신 거부</li>
</ul>
<p>이러한 데이터를 <strong>데이터 분석 서비스와 연동</strong>하여 사용자 행동을 분석할 수 있다.</p>
<hr>
<h3 id="큐-모니터링-1">큐 모니터링</h3>
<ul>
<li>메시지 큐에 쌓인 알림 수를 지속적으로 모니터링</li>
<li>큐의 크기가 계속 증가하면 <strong>작업 서버의 처리 속도가 부족한 상태</strong></li>
</ul>
<pre><code class="language-text">알림 생성량
     &gt;
Worker 처리량

→ Queue에 메시지 누적</code></pre>
<p>필요한 경우:</p>
<pre><code class="language-text">Worker 증설
→ 병렬 처리 증가
→ 큐에 쌓인 메시지 감소</code></pre>
<hr>
<h3 id="최종-설계에서-추가된-요소">최종 설계에서 추가된 요소</h3>
<p>2단계의 개략적인 설계에 다음 기능들이 추가되었다.</p>
<pre><code class="language-text">서비스
   ↓
알림 서버
 ├─ 인증
 ├─ 전송률 제한
 ├─ 사용자 설정 확인
 └─ 사용자 정보 조회
   ↓
메시지 큐
   ↓
작업 서버
 ├─ 재시도
 ├─ 알림 템플릿
 └─ 알림 로그
   ↓
제3자 서비스
   ↓
사용자 단말
   ↓
이벤트 추적 / 모니터링</code></pre>
<hr>
<h3 id="최종-정리">최종 정리</h3>
<p>이번 알림 시스템 설계에서 중점적으로 고려한 내용은 다음과 같다.</p>
<pre><code class="language-text">확장성
→ 알림 서버 / Worker 수평 확장
→ 메시지 큐를 통한 비동기 처리

안정성
→ 실패 시 재시도
→ 알림 로그 저장
→ 중복 탐지

보안
→ 인증된 클라이언트만 API 호출 허용

사용자 설정
→ 사용자의 알림 수신 여부 반영

전송률 제한
→ 지나친 알림 전송 방지

템플릿
→ 반복되는 알림 생성 과정 단순화

모니터링
→ 큐 상태 및 시스템 처리량 확인

이벤트 추적
→ 발송부터 클릭까지 사용자 행동 분석</code></pre>
<blockquote>
<p><strong>핵심:</strong> 단순히 알림을 전달하는 것이 아니라, <strong>확장성·안정성·보안·사용자 설정·모니터링까지 고려하여 실제 대규모 서비스에서 운영 가능한 알림 시스템을 설계하는 것이 핵심</strong>이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[CS 면접 바이블: Chapter 1 (4)]]></title>
            <link>https://velog.io/@jung_ji_in02/CS-%EB%A9%B4%EC%A0%91-%EB%B0%94%EC%9D%B4%EB%B8%94-Chapter-1-4</link>
            <guid>https://velog.io/@jung_ji_in02/CS-%EB%A9%B4%EC%A0%91-%EB%B0%94%EC%9D%B4%EB%B8%94-Chapter-1-4</guid>
            <pubDate>Tue, 11 Aug 2026 02:31:50 GMT</pubDate>
            <description><![CDATA[<h2 id="115-프록시-패턴과-프록시-서버">1.1.5 프록시 패턴과 프록시 서버</h2>
<ul>
<li><strong>프록시 객체</strong>: 프록시 패턴이 적용된 객체</li>
</ul>
<h3 id="프록시-패턴">프록시 패턴</h3>
<ul>
<li><p>그림</p>
</li>
<li><p>대상 객체에 접근하기 전에 <strong>접근 흐름을 가로채는 중간 계층</strong></p>
</li>
<li><p>요청을 필터링하거나 수정하여 대상 객체에 대한 접근 제어</p>
</li>
<li><p>객체의 기능을 보완하면서 보안성 강화</p>
</li>
<li><p>데이터 검증, 캐싱, 로깅 등에 활용</p>
</li>
<li><p>객체뿐만 아니라 <strong>프록시 서버</strong>에도 적용</p>
</li>
</ul>
<blockquote>
<h4 id="프록시-서버에서의-캐싱">프록시 서버에서의 캐싱</h4>
<p>자주 사용하는 데이터를 캐시에 저장한 뒤, 동일한 요청이 들어오면 원격 서버에 다시 요청하지 않고 캐시의 데이터를 반환하는 방식</p>
<ul>
<li>원격 서버 요청 횟수 감소</li>
<li>네트워크 트래픽 감소</li>
<li>응답 속도 향상</li>
</ul>
</blockquote>
<hr>
<h3 id="프록시-서버">프록시 서버</h3>
<ul>
<li>클라이언트와 서버 사이에 위치하는 <strong>중간 서버</strong></li>
<li>클라이언트가 프록시 서버를 통해 다른 네트워크 서비스에 <strong>간접적으로 접근하도록 중계</strong></li>
</ul>
<h4 id="프록시-서버로-쓰는-nginx">프록시 서버로 쓰는 nginx</h4>
<ul>
<li>비동기 이벤트 기반 구조의 웹 서버</li>
<li>적은 자원으로 다수의 연결을 효과적으로 처리</li>
<li>주로 <strong>Node.js 서버 앞단의 프록시 서버</strong>로 활용</li>
</ul>
<blockquote>
<p>&quot;Node.js의 버퍼 오버플로우 취약점을 예방하기 위해서는 nginx를 프록시 서버로 앞단에 놓고 Node.js를 뒤쪽에 놓는 것이 좋다.&quot;
— Ryan Dahl</p>
</blockquote>
<p>nginx를 앞단에 두었을 때의 장점</p>
<ul>
<li>외부 사용자의 실제 서버 직접 접근 차단</li>
<li>중간 계층을 통한 보안 강화</li>
<li>실제 서버의 IP 및 포트 은닉</li>
<li>정적 자원의 gzip 압축</li>
<li>메인 서버 앞단에서 요청 로깅 가능</li>
</ul>
<blockquote>
<h4 id="버퍼-오버플로우">버퍼 오버플로우</h4>
<p>버퍼의 크기를 초과하는 데이터가 입력되어 <strong>할당된 메모리 영역을 넘어 다른 영역까지 침범하는 현상</strong></p>
<p>이 과정에서 다른 메모리의 주소나 값이 변경되어 보안 취약점으로 이어질 수 있음.</p>
</blockquote>
<blockquote>
<h4 id="gzip-압축">gzip 압축</h4>
<p><strong>DEFLATE 알고리즘</strong>을 기반으로 데이터를 압축하는 방식</p>
<ul>
<li>데이터 전송량 감소</li>
<li>네트워크 사용량 감소</li>
<li>단, 압축·해제 과정에서 CPU 연산 비용 발생</li>
</ul>
<p>따라서 전송량 감소 효과와 CPU 오버헤드를 고려하여 적용 필요</p>
</blockquote>
<hr>
<h3 id="프록시-서버로-쓰는-cloudflare">프록시 서버로 쓰는 Cloudflare</h3>
<ul>
<li>웹 서버 앞단에 위치하는 <strong>프록시 서버</strong></li>
<li>사용자 요청을 먼저 Cloudflare가 받은 뒤 실제 웹 서버로 전달</li>
<li>외부 사용자가 실제 웹 서버에 직접 접근하는 것을 줄여 <strong>보안 강화</strong></li>
<li>주로 <strong>DDoS 공격 방어, HTTPS 구축, 캐싱/CDN</strong> 등에 활용</li>
</ul>
<h4 id="ddos-공격-방어">DDoS 공격 방어</h4>
<ul>
<li>DDoS는 짧은 시간 동안 서버에 대량의 요청을 보내 <strong>서비스를 마비시키는 공격</strong></li>
<li>Cloudflare가 실제 서버 앞단에서 트래픽을 먼저 검사</li>
<li>정상 사용자가 아닌 것으로 판단되는 <strong>의심스러운 트래픽을 자동 차단</strong></li>
<li>필요에 따라 CAPTCHA 등의 검증을 통해 정상 사용자 여부 확인</li>
<li>공격 트래픽이 실제 서버까지 도달하는 것을 줄여 <strong>서버 부하 및 불필요한 클라우드 비용 감소</strong></li>
<li>Cloudflare의 대규모 네트워크와 캐싱을 활용하여 DDoS 공격 대응</li>
</ul>
<pre><code class="language-text">사용자 ───────→ Cloudflare ───────→ 웹 서버
크롤러 ──────→     ↓
공격자 ──────→  트래픽 검사
                  ↓
             의심 트래픽 차단</code></pre>
<h4 id="https-구축">HTTPS 구축</h4>
<ul>
<li>HTTPS 사용을 위해서는 일반적으로 <strong>SSL/TLS 인증서</strong>가 필요</li>
<li>Cloudflare를 프록시 서버로 사용하면 Cloudflare를 통해 HTTPS 연결 구성 가능</li>
<li>사용자와 Cloudflare 사이의 통신을 암호화하여 <strong>데이터 전송 보안 강화</strong></li>
</ul>
<h4 id="cdn">CDN</h4>
<ul>
<li><strong>CDN(Content Delivery Network)</strong>: 여러 지역의 서버에 콘텐츠를 캐싱하고 사용자와 가까운 서버에서 콘텐츠를 제공하는 네트워크</li>
<li>Cloudflare가 이미지, CSS, JavaScript 등의 정적 콘텐츠를 캐싱</li>
<li>매 요청마다 원본 서버에 접근할 필요 감소</li>
<li>원본 서버의 트래픽 및 부하 감소</li>
<li>사용자와 가까운 서버에서 콘텐츠를 제공하여 <strong>응답 속도 향상</strong></li>
</ul>
<blockquote>
<p>핵심적으로는 <strong>사용자 → 실제 서버</strong>로 바로 접근하는 구조에서
<strong>사용자 → Cloudflare → 실제 서버</strong> 구조로 변경하여 Cloudflare가 보안, 트래픽 제어, 캐싱 등을 대신 처리하는 방식</p>
</blockquote>
<hr>
<h2 id="116-이터레이터-패턴">1.1.6 이터레이터 패턴</h2>
<ul>
<li><strong>이터레이터(iterator)</strong>를 사용하여 컬렉션의 요소에 순차적으로 접근하는 패턴</li>
<li>자료구조의 내부 구조와 관계없이 <strong>동일한 인터페이스를 통해 순회 가능</strong></li>
<li>배열, <code>Map</code>, <code>Set</code>처럼 서로 다른 자료구조도 같은 방식으로 순회 가능</li>
</ul>
<h3 id="예시-코드">예시 코드</h3>
<pre><code class="language-javascript">const mp = new Map()
mp.set(&#39;a&#39;, 1)
mp.set(&#39;b&#39;, 2)
mp.set(&#39;c&#39;, 3)

const st = new Set()
st.add(1)
st.add(2)
st.add(3)

for (let a of mp) console.log(a)
for (let a of st) console.log(a)

/*
[ &#39;a&#39;, 1 ]
[ &#39;b&#39;, 2 ]
[ &#39;c&#39;, 3 ]
1
2
3
*/</code></pre>
<ul>
<li><code>Map</code>과 <code>Set</code>은 서로 다른 자료구조</li>
<li>하지만 둘 다 <strong>이터러블(iterable)</strong>하기 때문에 <code>for...of</code>를 이용하여 동일한 방식으로 순회 가능</li>
<li>즉, 자료구조의 내부 구현을 알 필요 없이 <strong>이터레이터라는 공통된 방식으로 요소에 접근</strong></li>
</ul>
<blockquote>
<h4 id="이터레이터-프로토콜">이터레이터 프로토콜</h4>
<p>이터러블 객체의 요소를 <strong>어떤 방식으로 순회할 것인지 정의한 규칙</strong></p>
<p>JavaScript에서는 <code>next()</code>를 통해 다음 요소에 접근하며 일반적으로 <code>{ value, done }</code> 형태의 결과를 반환</p>
</blockquote>
<blockquote>
<h4 id="이터러블-객체">이터러블 객체</h4>
<p>반복 가능한 객체</p>
<p><code>Symbol.iterator</code> 메서드를 가지고 있어 <code>for...of</code> 등으로 순회할 수 있는 객체</p>
<p>ex) <code>Array</code>, <code>Map</code>, <code>Set</code>, <code>String</code></p>
</blockquote>
<hr>
<h2 id="117-노출모듈-패턴">1.1.7 노출모듈 패턴</h2>
<ul>
<li><strong>즉시 실행 함수(IIFE)</strong>를 이용하여 외부에 공개할 변수와 숨길 변수를 구분하는 패턴</li>
<li>모듈 내부의 변수와 함수 중 필요한 것만 외부에 노출</li>
<li><strong>정보 은닉과 캡슐화</strong>를 구현하는 데 활용</li>
</ul>
<blockquote>
<p>참고: 책의 설명처럼 과거 JavaScript에서는 <code>private</code>, <code>public</code> 접근 제어자를 직접 사용하는 방식이 제한적이었기 때문에 이러한 패턴이 많이 사용되었습니다.
현재 JavaScript 클래스에서는 <code>#privateField</code>와 같은 <strong>private class field</strong>도 지원합니다.</p>
</blockquote>
<h3 id="예시-코드-1">예시 코드</h3>
<pre><code class="language-javascript">const pukuba = (() =&gt; {
  const a = 1
  const b = () =&gt; 2

  const public = {
    c: 2,
    d: () =&gt; 3
  }

  return public
})()

console.log(pukuba)
console.log(pukuba.a)

/*
{ c: 2, d: [Function: d] }
undefined
*/</code></pre>
<ul>
<li><p><code>a</code>, <code>b</code></p>
<ul>
<li>외부로 반환되지 않음</li>
<li>외부에서 직접 접근 불가능</li>
<li><strong>private 역할</strong></li>
</ul>
</li>
<li><p><code>c</code>, <code>d</code></p>
<ul>
<li>객체에 담아 외부로 반환</li>
<li>다른 코드에서 접근 가능</li>
<li><strong>public 역할</strong></li>
</ul>
</li>
</ul>
<pre><code class="language-text">즉시 실행 함수 내부
│
├─ a, b → 외부에 노출 X → private
│
└─ c, d → return으로 노출 → public</code></pre>
<ul>
<li>노출모듈 패턴과 유사한 방식으로 모듈 내부 구현을 숨기고 필요한 기능만 외부에 공개 가능</li>
<li>JavaScript의 <strong>CommonJS(CJS)</strong> 등의 모듈 방식에서도 모듈 외부에 필요한 값만 내보내는 개념 활용</li>
</ul>
<blockquote>
<h4 id="public">public</h4>
<p>클래스나 모듈 <strong>외부에서도 접근 가능한 범위</strong></p>
<p>외부에서 자유롭게 변수나 메서드에 접근 가능</p>
</blockquote>
<blockquote>
<h4 id="protected">protected</h4>
<p>클래스 내부와 해당 클래스를 <strong>상속받은 하위 클래스에서 접근 가능한 범위</strong></p>
<p>일반적인 JavaScript에는 <code>protected</code> 접근 제어자가 문법적으로 직접 제공되지 않음</p>
</blockquote>
<blockquote>
<h4 id="private">private</h4>
<p>클래스나 모듈 <strong>내부에서만 접근 가능한 범위</strong></p>
<p>외부에서 직접 접근할 수 없도록 데이터를 숨기는 데 사용</p>
</blockquote>
<blockquote>
<h4 id="즉시-실행-함수iife">즉시 실행 함수(IIFE)</h4>
<p>함수를 정의함과 동시에 <strong>즉시 실행하는 함수</strong></p>
<p>별도의 함수 호출 없이 바로 실행되며, 함수 내부에 독립적인 스코프를 만들어 외부에서 변수에 직접 접근하지 못하게 할 수 있음.</p>
<pre><code class="language-javascript">(() =&gt; {
  // 실행할 코드
})()</code></pre>
</blockquote>
<hr>
<h2 id="118-mvc-패턴">1.1.8 MVC 패턴</h2>
<ul>
<li>MVC 패턴은 <strong>Model, View, Controller</strong>로 애플리케이션을 분리하는 디자인 패턴</li>
<li>각 구성 요소의 역할을 분리하여 <strong>관심사의 분리(Separation of Concerns)</strong> 가능</li>
<li>각 영역에 집중해서 개발할 수 있어 <strong>유지보수, 재사용성, 확장성 향상</strong></li>
<li>애플리케이션이 복잡해질수록 모델과 뷰 사이의 관계 및 전체 구조가 복잡해질 수 있음</li>
</ul>
<h3 id="모델">모델</h3>
<ul>
<li>애플리케이션에서 사용하는 <strong>데이터와 데이터 처리 로직</strong>을 담당</li>
<li>데이터베이스의 데이터, 변수, 상수 등의 정보 관리</li>
<li>데이터의 상태를 저장하고 변경</li>
</ul>
<p>예를 들어 화면에 사각형 박스와 글자가 있다면 모델에는 다음과 같은 정보가 포함될 수 있음.</p>
<ul>
<li><p>사각형의 위치 정보</p>
</li>
<li><p>글자 내용</p>
</li>
<li><p>글자 위치</p>
</li>
<li><p>글자 인코딩 및 형식 등의 데이터</p>
</li>
<li><p>사용자가 데이터를 생성하거나 수정하면 <strong>컨트롤러를 통해 모델의 데이터가 생성 또는 갱신</strong></p>
</li>
</ul>
<h3 id="뷰">뷰</h3>
<ul>
<li>사용자에게 보여지는 <strong>화면 및 UI 요소</strong>를 담당</li>
<li><code>input</code>, <code>checkbox</code>, <code>textarea</code> 등 사용자 인터페이스 요소 포함</li>
<li>모델의 데이터를 기반으로 사용자에게 화면을 표시</li>
<li>모델의 데이터를 직접 저장하기보다는 <strong>화면에 표현하는 역할에 집중</strong></li>
<li>사용자의 입력이나 이벤트가 발생하면 이를 컨트롤러에 전달</li>
</ul>
<h3 id="컨트롤러">컨트롤러</h3>
<ul>
<li><strong>모델과 뷰 사이를 연결하는 역할</strong></li>
<li>사용자의 요청과 이벤트 처리</li>
<li>요청을 해석하여 적절한 모델을 호출</li>
<li>모델의 처리 결과를 기반으로 적절한 뷰 선택</li>
<li>모델과 뷰의 흐름 및 생명주기 관리</li>
</ul>
<pre><code class="language-text">사용자
  ↓ 요청
Controller
  ↓
Model
  ↓ 처리 결과
Controller
  ↓
View
  ↓
사용자에게 화면 표시</code></pre>
<p>즉,</p>
<ul>
<li><strong>Model</strong> → 데이터</li>
<li><strong>View</strong> → 화면</li>
<li><strong>Controller</strong> → 요청 처리 및 Model·View 연결</li>
</ul>
<hr>
<h3 id="mvc-패턴의-예-spring-mvc">MVC 패턴의 예: Spring MVC</h3>
<ul>
<li>MVC 패턴을 기반으로 웹 애플리케이션을 개발할 수 있도록 지원하는 <strong>Spring의 웹 프레임워크</strong></li>
<li>HTTP 요청 처리, 데이터 전달, 화면 반환 등을 편리하게 구현 가능</li>
<li>애너테이션을 이용하여 사용자의 요청 값을 쉽게 전달받을 수 있음</li>
</ul>
<pre><code class="language-java">@GetMapping(&quot;/users/{id}&quot;)
public String getUser(
        @PathVariable Long id,
        @RequestParam String name) {
    ...
}</code></pre>
<p>주요 애너테이션</p>
<ul>
<li><p><code>@RequestParam</code></p>
<ul>
<li>쿼리 파라미터 값 처리</li>
</ul>
</li>
<li><p><code>@RequestHeader</code></p>
<ul>
<li>HTTP Header 값 처리</li>
</ul>
</li>
<li><p><code>@PathVariable</code></p>
<ul>
<li>URL 경로에 포함된 값 처리</li>
</ul>
</li>
</ul>
<blockquote>
<p><code>@PathValue</code>가 아니라 <strong><code>@PathVariable</code></strong>이 정확한 명칭입니다.</p>
</blockquote>
<p>Spring MVC의 장점</p>
<ul>
<li><p>요청 값의 바인딩과 검증을 편리하게 처리</p>
</li>
<li><p>잘못된 요청을 쉽게 검증 가능</p>
<ul>
<li>ex) 숫자가 필요한 값에 문자열이 전달된 경우</li>
</ul>
</li>
<li><p>Controller, Service 등의 코드를 분리하여 <strong>재사용성과 유지보수성 향상</strong></p>
</li>
<li><p>단위 테스트 및 통합 테스트 용이</p>
</li>
<li><p>리디렉션 및 요청 처리 흐름 구현 용이</p>
</li>
</ul>
<blockquote>
<h4 id="핵심">핵심</h4>
<p>MVC는 애플리케이션을 <strong>데이터(Model) / 화면(View) / 제어 로직(Controller)</strong>으로 분리하여 각 구성 요소의 역할을 명확하게 만드는 패턴입니다.</p>
</blockquote>
<hr>
<h2 id="119-mvp-패턴">1.1.9 MVP 패턴</h2>
<ul>
<li><strong>MVC 패턴에서 파생된 디자인 패턴</strong></li>
<li>MVC의 <code>Controller</code>가 <strong>Presenter</strong>로 변경된 구조</li>
<li>Presenter가 View와 Model 사이에서 데이터를 전달하고 UI 로직을 처리</li>
<li><strong>View와 Presenter가 1:1 관계</strong>를 가지는 경우가 많아 MVC보다 결합도가 높은 편</li>
</ul>
<pre><code class="language-text">View ↔ Presenter ↔ Model</code></pre>
<ul>
<li><strong>Model</strong> → 데이터 및 비즈니스 로직</li>
<li><strong>View</strong> → 사용자에게 보여지는 화면</li>
<li><strong>Presenter</strong> → View의 요청을 받아 Model을 처리하고 결과를 View에 전달</li>
</ul>
<blockquote>
<h4 id="presenter">Presenter</h4>
<p>View와 Model 사이에서 중간 역할을 하며, <strong>화면에 표시할 데이터와 UI 관련 로직을 처리하는 계층</strong></p>
</blockquote>
<hr>
<h2 id="1110-mvvm-패턴">1.1.10 MVVM 패턴</h2>
<ul>
<li>MVC의 <code>Controller</code>가 <strong>ViewModel</strong>로 변경된 디자인 패턴</li>
<li><code>Model</code>, <code>View</code>, <code>ViewModel</code>로 구성</li>
<li><strong>Command와 Data Binding</strong>을 활용</li>
<li>View와 ViewModel 사이의 데이터 바인딩을 통해 상태 변화가 UI에 자동 반영</li>
<li>UI와 비즈니스 로직의 분리가 쉬워 <strong>재사용성과 테스트 용이성 향상</strong></li>
</ul>
<pre><code class="language-text">View ↔ ViewModel ↔ Model</code></pre>
<h3 id="뷰모델">뷰모델</h3>
<ul>
<li><strong>View를 추상화하여 View에 필요한 상태와 동작을 관리하는 계층</strong></li>
<li>Model의 데이터를 View가 사용하기 적합한 형태로 가공</li>
<li>View가 직접 Model을 다루지 않도록 중간 역할 수행</li>
</ul>
<hr>
<h3 id="mvvm-패턴의-예-vuejs">MVVM 패턴의 예: Vue.js</h3>
<ul>
<li>MVVM과 유사한 구조를 활용하는 대표적인 <strong>프런트엔드 프레임워크</strong></li>
<li><strong>반응성(Reactivity)</strong>을 기반으로 데이터 변경 시 화면을 자동 갱신</li>
<li><code>computed</code>, <code>watch</code> 등을 이용하여 반응형 데이터 처리 가능</li>
<li>데이터 값을 변경하면 연결된 UI가 자동으로 변경</li>
<li>컴포넌트 기반으로 <strong>재사용 가능한 UI 구성 가능</strong></li>
</ul>
<pre><code class="language-javascript">const count = ref(0)

count.value++</code></pre>
<ul>
<li><code>count</code> 값이 변경되면 이를 사용하는 화면도 자동으로 갱신</li>
<li>HTML 템플릿과 데이터를 연결하여 UI 구성 가능</li>
</ul>
<blockquote>
<h4 id="command">Command</h4>
<p>사용자의 클릭, 입력 등 <strong>여러 UI 동작을 하나의 실행 가능한 액션으로 추상화하는 방식</strong></p>
<p>View가 직접 로직을 수행하지 않고 ViewModel에 정의된 동작을 실행하도록 구성</p>
</blockquote>
<blockquote>
<h4 id="data-binding">Data Binding</h4>
<p><strong>화면에 표시되는 데이터와 애플리케이션의 데이터를 연결하는 기법</strong></p>
<p>데이터가 변경되면 View에도 변경 사항이 자동으로 반영됨.</p>
</blockquote>
<blockquote>
<h4 id="양방향-데이터-바인딩">양방향 데이터 바인딩</h4>
<p><code>ViewModel → View</code>뿐만 아니라 <code>View → ViewModel</code> 방향으로도 값이 자동 반영되는 방식</p>
<pre><code class="language-text">ViewModel 변경 → View 변경
View 변경 → ViewModel 변경</code></pre>
<p>Vue.js에서는 대표적으로 <code>v-model</code>을 통해 양방향 데이터 바인딩을 구현할 수 있음.</p>
</blockquote>
<h3 id="핵심-차이">핵심 차이</h3>
<ul>
<li><strong>MVC</strong> → Controller가 Model과 View 연결</li>
<li><strong>MVP</strong> → Presenter가 View와 직접 연결되어 UI 로직 처리</li>
<li><strong>MVVM</strong> → ViewModel과 View를 데이터 바인딩으로 연결</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CS 면접 바이블: Chapter 1 (3)]]></title>
            <link>https://velog.io/@jung_ji_in02/CS-%EB%A9%B4%EC%A0%91-%EB%B0%94%EC%9D%B4%EB%B8%94-Chapter-1-3</link>
            <guid>https://velog.io/@jung_ji_in02/CS-%EB%A9%B4%EC%A0%91-%EB%B0%94%EC%9D%B4%EB%B8%94-Chapter-1-3</guid>
            <pubDate>Mon, 10 Aug 2026 08:43:55 GMT</pubDate>
            <description><![CDATA[<h2 id="113-전략-패턴">1.1.3 전략 패턴</h2>
<p><img src="https://velog.velcdn.com/images/jung_ji_in02/post/bdeaecd7-e878-43e3-aec3-6d82be6e5c5d/image.png" alt=""></p>
<ul>
<li>객체의 행위를 직접 수정하지 않고, <strong>행위를 전략(Strategy) 객체로 분리하여 교체하는 디자인 패턴</strong></li>
<li>여러 알고리즘을 하나의 인터페이스로 캡슐화하여 상황에 따라 원하는 전략 선택 가능</li>
<li><strong>정책 패턴(Policy Pattern)</strong>이라고도 함</li>
</ul>
<h3 id="핵심-구조">핵심 구조</h3>
<ul>
<li><strong>Strategy</strong>: 공통 동작을 정의하는 인터페이스</li>
<li><strong>ConcreteStrategy</strong>: 실제 동작을 구현한 구체적인 전략</li>
<li><strong>Context</strong>: 전략을 전달받아 해당 전략의 동작을 실행하는 객체</li>
</ul>
<h3 id="예시-결제-방식">예시: 결제 방식</h3>
<p>결제 기능 자체는 동일하지만 결제 방식만 변경하는 경우</p>
<pre><code class="language-text">ShoppingCart (Context)
        ↓
PaymentStrategy (Strategy)
       ↙      ↘
LUNACard    KAKAOCard</code></pre>
<ul>
<li><code>PaymentStrategy</code> → 결제 전략 인터페이스</li>
<li><code>LUNACardStrategy</code> → LUNACard 결제 전략</li>
<li><code>KAKAOCardStrategy</code> → KAKAOCard 결제 전략</li>
<li><code>ShoppingCart</code> → 선택된 결제 전략을 사용하는 Context</li>
</ul>
<h3 id="예시-코드">예시 코드</h3>
<pre><code class="language-java">import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;

// 1. 전략 인터페이스
interface PaymentStrategy {
    void pay(int amount);
}

// 2-1. 구체적인 전략 1: KAKAOCard
class KAKAOCardStrategy implements PaymentStrategy {
    private String name;
    private String cardNumber;
    private String cvv;
    private String dateOfExpiry;

    public KAKAOCardStrategy(String nm, String ccNum, String cvv, String expiryDate) {
        this.name = nm;
        this.cardNumber = ccNum;
        this.cvv = cvv;
        this.dateOfExpiry = expiryDate;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + &quot; paid using KAKAOCard.&quot;);
    }
}

// 2-2. 구체적인 전략 2: LUNACard
class LUNACardStrategy implements PaymentStrategy {
    private String emailId;
    private String password;

    public LUNACardStrategy(String email, String pwd) {
        this.emailId = email;
        this.password = pwd;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + &quot; paid using LUNACard.&quot;);
    }
}

// 3. 상품(Item) 클래스
class Item {
    private String name;
    private int price;

    public Item(String name, int cost) {
        this.name = name;
        this.price = cost;
    }

    public String getName() {
        return name;
    }

    public int getPrice() {
        return price;
    }
}

// 4. 쇼핑카트 (컨텍스트) 클래스
class ShoppingCart {
    List&lt;Item&gt; items;

    public ShoppingCart() {
        this.items = new ArrayList&lt;Item&gt;();
    }

    public void addItem(Item item) {
        this.items.add(item);
    }

    public void removeItem(Item item) {
        this.items.remove(item);
    }

    public int calculateTotal() {
        int sum = 0;
        for (Item item : items) {
            sum += item.getPrice();
        }
        return sum;
    }

    // 전략 객체를 매개변수로 받아 결제를 수행
    public void pay(PaymentStrategy paymentMethod) {
        int amount = calculateTotal();
        paymentMethod.pay(amount);
    }
}

// 5. 실행 클래스
public class HelloWorld {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        Item A = new Item(&quot;kundolA&quot;, 100);
        Item B = new Item(&quot;kundolB&quot;, 300);

        cart.addItem(A);
        cart.addItem(B);

        // LUNACard로 결제 (100 + 300 = 400원)
        cart.pay(new LUNACardStrategy(&quot;kundol@example.com&quot;, &quot;pukubababo&quot;));

        // KAKAOCard로 결제 (100 + 300 = 400원)
        cart.pay(new KAKAOCardStrategy(&quot;Ju hongchul&quot;, &quot;123456789&quot;, &quot;123&quot;, &quot;12/01&quot;));
    }
}</code></pre>
<h3 id="동작-흐름">동작 흐름</h3>
<pre><code class="language-java">cart.pay(new LUNACardStrategy(...));
cart.pay(new KAKAOCardStrategy(...));</code></pre>
<p><code>ShoppingCart</code>의 결제 로직을 변경하지 않고 <strong>전달하는 전략 객체만 변경하여 다른 결제 방식 사용</strong></p>
<h3 id="실행-결과">실행 결과</h3>
<pre><code class="language-java">400 paid using LUNACard.
400 paid using KAKAOCard.</code></pre>
<blockquote>
<p><strong>컨텍스트(Context)</strong>
특정 작업을 수행하는 데 필요한 상황이나 관련 정보를 의미하며, 전략 패턴에서는 <strong>전략 객체를 사용하여 실제 작업을 수행하는 객체</strong>를 의미함.</p>
</blockquote>
<h3 id="한-줄-정리">한 줄 정리</h3>
<p><strong>변하는 행위를 전략으로 분리하여, Context의 코드를 수정하지 않고 전략만 교체할 수 있도록 하는 패턴</strong></p>
<hr>
<h3 id="passport의-전략-패턴">Passport의 전략 패턴</h3>
<ul>
<li>Passport는 Node.js에서 인증 기능을 구현할 때 사용하는 미들웨어 라이브러리</li>
<li>인증 방식을 각각 하나의 전략(Strategy) 으로 분리하여 필요한 인증 방식 선택 가능</li>
</ul>
<h3 id="대표적인-인증-전략">대표적인 인증 전략</h3>
<ol>
<li>LocalStrategy</li>
</ol>
<ul>
<li>서비스 자체 회원가입 기반 인증</li>
<li>아이디와 비밀번호를 이용한 인증</li>
</ul>
<ol start="2">
<li>OAuth Strategy</li>
</ol>
<ul>
<li>외부 서비스 계정을 이용한 인증</li>
<li>ex) Google, Facebook, Naver 등</li>
</ul>
<blockquote>
<p>즉, 인증 로직 자체를 변경하지 않고 사용할 인증 전략만 교체하는 구조</p>
</blockquote>
<h3 id="예시-코드-1">예시 코드</h3>
<pre><code>var passport = require(&#39;passport&#39;)
  , LocalStrategy = require(&#39;passport-local&#39;).Strategy;

passport.use(new LocalStrategy(
  function(username, password, done) {
    User.findOne({ username: username }, function(err, user) {
      if (err) { return done(err); }
      if (!user) {
        return done(null, false, { message: &#39;Incorrect username.&#39; });
      }
      if (!user.validPassword(password)) {
        return done(null, false, { message: &#39;Incorrect password.&#39; });
      }
      return done(null, user);
    });
  }
));</code></pre><blockquote>
<h4 id="핵심">핵심</h4>
<p>Passport는 인증 방법을 각각 전략으로 분리하여 LocalStrategy, OAuth 전략 등을 상황에 따라 선택할 수 있음.</p>
</blockquote>
<h2 id="114-옵저버-패턴">1.1.4 옵저버 패턴</h2>
<p><img src="https://velog.velcdn.com/images/jung_ji_in02/post/11bd00b0-b7b5-4bfc-9825-0d04c3831646/image.png" alt=""></p>
<ul>
<li>어떤 객체의 <strong>상태 변화를 관찰하다가 변화가 발생하면 관련 객체들에게 자동으로 알리는 디자인 패턴</strong></li>
<li>하나의 객체 변화가 여러 객체에 영향을 주어야 할 때 사용</li>
</ul>
<h3 id="구성-요소">구성 요소</h3>
<ul>
<li><strong>Subject(주체)</strong>: 상태가 변경되는 객체</li>
<li><strong>Observer(옵저버)</strong>: 주체의 상태 변화를 전달받는 객체</li>
</ul>
<pre><code class="language-text">Subject
   │
   │ 상태 변경
   ↓
notify()
   │
   ├──→ Observer A
   ├──→ Observer B
   └──→ Observer C</code></pre>
<h3 id="예시-sns-팔로우">예시: SNS 팔로우</h3>
<p>어떤 사용자를 팔로우한 상황</p>
<pre><code class="language-text">사용자(Subject)
     │
     │ 게시물 업로드
     ↓
   알림
  ↙  ↓  ↘
팔로워 A  B  C</code></pre>
<ul>
<li>사용자가 게시물을 등록하면 상태 변화 발생</li>
<li>해당 사용자를 팔로우하고 있는 팔로워들에게 알림 전달</li>
<li>팔로워들은 알림을 받은 후 각각 필요한 동작 수행</li>
</ul>
<blockquote>
<p>Subject와 Observer를 반드시 별도의 객체로 명확히 구분해야 하는 것은 아니며, 구현 방식에 따라 상태를 가진 객체가 직접 옵저버들을 관리할 수도 있음.</p>
</blockquote>
<h3 id="옵저버-패턴과-mvc">옵저버 패턴과 MVC</h3>
<p>옵저버 패턴은 <strong>이벤트 기반 시스템이나 MVC와 같은 구조에서 활용될 수 있음</strong></p>
<pre><code class="language-text">Model
  │
  │ 상태 변경
  ↓
notify / update
  │
  ↓
View</code></pre>
<ul>
<li><code>Model</code>의 데이터 변경</li>
<li>등록된 <code>View</code>에 변경 사실 전달</li>
<li><code>View</code>가 새로운 상태를 기반으로 화면 갱신</li>
</ul>
<p>※ MVC 전체가 옵저버 패턴인 것은 아니며, <strong>Model의 변경을 View에 전달하는 부분에서 옵저버 패턴을 활용할 수 있음</strong></p>
<h3 id="한-줄-정리-1">한 줄 정리</h3>
<p><strong>한 객체의 상태가 변하면 해당 객체를 관찰하고 있는 여러 객체에게 자동으로 변화를 전달하는 패턴</strong></p>
<h3 id="예시-코드-2">예시 코드</h3>
<h4 id="자바에서의-옵저버-패턴">자바에서의 옵저버 패턴</h4>
<pre><code class="language-java">interface Subject {
    void register(Observer obj);
    void unregister(Observer obj);
    void notifyObservers();
    Object getUpdate(Observer obj);
}

interface Observer {
    void update();
}</code></pre>
<ul>
<li><code>Subject</code> → 옵저버 등록·삭제·알림을 담당하는 주체 인터페이스</li>
<li><code>Observer</code> → 상태 변경 알림을 받기 위한 인터페이스</li>
<li><code>update()</code> → 주체의 상태가 변경되었을 때 옵저버가 실행하는 메서드</li>
</ul>
<pre><code class="language-java">class Topic implements Subject {
    private List&lt;Observer&gt; observers;
    private String message;

    @Override
    public void register(Observer obj) {
        if (!observers.contains(obj))
            observers.add(obj);
    }

    @Override
    public void unregister(Observer obj) {
        observers.remove(obj);
    }

    @Override
    public void notifyObservers() {
        observers.forEach(Observer::update);
    }

    @Override
    public Object getUpdate(Observer obj) {
        return this.message;
    }

    public void postMessage(String msg) {
        this.message = msg;
        notifyObservers();
    }
}</code></pre>
<ul>
<li><code>Topic</code> → 상태가 변경되는 <strong>Subject</strong></li>
<li><code>observers</code> → 현재 등록된 옵저버 목록</li>
<li><code>register()</code> → 옵저버 등록</li>
<li><code>unregister()</code> → 옵저버 제거</li>
<li><code>notifyObservers()</code> → 모든 옵저버의 <code>update()</code> 호출</li>
<li><code>postMessage()</code> → 새로운 메시지 저장 후 모든 옵저버에게 변경 알림</li>
</ul>
<pre><code class="language-java">class TopicSubscriber implements Observer {
    private String name;
    private Subject topic;

    @Override
    public void update() {
        String msg = (String) topic.getUpdate(this);
        System.out.println(name + &quot;:: got message &gt;&gt; &quot; + msg);
    }
}</code></pre>
<ul>
<li><code>TopicSubscriber</code> → <code>Observer</code>를 구현한 실제 구독자</li>
<li><code>update()</code> 호출 시 <code>Topic</code>의 최신 메시지 조회</li>
</ul>
<h4 id="실행-흐름">실행 흐름</h4>
<pre><code class="language-java">Topic topic = new Topic();

Observer a = new TopicSubscriber(&quot;a&quot;, topic);
Observer b = new TopicSubscriber(&quot;b&quot;, topic);
Observer c = new TopicSubscriber(&quot;c&quot;, topic);

topic.register(a);
topic.register(b);
topic.register(c);

topic.postMessage(&quot;amumu is op champion!!&quot;);</code></pre>
<pre><code class="language-text">Topic에 메시지 등록
        ↓
notifyObservers()
        ↓
 ┌──────┼──────┐
 ↓      ↓      ↓
 a      b      c
update update update</code></pre>
<ul>
<li><code>a</code>, <code>b</code>, <code>c</code>를 <code>Topic</code>의 옵저버로 등록</li>
<li><code>Topic</code>에 새로운 메시지 발생</li>
<li><code>notifyObservers()</code> 실행</li>
<li>등록된 모든 옵저버의 <code>update()</code> 호출</li>
<li>각 옵저버가 변경된 메시지 확인</li>
</ul>
<blockquote>
<p><strong>핵심:</strong> <code>Topic</code>의 상태가 변경되면 등록된 모든 <code>TopicSubscriber</code>에게 자동으로 변경 사실 전달</p>
</blockquote>
<hr>
<h3 id="상속과-구현">상속과 구현</h3>
<h4 id="상속-extends">상속 <code>extends</code></h4>
<ul>
<li><strong>부모 클래스의 변수·메서드를 자식 클래스가 물려받아 사용하는 방식</strong></li>
<li>일반 클래스 또는 추상 클래스 기반</li>
<li>기존 기능을 재사용하면서 추가 기능 확장 가능</li>
</ul>
<pre><code class="language-java">class Child extends Parent {
}</code></pre>
<h4 id="구현-implements">구현 <code>implements</code></h4>
<ul>
<li><strong>인터페이스에서 정의한 메서드를 클래스가 직접 구현하는 방식</strong></li>
<li>인터페이스는 &quot;어떤 기능을 해야 하는지&quot;에 대한 규칙 제공</li>
<li>구현 클래스는 해당 메서드의 실제 동작 정의</li>
</ul>
<pre><code class="language-java">class Topic implements Subject {
}</code></pre>
<blockquote>
<p>옵저버 예제에서는 <code>Topic implements Subject</code>, <code>TopicSubscriber implements Observer</code> 형태로 <strong>인터페이스를 구현</strong></p>
</blockquote>
<hr>
<h3 id="자바스크립트에서의-옵저버-패턴">자바스크립트에서의 옵저버 패턴</h3>
<ul>
<li>자바스크립트에서는 <strong><code>Proxy</code> 객체를 이용해 객체의 변화를 감지</strong>하는 방식으로 구현 가능</li>
<li>객체의 속성 조회·변경 등의 동작을 중간에서 가로채어 추가 로직 수행 가능</li>
</ul>
<h3 id="proxy-객체">Proxy 객체</h3>
<pre><code class="language-javascript">const p = new Proxy(target, handler);</code></pre>
<ul>
<li><code>target</code> → Proxy가 감싸는 원본 객체</li>
<li><code>handler</code> → 원본 객체의 동작을 가로챘을 때 수행할 로직</li>
</ul>
<h4 id="주요-트랩trap">주요 트랩(Trap)</h4>
<ul>
<li><code>get()</code> → 속성 조회 가로채기</li>
<li><code>set()</code> → 속성 변경 가로채기</li>
<li><code>has()</code> → <code>in</code> 연산자 사용 가로채기</li>
</ul>
<p>예를 들어,</p>
<pre><code class="language-javascript">const handler = {
    get: function(target, name) {
        return name === &#39;name&#39;
            ? `${target.a} ${target.b}`
            : target[name];
    }
};

const p = new Proxy(
    { a: &#39;KUNDOL&#39;, b: &#39;IS AMUMU ZANGIN&#39; },
    handler
);

console.log(p.name);</code></pre>
<pre><code class="language-text">KUNDOL IS AMUMU ZANGIN</code></pre>
<ul>
<li>실제 객체에는 <code>name</code> 속성이 존재하지 않음</li>
<li><code>p.name</code> 접근 발생</li>
<li>Proxy의 <code>get()</code>이 접근을 가로챔</li>
<li><code>a</code>, <code>b</code> 값을 이용해 새로운 값 반환</li>
</ul>
<hr>
<h3 id="proxy-객체를-이용한-옵저버-패턴">Proxy 객체를 이용한 옵저버 패턴</h3>
<pre><code class="language-javascript">function createReactiveObject(target, callback) {
    const proxy = new Proxy(target, {
        set(obj, prop, value) {
            if (value !== obj[prop]) {
                const prev = obj[prop];
                obj[prop] = value;

                callback(
                    `${prop}가 [${prev}] &gt;&gt; [${value}] 로 변경되었습니다.`
                );
            }
            return true;
        }
    });

    return proxy;
}</code></pre>
<p>사용 예시</p>
<pre><code class="language-javascript">const a = {
    &quot;형규&quot;: &quot;솔로&quot;
};

const b = createReactiveObject(a, console.log);

b.형규 = &quot;솔로&quot;;
b.형규 = &quot;커플&quot;;</code></pre>
<pre><code class="language-text">형규가 [솔로] &gt;&gt; [커플] 로 변경되었습니다.</code></pre>
<h4 id="동작-흐름-1">동작 흐름</h4>
<pre><code class="language-text">b.형규 값 변경
      ↓
Proxy의 set() 호출
      ↓
기존 값과 새로운 값 비교
      ↓
값이 변경된 경우 callback 실행
      ↓
변경 사실 전달</code></pre>
<ul>
<li>동일한 값을 다시 대입 → 별도 동작 없음</li>
<li>다른 값을 대입 → <code>set()</code>이 변경 감지</li>
<li>변경 발생 시 콜백 함수를 통해 알림</li>
</ul>
<blockquote>
<p><strong>옵저버 패턴과의 연결:</strong> 객체의 상태 변화를 감지하고, 변화가 발생했을 때 등록된 동작을 실행한다는 점에서 옵저버 패턴과 유사한 구조</p>
</blockquote>
<hr>
<h3 id="vuejs-30의-옵저버-패턴">Vue.js 3.0의 옵저버 패턴</h3>
<ul>
<li>Vue.js 3의 반응형 시스템에서 <strong><code>Proxy</code>를 활용하여 객체의 상태 변화를 감지</strong></li>
<li><code>reactive()</code> 등으로 생성한 반응형 데이터가 변경되면 관련된 화면도 자동 갱신</li>
</ul>
<pre><code class="language-text">데이터 변경
   ↓
Proxy가 변경 감지
   ↓
관련 반응형 효과 실행
   ↓
DOM 갱신</code></pre>
<ul>
<li><code>get()</code> → 어떤 값이 사용되는지 추적하는 데 활용</li>
<li><code>set()</code> → 값의 변경 감지에 활용</li>
<li>데이터와 화면을 연결하여 <strong>상태 변경 → 화면 갱신</strong> 구조 구현</li>
</ul>
<blockquote>
<p><strong>DOM(Document Object Model)</strong>
웹 페이지를 구성하는 HTML 요소들을 객체 형태로 표현한 구조</p>
</blockquote>
<p>※ 정확히 말하면 Vue 3의 반응성 시스템 전체를 단순한 GoF 옵저버 패턴 하나로만 설명하기는 어렵고, <strong>Proxy를 통한 변경 감지와 의존성 추적·효과 실행이 결합된 반응형 시스템</strong>으로 이해하는 것이 더 정확함.</p>
<hr>
<h3 id="한-줄-정리-2">한 줄 정리</h3>
<p><strong>옵저버 패턴은 Subject의 상태 변화를 감지하여 관련 Observer들에게 자동으로 알리고, Java에서는 인터페이스와 옵저버 목록으로, JavaScript에서는 Proxy 등을 활용해 비슷한 반응형 구조를 구현할 수 있음.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CS 면접 바이블: Chapter 1 (2)]]></title>
            <link>https://velog.io/@jung_ji_in02/CS-%EB%A9%B4%EC%A0%91-%EB%B0%94%EC%9D%B4%EB%B8%94-Chapter-2</link>
            <guid>https://velog.io/@jung_ji_in02/CS-%EB%A9%B4%EC%A0%91-%EB%B0%94%EC%9D%B4%EB%B8%94-Chapter-2</guid>
            <pubDate>Fri, 07 Aug 2026 05:17:08 GMT</pubDate>
            <description><![CDATA[<p>이 내용은 지난 장의 내용을 이어서 정리한 내용이다.</p>
<hr>
<h2 id="112-팩토리-패턴">1.1.2 팩토리 패턴</h2>
<p>팩토리 패턴(Factory Pattern)은 <strong>객체를 사용하는 코드와 객체를 생성하는 코드를 분리하는 디자인 패턴</strong>이다.</p>
<p>즉, 객체를 직접 생성하지 않고 별도의 팩토리를 통해 객체를 생성하도록 구성한다.</p>
<p>특히 상속 관계를 이용하는 팩토리 메서드 패턴에서는</p>
<ul>
<li>상위 클래스가 객체 생성의 전체적인 구조를 정의</li>
<li>하위 클래스가 실제로 어떤 객체를 생성할지 결정</li>
</ul>
<p>하는 방식으로 구성된다.</p>
<pre><code class="language-text">객체를 사용하는 코드
        ↓
     팩토리
        ↓
어떤 객체를 만들지 결정
        ↓
구체적인 객체 생성</code></pre>
<p>상위 클래스와 구체적인 객체 생성 로직이 분리되므로 서로 간의 의존성이 줄어들고 <strong>느슨한 결합(Loose Coupling)</strong>을 만들 수 있다.</p>
<hr>
<h3 id="상위-클래스">상위 클래스</h3>
<p>상위 클래스는 객체 생성 과정의 전체적인 뼈대를 담당한다.</p>
<ul>
<li>구체적으로 어떤 클래스의 인스턴스를 생성하는지 알 필요가 없음</li>
<li>객체 생성 방식이 변경되더라도 사용하는 코드의 변경을 최소화할 수 있음</li>
<li>객체 생성 로직을 한곳에서 관리할 수 있어 유지보수가 쉬움</li>
<li>새로운 객체 종류가 추가되어도 기존 코드에 미치는 영향을 줄일 수 있음</li>
</ul>
<p>예를 들어 커피를 만드는 시스템이라면 상위 클래스는 <strong>커피를 주문받고 생성하는 전체적인 과정</strong>을 담당한다.</p>
<hr>
<h3 id="하위-클래스">하위 클래스</h3>
<p>하위 클래스는 <strong>실제로 어떤 객체를 생성할지 결정하고 구체적인 생성 로직을 구현한다.</strong></p>
<p>예를 들어 다음과 같이 나눌 수 있다.</p>
<pre><code class="language-text">LatteFactory
    ↓
Latte 생성

EspressoFactory
    ↓
Espresso 생성</code></pre>
<p>즉,</p>
<ul>
<li>상위 클래스 → 객체 생성의 전체적인 구조</li>
<li>하위 클래스 → 실제로 생성할 객체와 구체적인 생성 방법</li>
</ul>
<p>을 담당한다.</p>
<hr>
<h3 id="커피-공장으로-이해하기">커피 공장으로 이해하기</h3>
<p>커피 공장에 비유하면 다음과 같다.</p>
<pre><code class="language-text">커피 주문
   ↓
CoffeeFactory
   ↓
어떤 커피인가?
   ↓
 ┌───────────────┐
 │               │
LatteFactory   EspressoFactory
 │               │
Latte 생성     Espresso 생성</code></pre>
<p><code>CoffeeFactory</code>는 전체 생산 공정을 관리하고, <code>LatteFactory</code>, <code>EspressoFactory</code>는 각각 어떤 커피를 실제로 만들 것인지 결정한다.</p>
<hr>
<h2 id="자바스크립트에서-객체-생성">자바스크립트에서 객체 생성</h2>
<p>자바스크립트의 <code>Object</code> 생성자는 전달받은 값에 따라 서로 다른 Wrapper 객체를 생성할 수 있다.</p>
<pre><code class="language-javascript">const num = new Object(42)
const str = new Object(&#39;abc&#39;)

console.log(num.constructor.name) // Number
console.log(str.constructor.name) // String</code></pre>
<p>숫자를 전달하면 <code>Number</code> 객체가 생성되고, 문자열을 전달하면 <code>String</code> 객체가 생성된다.</p>
<p>즉, 입력값에 따라 생성되는 객체의 타입이 달라질 수 있다는 점에서 팩토리와 비슷한 객체 생성 방식을 확인할 수 있다.</p>
<p>다만 이것을 일반적인 의미의 <strong>팩토리 메서드 패턴 자체라고 보는 것은 정확하지 않고</strong>, 객체 생성 로직을 내부에서 결정하는 예시 정도로 이해하는 것이 좋다.</p>
<hr>
<h2 id="커피-팩토리-예제">커피 팩토리 예제</h2>
<pre><code class="language-javascript">class CoffeeFactory {
  static createCoffee(type) {
    const factory = factoryList[type]
    return factory.createCoffee()
  }
}

class Latte {
  constructor() {
    this.name = &quot;latte&quot;
  }
}

class Espresso {
  constructor() {
    this.name = &quot;espresso&quot;
  }
}

class LatteFactory extends CoffeeFactory {
  static createCoffee() {
    return new Latte()
  }
}

class EspressoFactory extends CoffeeFactory {
  static createCoffee() {
    return new Espresso()
  }
}

const factoryList = {
  LatteFactory,
  EspressoFactory
}

const main = () =&gt; {
  const coffee = CoffeeFactory.createCoffee(&quot;LatteFactory&quot;)

  console.log(coffee.name) // latte
}

main()</code></pre>
<p>전체 흐름은 다음과 같다.</p>
<pre><code class="language-text">CoffeeFactory.createCoffee(&quot;LatteFactory&quot;)
                ↓
factoryList에서 LatteFactory 선택
                ↓
LatteFactory.createCoffee()
                ↓
new Latte()
                ↓
Latte 객체 반환</code></pre>
<p><code>CoffeeFactory</code>는 어떤 팩토리를 사용할지 결정하는 역할을 하고, <code>LatteFactory</code>와 <code>EspressoFactory</code>는 실제 객체 생성 방법을 담당한다.</p>
<p>따라서 객체를 사용하는 쪽에서는 직접 다음과 같이 작성할 필요가 없다.</p>
<pre><code class="language-javascript">const coffee = new Latte()</code></pre>
<p>대신 팩토리를 통해 객체를 생성한다.</p>
<pre><code class="language-javascript">const coffee = CoffeeFactory.createCoffee(&quot;LatteFactory&quot;)</code></pre>
<p>이를 통해 객체 생성 로직과 객체를 사용하는 로직을 분리할 수 있다.</p>
<hr>
<h2 id="static-메서드">static 메서드</h2>
<p><code>CoffeeFactory</code>의 <code>createCoffee()</code>는 <code>static</code>으로 선언되어 있다.</p>
<pre><code class="language-javascript">static createCoffee(type) {
  ...
}</code></pre>
<p>정적 메서드는 클래스의 인스턴스를 생성하지 않고도 클래스 자체에서 직접 호출할 수 있다.</p>
<pre><code class="language-javascript">CoffeeFactory.createCoffee(&quot;LatteFactory&quot;)</code></pre>
<p>따라서 다음과 같이 객체를 먼저 생성할 필요가 없다.</p>
<pre><code class="language-javascript">const factory = new CoffeeFactory()
factory.createCoffee(...)</code></pre>
<p>즉, 이 예제에서 <code>static</code>을 사용하는 핵심 이유는 <strong>팩토리 객체를 별도로 생성하지 않고 객체 생성 기능을 바로 사용할 수 있도록 하기 위함</strong>이다.</p>
<blockquote>
<p><code>static</code> 메서드의 핵심은 클래스의 인스턴스가 아니라 클래스 자체에 속한 메서드라는 점이다.</p>
</blockquote>
<hr>
<h2 id="의존성-주입과의-차이">의존성 주입과의 차이</h2>
<p>팩토리 패턴과 의존성 주입(Dependency Injection)은 서로 관련될 수 있지만 동일한 개념은 아니다.</p>
<p>팩토리 패턴은</p>
<blockquote>
<p><strong>객체를 어떻게 생성할 것인가</strong></p>
</blockquote>
<p>에 초점을 둔다.</p>
<p>반면 의존성 주입은</p>
<blockquote>
<p><strong>객체가 필요로 하는 다른 객체를 외부에서 어떻게 전달할 것인가</strong></p>
</blockquote>
<p>에 초점을 둔다.</p>
<p>따라서 위 예제는 기본적으로 <strong>팩토리를 이용한 객체 생성 패턴</strong>으로 보는 것이 적절하며, 단순히 <code>factoryList</code>를 사용했다는 이유만으로 의존성 주입이라고 보기는 어렵다.</p>
<hr>
<h3 id="자바의-팩토리-패턴">자바의 팩토리 패턴</h3>
<pre><code class="language-java">enum CoffeeType {
    LATTE,
    ESPRESSO
}

abstract class Coffee {
    protected String name;

    public String getName() {
        return name;
    }
}

class Latte extends Coffee {
    public Latte() {
        name = &quot;Latte&quot;;
    }
}

class Espresso extends Coffee {
    public Espresso() {
        name = &quot;Espresso&quot;;
    }
}

class CoffeeFactory {
    public static Coffee createCoffee(CoffeeType type) {
        switch (type) {
            case LATTE:
                return new Latte();
            case ESPRESSO:
                return new Espresso();
            default:
                throw new IllegalArgumentException(&quot;Invalid coffee type: &quot; + type);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE);
        System.out.println(coffee.getName()); // Latte
    }
}</code></pre>
<ul>
<li><code>Coffee</code> → 공통 상위 클래스</li>
<li><code>Latte</code>, <code>Espresso</code> → 실제 생성되는 객체</li>
<li><code>CoffeeFactory</code> → 전달받은 타입에 따라 적절한 객체 생성</li>
<li><code>Main</code> → 객체를 직접 <code>new</code>하지 않고 팩토리를 통해 생성</li>
</ul>
<pre><code class="language-text">CoffeeType.LATTE
      ↓
CoffeeFactory
      ↓
new Latte()
      ↓
Latte 객체 반환</code></pre>
<blockquote>
<p>객체 생성 로직을 <code>CoffeeFactory</code>에 분리하여 객체를 사용하는 코드와 생성하는 코드의 결합도를 낮춘다.</p>
</blockquote>
<p>※ 이 코드는 엄밀히 말하면 <strong>Simple Factory</strong> 형태에 가깝다.</p>
<blockquote>
<h3 id="enum">Enum</h3>
</blockquote>
<ul>
<li>enum은 서로 관련된 상수들을 하나의 타입으로 묶어 관리하는 자료형이다.</li>
<li>정해진 값 중 하나만 사용할 수 있어 잘못된 값이 들어가는 것을 방지할 수 있다.
ex) LATTE, ESPRESSO처럼 선택 가능한 종류가 제한된 경우에 사용한다.</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[CS 면접 바이블: Chapter 1]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%A9%B4%EC%A0%91%EC%9D%84-%EC%9C%84%ED%95%9C-CS-%EC%8A%A4%ED%84%B0%EB%94%94</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%A9%B4%EC%A0%91%EC%9D%84-%EC%9C%84%ED%95%9C-CS-%EC%8A%A4%ED%84%B0%EB%94%94</guid>
            <pubDate>Wed, 05 Aug 2026 05:04:25 GMT</pubDate>
            <description><![CDATA[<p>이 정리는 나중에 내가 확인하기 위한 정리이다.</p>
<p>그래서 오로지 나를 위해서 내가 알아볼 수 있도록 정리할 것이며, 내가 이해하기 쉬운 방법으로 정리해보려고한다.</p>
<p>내가 그렇게 정리에 능통하지 않아서, 내가 먼저 정리한 것을 AI의 도움을 받아 첨삭할 예정이다.</p>
<p>다만 모든 글쓰기를 AI에게 맡기는 것이 아닌, 무조건 내가 먼저 정리한 후에 그 정리본을 AI를 통해 좀 더 보기 쉽게 정리할 예정이다.</p>
<hr>
<h1 id="디자인-패턴과-프로그래밍-패러다임">디자인 패턴과 프로그래밍 패러다임</h1>
<p><strong>라이브러리</strong>와 <strong>프레임워크</strong>를 제대로 사용하려면 그 기반이 되는 <strong>디자인 패턴</strong>과 <strong>프로그래밍 패러다임</strong>을 이해할 필요가 있다.</p>
<h4 id="프로그래밍-패러다임">프로그래밍 패러다임</h4>
<ul>
<li>프로그램의 데이터와 로직을 어떤 관점에서 구성할 것인지에 대한 전반적인 사고방식</li>
</ul>
<h4 id="디자인-패턴">디자인 패턴</h4>
<ul>
<li>프로그램을 설계하면서 반복적으로 발생하는 문제를 해결하기 위해 정리해놓은 재사용 가능한 설계 방법</li>
</ul>
<hr>
<h3 id="라이브러리">라이브러리</h3>
<ul>
<li><p>라이브러리는 특정 기능을 미리 구현해놓은 코드의 모음이다.</p>
</li>
<li><p>일종의 공구함이라고 생각하면 된다. 개발자가 기능이 필요한 시점에 라이브러리를 직접 호출하여 사용한다.</p>
</li>
<li><p>예시는 다음과 같다.</p>
<pre><code>날짜 및 시간 처리
수학 계산
HTTP 통신
JSON 변환
파일 처리</code></pre></li>
<li><p>Java에서는 java.time, Math, Jackson 등이 라이브러리 또는 API의 예시에 해당한다.</p>
<pre><code>int result = Math.max(10, 20);</code></pre></li>
<li><p>위 코드에서는 개발자가 필요한 시점에 Math.max()를 직접 호출한다.</p>
</li>
</ul>
<blockquote>
<p>라이브러리에서는 개발자가 전체 실행 흐름을 관리하고, 필요한 기능을 직접 호출한다.</p>
</blockquote>
<h3 id="프레임워크">프레임워크</h3>
<ul>
<li>프레임워크는 프로그램을 만들기 위한 전체적인 구조와 실행 흐름을 제공한다.</li>
<li>건물의 골조뿐만 아니라 건축 순서와 작업 규칙까지 어느 정도 정해놓은 시스템이라고 생각하면 된다.</li>
<li>프레임워크는 일반적으로 다음과 같은 내용을 제공한다.</li>
<li>프로그램의 전체 실행 구조<pre><code>코드가 작성될 위치와 형식
객체의 생성과 관리
요청 및 응답 처리
데이터베이스 접근 방식
보안과 예외 처리
각 기능이 실행되는 순서</code></pre></li>
<li>개발자는 프레임워크가 정한 구조와 규칙에 맞추어 필요한 코드를 작성한다.</li>
<li>예를 들어 Spring에서는 개발자가 컨트롤러 메서드를 작성해두면, HTTP 요청이 들어왔을 때 Spring이 해당 메서드를 찾아 실행한다.<pre><code>@GetMapping(&quot;/users&quot;)
public List&lt;User&gt; getUsers() {
  return userService.findAll();
}</code></pre></li>
<li>이처럼 프레임워크가 전체 실행 흐름을 관리하고 개발자의 코드를 호출하는 것을 제어의 역전이라고 한다.</li>
</ul>
<blockquote>
<p>라이브러리는 내가 호출하지만, 프레임워크는 내 코드를 호출한다.</p>
</blockquote>
<hr>
<h2 id="11-디자인-패턴">1.1 디자인 패턴</h2>
<p>디자인 패턴은 소프트웨어를 설계할 때 반복적으로 발생하는 문제를 효과적으로 해결하기 위해 정리해놓은 <strong>재사용 가능한 설계 방법</strong>이다.</p>
<p>디자인 패턴은 완성된 코드나 <strong>반드시 지켜야 하는 문법이 아니다</strong>. 특정 문제를 해결하기 위해 객체에 어떤 역할을 부여하고, 객체 사이의 관계를 어떻게 구성할 것인지 설명하는 <strong>설계 아이디어에 가깝다.</strong></p>
<p>일반적으로 디자인 패턴에는 다음 내용이 포함된다.</p>
<pre><code>어떤 상황에서 발생하는 문제인지
어떤 객체가 필요한지
객체들이 어떤 역할을 담당하는지
객체들이 어떻게 상호작용하는지
해당 구조의 장점과 단점은 무엇인지</code></pre><p>예를 들어 싱글턴 패턴은 프로그램 전체에서 하나의 객체만 존재해야 하는 문제를 해결하기 위한 설계 방법이다.</p>
<p>즉, 디자인 패턴은 과거의 개발자들이 반복적으로 경험한 설계 문제와 해결 방법을 정리해놓은 <strong>일종의 설계 참고서</strong>라고 볼 수 있다.</p>
<h3 id="111-싱글톤-패턴">1.1.1 싱글톤 패턴</h3>
<p>싱글톤 패턴은 특정 클래스의 인스턴스가 애플리케이션 전체에서 단 하나만 생성되도록 제한하고, 필요한 곳에서 동일한 인스턴스를 공유하도록 만드는 디자인 패턴이다.</p>
<p>일반적인 클래스는 하나의 클래스를 기반으로 여러 인스턴스를 생성할 수 있다.</p>
<pre><code class="language-javascript">const user1 = new User(); 
const user2 = new User();</code></pre>
<p>이 경우 user1과 user2는 같은 클래스로 생성되었지만 서로 다른 인스턴스이다.</p>
<p>반면 싱글톤 패턴에서는 클래스를 여러 번 호출하더라도 새로운 인스턴스를 계속 생성하지 않고, 처음 만들어진 인스턴스를 반환한다.</p>
<p>싱글톤 패턴은 다음과 같이 생성 비용이 크거나 애플리케이션 전체에서 동일한 상태를 공유해야 하는 객체에 주로 사용된다.</p>
<ul>
<li>데이터베이스 커넥션 풀</li>
<li>애플리케이션 설정 객체</li>
<li>로거</li>
<li>캐시 관리자</li>
<li>외부 API 클라이언트</li>
</ul>
<p>데이터베이스의 경우 실제로는 하나의 데이터베이스 연결만 공유하기보다, 여러 연결을 관리하는 커넥션 풀 객체를 하나 생성하여 공유하는 경우가 많다.</p>
<h4 id="장점">장점</h4>
<ul>
<li>애플리케이션 전체에서 동일한 인스턴스 공유</li>
<li>불필요한 객체 중복 생성 방지</li>
<li>생성 비용이 큰 객체의 초기화 비용 절감</li>
<li>공통 상태나 자원의 일관된 관리</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>여러 모듈이 하나의 객체에 강하게 의존</li>
<li>전역 상태로 사용될 가능성</li>
<li>한 곳의 상태 변경이 다른 코드에 영향을 줄 가능성</li>
<li>단위 테스트 시 가짜 객체로 대체하기 어려움</li>
<li>객체 사이의 의존 관계가 명확하게 드러나지 않을 수 있음</li>
</ul>
<blockquote>
<p>즉, 싱글톤 패턴은 객체를 하나만 만든다는 장점이 있지만, 지나치게 사용하면 코드 사이의 결합도가 높아질 수 있다.</p>
</blockquote>
<hr>
<h3 id="자바스크립트-객체의-비교">자바스크립트 객체의 비교</h3>
<p>자바스크립트에서 객체는 내부 값이 같더라도 각각 별도의 메모리 공간에 생성된다.</p>
<pre><code class="language-javascript">const obj1 = {
    a: 27
};

const obj2 = {
    a: 27
};

console.log(obj1 === obj2); // false</code></pre>
<p>obj1과 obj2는 내부 값은 같지만 서로 다른 위치에 생성된 별개의 객체이므로 false가 출력된다.</p>
<p>반면 같은 객체를 다른 변수에 할당하면 두 변수는 동일한 객체를 참조한다.</p>
<pre><code class="language-javascript">const obj1 = { a: 27 }; 
const obj2 = obj1; 

console.log(obj1 === obj2); // true</code></pre>
<hr>
<h3 id="자바스크립트의-싱글톤-패턴">자바스크립트의 싱글톤 패턴</h3>
<p>싱글톤 클래스는 인스턴스가 아직 존재하지 않을 때만 새로운 인스턴스를 생성하고, 이미 인스턴스가 존재한다면 기존 인스턴스를 반환하도록 구성할 수 있다.</p>
<pre><code class="language-javascript">class Singleton {
    constructor() { 
        if (Singleton.instance) { 
            return Singleton.instance; 
        } 

        Singleton.instance = this; 
    } 
} 

const a = new Singleton(); 
const b = new Singleton(); 

console.log(a === b); // true</code></pre>
<p>동작 과정은 다음과 같다.</p>
<pre><code>1. new Singleton()을 처음 호출
2. Singleton.instance가 존재하지 않으므로 새로운 인스턴스 생성
3. 생성된 인스턴스를 Singleton.instance에 저장
4. 두 번째로 new Singleton() 호출
5. 이미 저장된 Singleton.instance 반환
6. a와 b가 동일한 인스턴스를 참조</code></pre><p>다만 생성자를 계속 호출하는 방식보다, 인스턴스를 가져오는 정적 메서드를 별도로 두는 방식이 의도를 더 명확하게 표현할 수 있다.</p>
<pre><code class="language-javascript">class Singleton { 
    static instance; 

    static getInstance() { 
        if (!Singleton.instance) { 
            Singleton.instance = new Singleton(); 
        }

        return Singleton.instance; 
    } 
} 

const a = Singleton.getInstance(); 
const b = Singleton.getInstance(); 

console.log(a === b); // true</code></pre>
<p>이 코드에서는 getInstance()가 다음 역할을 한다.</p>
<ul>
<li>기존 인스턴스가 없으면 새로운 인스턴스 생성</li>
<li>기존 인스턴스가 있으면 해당 인스턴스 반환</li>
<li>호출하는 모든 코드에 동일한 인스턴스 제공</li>
</ul>
<p>따라서 싱글톤 패턴의 핵심은 단순히 객체 하나를 만드는 것이 아니라, 인스턴스 생성을 통제하고 동일한 인스턴스에 접근하도록 만드는 것이다.</p>
<hr>
<h3 id="데이터베이스-연결-모듈에서의-싱글톤-패턴">데이터베이스 연결 모듈에서의 싱글톤 패턴</h3>
<p>데이터베이스 연결 객체는 생성 비용이 크거나 애플리케이션 전체에서 공유해야 하는 경우가 많다.</p>
<p>따라서 데이터베이스 연결을 관리하는 객체를 매번 새롭게 생성하지 않고, 하나의 객체를 생성한 뒤 여러 모듈에서 공유하도록 싱글톤 패턴을 적용할 수 있다.</p>
<pre><code class="language-javascript">const URL = &quot;mongodb://localhost:27017/kundolapp&quot;;

const createConnection = (url) =&gt; ({
    url,
    connected: true
});

class DB {
    constructor(url) {
        if (!DB.instance) {
            DB.instance = createConnection(url);
        }

        return DB.instance;
    }
}

const a = new DB(URL);
const b = new DB(URL);

console.log(a === b); // true</code></pre>
<p>동작 과정은 다음과 같다.</p>
<ol>
<li><code>new DB(URL)</code>을 처음 호출한다.</li>
<li><code>DB.instance</code>가 존재하지 않으므로 데이터베이스 연결 객체를 생성한다.</li>
<li>생성된 연결 객체를 <code>DB.instance</code>에 저장한다.</li>
<li>두 번째로 <code>new DB(URL)</code>을 호출한다.</li>
<li>이미 생성된 <code>DB.instance</code>를 반환한다.</li>
<li>따라서 <code>a</code>와 <code>b</code>는 동일한 객체를 참조한다.</li>
</ol>
<pre><code class="language-javascript">console.log(a === b); // true</code></pre>
<p>즉, <code>DB.instance</code>에 저장된 하나의 데이터베이스 연결 객체를 <code>a</code>와 <code>b</code>가 함께 사용한다.</p>
<p>이를 통해 데이터베이스 연결 객체를 불필요하게 여러 번 생성하는 것을 방지할 수 있다.</p>
<p>다만 위 코드에서 <code>DB.instance</code>는 <code>DB</code> 클래스의 인스턴스가 아니라 <code>createConnection()</code>이 반환한 연결 객체이다.</p>
<p>자바스크립트 생성자에서 객체를 명시적으로 반환하면 새롭게 생성된 <code>this</code> 대신 반환한 객체가 <code>new</code> 표현식의 결과가 되기 때문이다.</p>
<p>따라서 다음과 같은 <code>DB</code> 클래스의 메서드는 사용할 수 없다.</p>
<pre><code class="language-javascript">class DB {
    constructor(url) {
        if (!DB.instance) {
            DB.instance = createConnection(url);
        }

        return DB.instance;
    }

    connect() {
        return DB.instance;
    }
}</code></pre>
<p><code>new DB()</code>의 결과가 <code>DB</code> 객체가 아니라 연결 객체이므로 <code>connect()</code> 메서드를 가지고 있지 않기 때문이다.</p>
<pre><code class="language-javascript">const db = new DB(URL);

db.connect();
// TypeError: db.connect is not a function</code></pre>
<p><code>DB</code> 클래스 자체를 하나의 인스턴스로 유지하고 싶다면 다음과 같이 작성할 수 있다.</p>
<pre><code class="language-javascript">const URL = &quot;mongodb://localhost:27017/kundolapp&quot;;

const createConnection = (url) =&gt; ({
    url,
    connected: true
});

class DB {
    constructor(url) {
        if (DB.instance) {
            return DB.instance;
        }

        this.connection = createConnection(url);
        DB.instance = this;
    }

    connect() {
        return this.connection;
    }
}

const a = new DB(URL);
const b = new DB(URL);

console.log(a === b); // true
console.log(a.connect() === b.connect()); // true</code></pre>
<p>이 구조에서는 다음 두 객체가 구분된다.</p>
<ul>
<li><code>DB.instance</code>: 데이터베이스 연결을 관리하는 <code>DB</code> 클래스의 인스턴스</li>
<li><code>this.connection</code>: 실제 데이터베이스 연결 객체</li>
</ul>
<p>따라서 <code>a</code>와 <code>b</code>는 동일한 <code>DB</code> 인스턴스를 가리키며, <code>connect()</code>를 통해 동일한 연결 객체에 접근한다.</p>
<blockquote>
<p>실제 서버에서는 데이터베이스 연결 하나만 공유하기보다, 여러 연결을 관리하는 데이터베이스 클라이언트 또는 커넥션 풀 객체를 하나 생성하여 공유하는 경우가 일반적이다.</p>
</blockquote>
<hr>
<h3 id="자바에서의-싱글톤-패턴">자바에서의 싱글톤 패턴</h3>
<p>자바에서는 중첩 정적 클래스를 활용하여 싱글톤을 구현할 수 있다.</p>
<p>이를 일반적으로 <strong>정적 내부 클래스 방식</strong> 또는 <strong>Initialization-on-demand holder idiom</strong>이라고 부른다.</p>
<pre><code class="language-java">class Singleton {

    private Singleton() {
    }

    private static class SingletonInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstanceHolder.INSTANCE;
    }
}

public class HelloWorld {

    public static void main(String[] args) {
        Singleton a = Singleton.getInstance();
        Singleton b = Singleton.getInstance();

        System.out.println(a == b); // true
    }
}</code></pre>
<h4 id="1-생성자를-private으로-선언">1. 생성자를 <code>private</code>으로 선언</h4>
<pre><code class="language-java">private Singleton() {
}</code></pre>
<p>클래스 외부에서 다음과 같이 직접 인스턴스를 생성하지 못하도록 제한한다.</p>
<pre><code class="language-java">Singleton singleton = new Singleton();
// 컴파일 오류</code></pre>
<p>생성자를 제한하지 않으면 외부에서 새로운 인스턴스를 계속 생성할 수 있으므로 싱글톤 패턴이 성립하지 않는다.</p>
<h4 id="2-정적-내부-클래스에서-인스턴스-생성">2. 정적 내부 클래스에서 인스턴스 생성</h4>
<pre><code class="language-java">private static class SingletonInstanceHolder {
    private static final Singleton INSTANCE = new Singleton();
}</code></pre>
<p><code>SingletonInstanceHolder</code> 클래스는 <code>getInstance()</code>가 처음 호출되어 실제로 사용되는 시점에 초기화된다.</p>
<p>따라서 애플리케이션이 시작되자마자 싱글톤 인스턴스를 생성하지 않고, 필요한 시점에 생성하는 <strong>지연 초기화</strong>가 가능하다.</p>
<h4 id="3-getinstance를-통해-인스턴스-반환">3. <code>getInstance()</code>를 통해 인스턴스 반환</h4>
<pre><code class="language-java">public static Singleton getInstance() {
    return SingletonInstanceHolder.INSTANCE;
}</code></pre>
<p>외부에서는 생성자를 직접 호출하지 않고 <code>getInstance()</code>를 사용하여 인스턴스를 가져온다.</p>
<pre><code class="language-java">Singleton a = Singleton.getInstance();
Singleton b = Singleton.getInstance();</code></pre>
<p>두 변수에는 동일한 인스턴스가 반환된다.</p>
<pre><code class="language-java">System.out.println(a == b); // true</code></pre>
<p>자바에서 객체에 <code>==</code> 연산자를 사용하면 객체 내부의 값을 비교하는 것이 아니라 두 변수가 같은 객체를 참조하는지 비교한다.</p>
<p>따라서 결과가 <code>true</code>라는 것은 <code>a</code>와 <code>b</code>가 메모리상 동일한 <code>Singleton</code> 인스턴스를 참조한다는 뜻이다.</p>
<hr>
<h3 id="정적-내부-클래스-방식의-장점">정적 내부 클래스 방식의 장점</h3>
<ul>
<li>인스턴스가 필요한 시점에 생성되는 지연 초기화 지원</li>
<li>클래스 초기화 과정이 JVM에 의해 안전하게 처리됨</li>
<li>별도의 <code>synchronized</code> 없이 스레드 안전성 확보</li>
<li>구현이 비교적 간단함</li>
</ul>
<p>즉, 여러 스레드가 동시에 <code>getInstance()</code>를 호출하더라도 JVM의 클래스 초기화 규칙에 따라 <code>INSTANCE</code>는 한 번만 생성된다.</p>
<p>자바스크립트와 자바 구현 모두 다음 원리를 따른다.</p>
<ol>
<li>외부에서 인스턴스가 무분별하게 생성되지 않도록 통제한다.</li>
<li>최초 요청 시 인스턴스를 하나 생성한다.</li>
<li>생성한 인스턴스를 별도의 정적 공간에 저장한다.</li>
<li>이후 요청에는 새 객체를 생성하지 않고 기존 인스턴스를 반환한다.</li>
</ol>
<blockquote>
<p>싱글톤 패턴의 핵심은 단순히 객체 하나를 만드는 것이 아니라, 객체의 생성 경로를 통제하여 항상 동일한 인스턴스를 사용하도록 만드는 것이다.</p>
</blockquote>
<hr>
<h3 id="mongoose의-싱글톤-패턴">Mongoose의 싱글톤 패턴</h3>
<p>싱글톤 패턴은 Node.js에서 MongoDB를 연결할 때 사용하는 Mongoose에서도 찾아볼 수 있다.</p>
<p>다만 정확히 말하면,</p>
<blockquote>
<p><code>mongoose.connect()</code>가 싱글톤 인스턴스를 새롭게 만들어 직접 반환하는 것이 아니라, Node.js에서 공유되는 하나의 Mongoose 객체가 기본 데이터베이스 연결을 관리하는 구조이다.</p>
</blockquote>
<hr>
<h4 id="mongoose-객체가-공유되는-과정">Mongoose 객체가 공유되는 과정</h4>
<p>Node.js에서 다음과 같이 Mongoose 모듈을 불러올 수 있다.</p>
<pre><code class="language-javascript">const mongoose = require(&quot;mongoose&quot;);</code></pre>
<p>Node.js의 CommonJS 모듈은 처음 불러온 뒤 캐시에 저장된다. 따라서 동일한 경로의 모듈을 다시 <code>require()</code>하면 모듈이 새롭게 실행되는 것이 아니라, 기존에 반환했던 것과 동일한 객체가 반환된다.</p>
<pre><code class="language-javascript">const mongooseA = require(&quot;mongoose&quot;);
const mongooseB = require(&quot;mongoose&quot;);

console.log(mongooseA === mongooseB); // true</code></pre>
<p>따라서 애플리케이션의 여러 파일에서 Mongoose를 불러오더라도 일반적으로 동일한 Mongoose 객체를 공유한다.</p>
<pre><code class="language-text">userService.js ── require(&quot;mongoose&quot;) ──┐
                                       │
orderService.js ─ require(&quot;mongoose&quot;) ──┼── 동일한 Mongoose 객체
                                       │
app.js ───────── require(&quot;mongoose&quot;) ───┘</code></pre>
<p>이러한 구조가 싱글톤 패턴과 유사한 효과를 만든다.</p>
<p>단, 프로젝트에 Mongoose가 중복으로 설치되어 서로 다른 경로에서 불러와지는 등의 특수한 상황에서는 서로 다른 객체가 반환될 수도 있다.</p>
<hr>
<h4 id="mongooseconnect의-역할"><code>mongoose.connect()</code>의 역할</h4>
<p>Mongoose에서 MongoDB에 연결할 때는 다음과 같이 <code>connect()</code>를 사용한다.</p>
<pre><code class="language-javascript">const mongoose = require(&quot;mongoose&quot;);

const uri = &quot;mongodb://127.0.0.1:27017/kundolapp&quot;;

await mongoose.connect(uri);</code></pre>
<p><code>mongoose.connect()</code>는 Mongoose가 가지고 있는 <strong>기본 연결 객체</strong>를 사용하여 MongoDB에 연결한다. 연결 상태는 다음 객체를 통해 확인할 수 있다.</p>
<pre><code class="language-javascript">mongoose.connection;</code></pre>
<p>Mongoose 공식 문서에서는 <code>mongoose.connect()</code>가 기본 Mongoose 연결을 열며, 반환된 Promise가 연결에 성공하면 기존 Mongoose 객체로 이행된다고 설명한다.</p>
<p>따라서 다음과 같이 이해해야 한다.</p>
<pre><code class="language-javascript">const result = await mongoose.connect(uri);

console.log(result === mongoose); // true</code></pre>
<p>여기서 <code>connect()</code>가 호출될 때마다 새로운 싱글톤 객체를 만드는 것이 아니다.</p>
<p>이미 공유되고 있는 <code>mongoose</code> 객체가 자신의 기본 연결인 <code>mongoose.connection</code>을 이용하여 MongoDB에 연결하고, 연결이 끝나면 자기 자신인 Mongoose 객체를 반환하는 구조이다.</p>
<pre><code class="language-text">mongoose
 ├── connect(uri)
 ├── connection       ← 기본 데이터베이스 연결
 ├── model()
 ├── Schema
 └── 기타 Mongoose 기능</code></pre>
<p>즉, Mongoose에서 싱글톤처럼 공유되는 핵심 대상은 다음과 같다.</p>
<ul>
<li>Node.js 모듈 시스템을 통해 공유되는 기본 <code>mongoose</code> 객체</li>
<li>해당 객체가 관리하는 기본 연결인 <code>mongoose.connection</code></li>
<li>기본 Mongoose 객체에 등록된 모델과 스키마</li>
</ul>
<hr>
<h3 id="uri란">URI란?</h3>
<p><code>mongoose.connect()</code>의 첫 번째 매개변수 이름은 보통 <code>uri</code>이다.</p>
<pre><code class="language-javascript">mongoose.connect(uri);</code></pre>
<p>URI는 <strong>Uniform Resource Identifier</strong>의 약자로, 특정 자원을 식별하기 위한 문자열이다.</p>
<p>쉽게 말하면 다음과 같다.</p>
<blockquote>
<p>URI는 어떤 자원을 구분하고 가리키기 위한 전체적인 식별자이다.</p>
</blockquote>
<p>RFC 3986에서는 URI를 추상적 또는 물리적 자원을 식별하는 문자들의 집합으로 정의한다.</p>
<p>MongoDB에서는 다음과 같은 연결 문자열이 URI에 해당한다.</p>
<pre><code class="language-text">mongodb://127.0.0.1:27017/kundolapp</code></pre>
<p>MongoDB 공식 문서에서도 이러한 문자열을 <strong>MongoDB 연결 URI</strong> 또는 <strong>Connection String</strong>이라고 표현한다.</p>
<hr>
<h3 id="mongodb-uri의-구조">MongoDB URI의 구조</h3>
<pre><code class="language-text">mongodb://127.0.0.1:27017/kundolapp</code></pre>
<p>각 부분을 나누면 다음과 같다.</p>
<pre><code class="language-text">mongodb://  127.0.0.1  :27017  /kundolapp
──────────  ─────────  ──────  ──────────
스킴         호스트       포트     데이터베이스</code></pre>
<h4 id="mongodb"><code>mongodb://</code></h4>
<p>자원에 어떤 방식으로 접근하는지를 나타내는 <strong>스킴(Scheme)</strong>이다.</p>
<pre><code class="language-text">http://
https://
ftp://
mongodb://
mongodb+srv://</code></pre>
<p><code>mongodb://</code>는 일반적인 MongoDB 연결에 사용하는 스킴이다.</p>
<h4 id="127001"><code>127.0.0.1</code></h4>
<p>MongoDB 서버가 실행되고 있는 호스트이다.</p>
<pre><code class="language-text">127.0.0.1</code></pre>
<p>현재 컴퓨터를 의미하는 루프백 주소이다.</p>
<h4 id="27017"><code>27017</code></h4>
<p>MongoDB가 기본적으로 사용하는 포트 번호이다.</p>
<pre><code class="language-text">27017</code></pre>
<h4 id="kundolapp"><code>/kundolapp</code></h4>
<p>접속할 기본 데이터베이스의 이름이다.</p>
<pre><code class="language-text">kundolapp</code></pre>
<p>따라서 전체 URI는 다음 의미를 가진다.</p>
<blockquote>
<p>현재 컴퓨터의 27017번 포트에서 실행 중인 MongoDB 서버에 연결하고, 기본 데이터베이스로 <code>kundolapp</code>을 사용한다.</p>
</blockquote>
<p>MongoDB의 표준 연결 URI 형식은 다음과 같이 사용자 정보, 여러 호스트와 접속 옵션 등을 추가로 포함할 수도 있다.</p>
<pre><code class="language-text">mongodb://[사용자:비밀번호@]호스트[:포트]/[데이터베이스][?옵션]</code></pre>
<p>예시는 다음과 같다.</p>
<pre><code class="language-text">mongodb://user:password@127.0.0.1:27017/kundolapp?authSource=admin</code></pre>
<hr>
<h3 id="uri와-url의-차이">URI와 URL의 차이</h3>
<p>URI와 URL은 비슷하지만 기준이 약간 다르다.</p>
<h4 id="uri">URI</h4>
<p>자원을 <strong>식별</strong>하는 전체적인 개념이다.</p>
<pre><code class="language-text">특정 자원이 무엇인지 구분하는 식별자</code></pre>
<h4 id="url">URL</h4>
<p>URL은 <strong>Uniform Resource Locator</strong>의 약자로, 자원이 어디에 있으며 어떤 방법으로 접근할 수 있는지를 나타낸다.</p>
<pre><code class="language-text">자원의 위치와 접근 방법을 나타내는 식별자</code></pre>
<p>개념적으로는 URL이 URI에 포함된다고 이해할 수 있다.</p>
<pre><code class="language-text">URI
 └── URL</code></pre>
<p>예를 들어 다음 주소는 웹 페이지의 위치와 접근 방법을 나타낸다.</p>
<pre><code class="language-text">https://example.com/users</code></pre>
<ul>
<li><code>https</code>: 접근 방법</li>
<li><code>example.com</code>: 서버의 위치</li>
<li><code>/users</code>: 서버 내부의 자원 경로</li>
</ul>
<p>따라서 URL이면서 URI이기도 하다.</p>
<p>MongoDB 연결 문자열도 서버의 위치와 접근 방법을 포함한다.</p>
<pre><code class="language-text">mongodb://127.0.0.1:27017/kundolapp</code></pre>
<p>따라서 URL과 유사한 성격을 가지지만, MongoDB와 Mongoose에서는 공식적으로 다음 표현을 주로 사용한다.</p>
<ul>
<li>MongoDB URI</li>
<li>Connection URI</li>
<li>Connection String</li>
</ul>
<p>변수 이름은 문법적으로 자유롭게 정할 수 있다.</p>
<pre><code class="language-javascript">const url = &quot;mongodb://127.0.0.1:27017/kundolapp&quot;;
mongoose.connect(url);</code></pre>
<p>다음처럼 작성해도 동일하게 동작한다.</p>
<pre><code class="language-javascript">const connectionString =
    &quot;mongodb://127.0.0.1:27017/kundolapp&quot;;

mongoose.connect(connectionString);</code></pre>
<p>그러나 Mongoose의 함수 매개변수와 공식 문서에서 <code>uri</code>라는 표현을 사용하므로, 일반적으로 다음처럼 작성한다.</p>
<pre><code class="language-javascript">const uri = &quot;mongodb://127.0.0.1:27017/kundolapp&quot;;

await mongoose.connect(uri);</code></pre>
<hr>
<h3 id="실제-연결-코드">실제 연결 코드</h3>
<pre><code class="language-javascript">const mongoose = require(&quot;mongoose&quot;);

const MONGODB_URI =
    &quot;mongodb://127.0.0.1:27017/kundolapp&quot;;

async function connectDB() {
    try {
        await mongoose.connect(MONGODB_URI);

        console.log(&quot;MongoDB 연결 성공&quot;);

        return mongoose.connection;
    } catch (error) {
        console.error(&quot;MongoDB 연결 실패:&quot;, error);
        throw error;
    }
}

module.exports = connectDB;</code></pre>
<p>다른 파일에서는 다음과 같이 사용한다.</p>
<pre><code class="language-javascript">const connectDB = require(&quot;./connectDB&quot;);

async function startServer() {
    await connectDB();

    // 데이터베이스 연결이 완료된 뒤 서버 실행
}

startServer();</code></pre>
<p>여러 파일에서 <code>require(&quot;mongoose&quot;)</code>를 사용하더라도 일반적인 Node.js 실행 환경에서는 동일한 Mongoose 객체와 기본 연결을 공유하게 된다.</p>
<hr>
<h3 id="기존-내부-코드에-대한-주의점">기존 내부 코드에 대한 주의점</h3>
<p>다음 코드는 과거 Mongoose의 내부 구현을 단순화한 코드에 가깝다.</p>
<pre><code class="language-javascript">Mongoose.prototype.connect = function(uri, options, callback) {
    // ...
};</code></pre>
<p>현재 Mongoose를 설명할 때는 이 코드를 그대로 사용하는 것이 적절하지 않다.</p>
<p>Mongoose 7부터 <code>Mongoose.prototype.connect()</code>의 콜백 방식이 제거되었으며, 현재는 Promise 또는 <code>async/await</code> 방식으로 사용한다.</p>
<pre><code class="language-javascript">// 현재 사용 방식
await mongoose.connect(uri);</code></pre>
<p>또한 기존 코드에 작성된 다음 부분은 문법상 잘못된 표현이다.</p>
<pre><code class="language-javascript">this.instanceof Mongoose</code></pre>
<p>정확한 자바스크립트 문법은 다음과 같다.</p>
<pre><code class="language-javascript">this instanceof Mongoose</code></pre>
<p>다만 Mongoose의 내부 소스 코드는 버전에 따라 변경될 수 있기 때문에, 내부 코드 자체를 외우기보다는 다음 흐름을 이해하는 것이 중요하다.</p>
<ol>
<li>Node.js는 한 번 불러온 CommonJS 모듈을 캐시에 저장한다.</li>
<li>여러 파일에서 <code>require(&quot;mongoose&quot;)</code>를 호출해도 일반적으로 동일한 Mongoose 객체가 반환된다.</li>
<li>이 Mongoose 객체가 기본 연결인 <code>mongoose.connection</code>을 관리한다.</li>
<li><code>mongoose.connect(uri)</code>는 기본 연결을 MongoDB에 연결한다.</li>
<li><code>connect()</code>는 현재 Promise를 반환하며, 연결 성공 시 기존 Mongoose 객체로 이행된다.</li>
<li>따라서 <code>connect()</code>가 싱글톤을 새로 생성한다기보다, 공유되는 Mongoose 객체와 기본 연결이 싱글톤처럼 사용된다고 보는 것이 정확하다.</li>
<li><code>uri</code>는 MongoDB 서버, 포트, 데이터베이스와 접속 옵션을 표현하는 연결 식별자이다.</li>
</ol>
<blockquote>
<p>Mongoose의 싱글톤 구조는 <code>connect()</code>가 객체 하나를 생성하는 구조라기보다, Node.js 모듈 캐싱을 통해 공유되는 하나의 Mongoose 객체가 기본 데이터베이스 연결을 관리하는 구조이다.</p>
</blockquote>
<hr>
<h3 id="mysql의-싱글톤-패턴">MySQL의 싱글톤 패턴</h3>
<p>Node.js 에서 MySQL 데이터베이스를 연결할 때도 싱글톤 패턴이 쓰임</p>
<pre><code class="language-javascript">// 메인 모듈
const mysql = require(&#39;mysql&#39;);
const pool = mysql.createPool({ // 커넥션 풀 생성
  conncetionLimit: 10,
  host: &#39;example.org&#39;,
  user: &#39;kundol&#39;,
  password: &#39;secret&#39;,
  database: &#39;승철이디비&#39;
});
pool.connect();

// 모듈 A
pool.query(query, function (error, results, fields) {
  if (error) throw error;
  console.log(&#39;The solution is: &#39;, results[0].solution);
});
</code></pre>
<h3 id="싱글톤-패턴의-단점">싱글톤 패턴의 단점</h3>
<p>싱글톤 패턴은 애플리케이션 전체에서 하나의 인스턴스를 공유하기 때문에 사용하기 쉽고 실용적이다.</p>
<p>하지만 하나의 객체와 상태를 여러 모듈이 함께 사용하기 때문에 다음과 같은 문제가 발생할 수 있다.</p>
<ul>
<li>테스트 간 상태 공유</li>
<li>모듈 간 강한 결합</li>
<li>의존 관계 파악의 어려움</li>
<li>객체 교체의 어려움</li>
</ul>
<hr>
<h4 id="tdd와-단위-테스트에서의-문제">TDD와 단위 테스트에서의 문제</h4>
<p>TDD는 <strong>Test Driven Development</strong>의 약자로, 테스트 코드를 먼저 작성한 뒤 해당 테스트를 통과하도록 실제 코드를 구현하는 개발 방식이다.</p>
<p>TDD에서는 일반적으로 단위 테스트를 많이 작성한다.</p>
<p>단위 테스트는 다음 조건을 만족하는 것이 좋다.</p>
<ul>
<li>각각의 테스트가 서로 독립적으로 실행되어야 함</li>
<li>테스트 실행 순서가 결과에 영향을 주지 않아야 함</li>
<li>특정 테스트의 실행 결과가 다른 테스트에 영향을 주지 않아야 함</li>
<li>테스트할 대상만 분리하여 검증할 수 있어야 함</li>
</ul>
<p>하지만 싱글톤 객체는 애플리케이션 전체에서 하나의 인스턴스를 공유한다.</p>
<pre><code class="language-text">테스트 A ─┐
         ├── 동일한 싱글톤 인스턴스
테스트 B ─┘</code></pre>
<p>싱글톤 객체가 내부 상태를 가지고 있다면 테스트 A에서 변경한 상태가 테스트 B에도 남아 있을 수 있다.</p>
<pre><code class="language-javascript">class Counter {
    constructor() {
        if (Counter.instance) {
            return Counter.instance;
        }

        this.count = 0;
        Counter.instance = this;
    }

    increase() {
        this.count += 1;
    }
}</code></pre>
<pre><code class="language-javascript">const counter = new Counter();

counter.increase();

console.log(counter.count); // 1</code></pre>
<p>다른 테스트에서 다시 객체를 생성해도 새로운 객체가 만들어지지 않는다.</p>
<pre><code class="language-javascript">const anotherCounter = new Counter();

console.log(anotherCounter.count); // 1</code></pre>
<p><code>anotherCounter</code>는 새롭게 생성된 독립적인 객체가 아니라 기존의 싱글톤 인스턴스이다.</p>
<p>따라서 이전 테스트에서 변경된 <code>count</code> 값이 그대로 남아 있을 수 있다.</p>
<p>이로 인해 다음과 같은 문제가 발생한다.</p>
<ul>
<li>테스트 실행 순서에 따라 결과가 달라질 수 있음</li>
<li>테스트마다 새로운 인스턴스를 생성하기 어려움</li>
<li>테스트가 실패했을 때 원인을 찾기 어려움</li>
<li>테스트용 가짜 객체로 교체하기 어려움</li>
</ul>
<p>싱글톤 패턴이 TDD 자체를 불가능하게 만드는 것은 아니다.</p>
<p>다만 상태를 가진 싱글톤 객체를 전역적으로 사용하면 테스트의 독립성을 유지하기 어려워질 수 있다.</p>
<blockquote>
<p>싱글톤의 핵심적인 테스트 문제는 인스턴스가 하나라는 사실 자체보다, 하나의 상태와 의존성을 여러 테스트가 공유한다는 점이다.</p>
</blockquote>
<hr>
<h3 id="의존성이란">의존성이란?</h3>
<p>의존성은 한 객체나 모듈이 자신의 기능을 수행하기 위해 다른 객체나 모듈을 필요로 하는 관계이다.</p>
<p>A가 B에 의존한다는 것은 다음과 같은 의미이다.</p>
<blockquote>
<p>A가 동작하기 위해 B가 필요하며, B의 구현이나 사용 방식이 변경되면 A도 영향을 받을 가능성이 있다.</p>
</blockquote>
<p>예를 들어 <code>OrderService</code>가 직접 <code>MySQLDatabase</code>를 생성한다고 가정한다.</p>
<pre><code class="language-javascript">class MySQLDatabase {
    save(order) {
        console.log(&quot;MySQL에 주문 저장&quot;);
    }
}

class OrderService {
    constructor() {
        this.database = new MySQLDatabase();
    }

    createOrder(order) {
        this.database.save(order);
    }
}</code></pre>
<p><code>OrderService</code>는 내부에서 <code>MySQLDatabase</code>를 직접 생성한다.</p>
<pre><code class="language-javascript">this.database = new MySQLDatabase();</code></pre>
<p>따라서 <code>OrderService</code>는 <code>MySQLDatabase</code>의 구체적인 구현에 강하게 의존한다.</p>
<p>나중에 데이터베이스를 MongoDB로 변경하려면 <code>OrderService</code> 내부 코드도 수정해야 한다.</p>
<pre><code class="language-text">MySQLDatabase 변경
        ↓
OrderService도 수정</code></pre>
<p>이처럼 한 모듈의 변경이 다른 모듈의 변경으로 이어지는 정도를 <strong>결합도</strong>라고 한다.</p>
<ul>
<li>결합도가 높음: 모듈들이 서로 강하게 연결됨</li>
<li>결합도가 낮음: 모듈을 비교적 독립적으로 변경 가능</li>
</ul>
<hr>
<h3 id="의존성-주입">의존성 주입</h3>
<p>의존성 주입은 <strong>Dependency Injection</strong>, 줄여서 DI라고 한다.</p>
<p>의존성 주입은 객체가 자신에게 필요한 의존 객체를 직접 생성하지 않고 외부에서 전달받는 방식이다.</p>
<p>기존 방식에서는 <code>OrderService</code>가 직접 데이터베이스 객체를 생성했다.</p>
<pre><code class="language-javascript">class OrderService {
    constructor() {
        this.database = new MySQLDatabase();
    }
}</code></pre>
<p>의존성 주입을 적용하면 사용할 데이터베이스 객체를 외부에서 전달받는다.</p>
<pre><code class="language-javascript">class OrderService {
    constructor(database) {
        this.database = database;
    }

    createOrder(order) {
        this.database.save(order);
    }
}</code></pre>
<p>외부에서는 필요한 구현체를 생성하여 전달한다.</p>
<pre><code class="language-javascript">const database = new MySQLDatabase();
const orderService = new OrderService(database);</code></pre>
<p>동작 구조는 다음과 같다.</p>
<pre><code class="language-text">기존 방식

OrderService
     ↓ 직접 생성
MySQLDatabase</code></pre>
<pre><code class="language-text">의존성 주입 적용

외부 조립 영역
   ├── MySQLDatabase 생성
   └── OrderService에 전달</code></pre>
<p><code>OrderService</code>는 데이터베이스 객체를 직접 생성하지 않는다.</p>
<p>외부에서 전달받은 객체를 사용할 뿐이다.</p>
<blockquote>
<p>의존성 주입은 필요한 객체를 내부에서 직접 만들지 않고 외부에서 넣어주는 방식이다.</p>
</blockquote>
<hr>
<h3 id="의존성-주입자">의존성 주입자</h3>
<p>객체를 생성하고 필요한 의존성을 연결해주는 역할을 하는 대상을 의존성 주입자라고 볼 수 있다.</p>
<pre><code class="language-text">의존성 주입자
    ├── 하위 모듈 생성
    ├── 상위 모듈 생성
    └── 상위 모듈에 하위 모듈 전달</code></pre>
<p>직접 의존성을 주입할 수도 있다.</p>
<pre><code class="language-javascript">const database = new MySQLDatabase();
const orderService = new OrderService(database);</code></pre>
<p>Spring과 같은 프레임워크에서는 DI 컨테이너가 객체를 생성하고 연결하는 역할을 담당한다.</p>
<pre><code class="language-text">Spring 컨테이너
    ├── Repository 생성
    ├── Service 생성
    └── Service에 Repository 주입</code></pre>
<p>즉, 메인 모듈이 하위 모듈을 직접 생성하지 않고 외부의 조립 영역이나 DI 컨테이너가 두 모듈을 연결한다.</p>
<p>이를 통해 모듈 사이의 결합도를 낮출 수 있다.</p>
<p>이러한 관계를 <strong>디커플링</strong> 또는 <strong>느슨한 결합</strong>이라고 한다.</p>
<hr>
<h3 id="의존성-주입을-통한-싱글톤-문제-완화">의존성 주입을 통한 싱글톤 문제 완화</h3>
<p>싱글톤 객체를 모듈 내부에서 직접 가져오면 해당 모듈은 싱글톤 구현에 강하게 의존한다.</p>
<pre><code class="language-javascript">class UserService {
    constructor() {
        this.database = Database.getInstance();
    }
}</code></pre>
<p><code>UserService</code>는 내부에서 <code>Database.getInstance()</code>를 직접 호출한다.</p>
<p>따라서 실제 데이터베이스 대신 테스트용 객체를 사용하기 어렵다.</p>
<p>의존성 주입을 적용하면 데이터베이스 객체를 외부에서 전달받을 수 있다.</p>
<pre><code class="language-javascript">class UserService {
    constructor(database) {
        this.database = database;
    }
}</code></pre>
<p>실제 애플리케이션에서는 싱글톤 데이터베이스 객체를 전달한다.</p>
<pre><code class="language-javascript">const database = Database.getInstance();
const userService = new UserService(database);</code></pre>
<p>테스트에서는 가짜 데이터베이스 객체를 전달한다.</p>
<pre><code class="language-javascript">class FakeDatabase {
    findUser() {
        return {
            id: 1,
            name: &quot;테스트 사용자&quot;
        };
    }
}

const fakeDatabase = new FakeDatabase();
const userService = new UserService(fakeDatabase);</code></pre>
<p>이 구조에서는 <code>UserService</code>가 싱글톤 데이터베이스를 직접 가져오지 않는다.</p>
<p>외부에서 어떤 객체를 전달하느냐에 따라 실제 객체와 테스트 객체를 자유롭게 교체할 수 있다.</p>
<pre><code class="language-text">실제 실행

UserService
     ↓
실제 Database</code></pre>
<pre><code class="language-text">테스트 실행

UserService
     ↓
FakeDatabase</code></pre>
<p>따라서 싱글톤을 사용하더라도 해당 싱글톤을 모듈 내부에서 직접 참조하지 않고 의존성 주입으로 전달하면 결합도를 낮추고 테스트하기 쉬운 구조를 만들 수 있다.</p>
<hr>
<h3 id="의존성-주입의-장점">의존성 주입의 장점</h3>
<h4 id="1-객체를-쉽게-교체할-수-있음">1. 객체를 쉽게 교체할 수 있음</h4>
<p>모듈 내부 코드를 수정하지 않고 외부에서 전달하는 구현체만 변경할 수 있다.</p>
<pre><code class="language-javascript">const orderService =
    new OrderService(new MySQLDatabase());</code></pre>
<pre><code class="language-javascript">const orderService =
    new OrderService(new MongoDatabase());</code></pre>
<p><code>OrderService</code>는 수정하지 않아도 된다.</p>
<hr>
<h4 id="2-테스트하기-쉬움">2. 테스트하기 쉬움</h4>
<p>실제 데이터베이스나 외부 API 대신 테스트용 가짜 객체를 주입할 수 있다.</p>
<pre><code class="language-javascript">const orderService =
    new OrderService(new FakeDatabase());</code></pre>
<p>외부 시스템에 실제로 연결하지 않고도 <code>OrderService</code>의 로직만 독립적으로 테스트할 수 있다.</p>
<hr>
<h4 id="3-마이그레이션이-쉬움">3. 마이그레이션이 쉬움</h4>
<p>데이터베이스, 메시지 브로커, 외부 API 등의 구현 기술을 변경할 때 영향을 받는 코드의 범위를 줄일 수 있다.</p>
<pre><code class="language-text">MySQL → MongoDB
SMTP → 외부 이메일 API
로컬 저장소 → 클라우드 저장소</code></pre>
<hr>
<h4 id="4-의존-관계가-명확해짐">4. 의존 관계가 명확해짐</h4>
<p>생성자의 매개변수를 보면 해당 객체가 어떤 의존성을 필요로 하는지 확인할 수 있다.</p>
<pre><code class="language-javascript">class OrderService {
    constructor(database, paymentService) {
        this.database = database;
        this.paymentService = paymentService;
    }
}</code></pre>
<p><code>OrderService</code>가 다음 두 객체에 의존한다는 사실이 명확하게 드러난다.</p>
<ul>
<li><code>database</code></li>
<li><code>paymentService</code></li>
</ul>
<hr>
<h4 id="5-의존성-방향을-일관되게-관리할-수-있음">5. 의존성 방향을 일관되게 관리할 수 있음</h4>
<p>구현체에 직접 의존하는 대신 인터페이스나 추상화에 의존하도록 설계할 수 있다.</p>
<pre><code class="language-text">OrderService
     ↓
Database 인터페이스
     ↑
MySQLDatabase
MongoDatabase
FakeDatabase</code></pre>
<p>상위 모듈은 특정 데이터베이스 구현을 몰라도 된다.</p>
<hr>
<h3 id="의존성-주입의-단점">의존성 주입의 단점</h3>
<h4 id="1-클래스와-인터페이스-수-증가">1. 클래스와 인터페이스 수 증가</h4>
<p>구현체와 추상화 계층을 분리하면서 클래스나 파일의 수가 늘어날 수 있다.</p>
<pre><code class="language-text">Database
MySQLDatabase
MongoDatabase
FakeDatabase
DatabaseFactory</code></pre>
<p>작은 프로그램에서는 이러한 구조가 오히려 과도하게 복잡하게 느껴질 수 있다.</p>
<hr>
<h4 id="2-초기-구조-이해의-어려움">2. 초기 구조 이해의 어려움</h4>
<p>객체가 내부에서 직접 생성되지 않기 때문에 실제로 어떤 객체가 주입되는지 여러 설정 파일이나 DI 컨테이너를 확인해야 할 수 있다.</p>
<pre><code class="language-text">Service 코드
    ↓
인터페이스
    ↓
DI 설정
    ↓
실제 구현체</code></pre>
<hr>
<h4 id="3-di-컨테이너에-대한-학습-필요">3. DI 컨테이너에 대한 학습 필요</h4>
<p>Spring과 같은 프레임워크에서는 객체의 생성과 주입을 컨테이너가 담당한다.</p>
<p>따라서 다음과 같은 개념을 추가로 이해해야 한다.</p>
<ul>
<li>빈 등록</li>
<li>컴포넌트 스캔</li>
<li>생성자 주입</li>
<li>객체 생명주기</li>
<li>스코프</li>
<li>순환 의존성</li>
</ul>
<hr>
<h4 id="4-일부-런타임-비용">4. 일부 런타임 비용</h4>
<p>DI 컨테이너를 사용하는 경우 객체 탐색, 생성, 리플렉션, 프록시 생성 등의 과정에서 약간의 추가 비용이 발생할 수 있다.</p>
<p>다만 일반적인 애플리케이션에서는 이러한 비용보다 유지보수성과 테스트 편의성의 장점이 더 큰 경우가 많다.</p>
<hr>
<h3 id="의존성-주입과-의존성-역전-원칙">의존성 주입과 의존성 역전 원칙</h3>
<p>다음 내용은 엄밀히 말하면 의존성 주입 자체의 원칙이라기보다 <strong>의존성 역전 원칙</strong>에 해당한다.</p>
<p>의존성 역전 원칙은 <strong>Dependency Inversion Principle</strong>, 줄여서 DIP라고 한다.</p>
<blockquote>
<p>상위 수준 모듈은 하위 수준 모듈의 구체적인 구현에 직접 의존하면 안 된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.</p>
</blockquote>
<p>또한 다음 원칙을 따른다.</p>
<blockquote>
<p>추상화는 구체적인 세부 구현에 의존하지 않고, 세부 구현이 추상화에 의존해야 한다.</p>
</blockquote>
<hr>
<h4 id="잘못된-의존-방향">잘못된 의존 방향</h4>
<pre><code class="language-text">OrderService
     ↓
MySQLDatabase</code></pre>
<p><code>OrderService</code>가 구체적인 <code>MySQLDatabase</code>에 직접 의존한다.</p>
<p>MySQL을 MongoDB로 변경하면 <code>OrderService</code>도 수정해야 할 수 있다.</p>
<hr>
<h4 id="의존성-역전-원칙-적용">의존성 역전 원칙 적용</h4>
<pre><code class="language-text">OrderService
     ↓
Database 인터페이스
     ↑
MySQLDatabase
MongoDatabase</code></pre>
<ul>
<li>상위 모듈: <code>OrderService</code></li>
<li>추상화: <code>Database</code></li>
<li>하위 모듈: <code>MySQLDatabase</code>, <code>MongoDatabase</code></li>
</ul>
<p>상위 모듈과 하위 모듈이 모두 <code>Database</code>라는 추상화에 의존한다.</p>
<pre><code class="language-javascript">class OrderService {
    constructor(database) {
        this.database = database;
    }
}</code></pre>
<p><code>OrderService</code>는 전달받은 객체가 MySQL인지 MongoDB인지 알 필요가 없다.</p>
<p>해당 객체가 필요한 기능을 제공하기만 하면 된다.</p>
<hr>
<h3 id="di와-dip의-차이">DI와 DIP의 차이</h3>
<p>DI와 DIP는 관련이 있지만 같은 개념은 아니다.</p>
<h4 id="di">DI</h4>
<p>객체에 필요한 의존성을 외부에서 전달하는 구현 방식이다.</p>
<pre><code class="language-text">필요한 객체를 외부에서 주입</code></pre>
<h4 id="dip">DIP</h4>
<p>상위 모듈과 하위 모듈이 구체적인 구현이 아니라 추상화에 의존하도록 만드는 설계 원칙이다.</p>
<pre><code class="language-text">구현체가 아닌 추상화에 의존</code></pre>
<p>의존성 주입은 의존성 역전 원칙을 구현하는 대표적인 방법 중 하나이다.</p>
<pre><code class="language-text">DIP
 └── 이를 구현하는 방법 중 하나가 DI</code></pre>
<hr>
<ol>
<li>싱글톤은 하나의 인스턴스와 상태를 여러 모듈이 공유한다.</li>
<li>상태가 공유되면 테스트 간 독립성이 깨질 수 있다.</li>
<li>싱글톤 객체를 모듈 내부에서 직접 가져오면 결합도가 높아진다.</li>
<li>의존성 주입은 필요한 객체를 내부에서 생성하지 않고 외부에서 전달받는 방식이다.</li>
<li>의존성 주입을 사용하면 실제 객체와 테스트 객체를 쉽게 교체할 수 있다.</li>
<li>의존성 주입은 테스트, 유지보수, 기술 변경에 유리하다.</li>
<li>클래스와 설정이 늘어나 구조가 복잡해질 수 있다는 단점도 있다.</li>
<li>상위 모듈과 하위 모듈이 모두 추상화에 의존해야 한다는 내용은 DI 원칙이 아니라 의존성 역전 원칙이다.</li>
</ol>
<blockquote>
<p>싱글톤의 문제는 하나의 객체를 사용한다는 사실 자체보다, 해당 객체를 여러 모듈이 직접 참조하고 상태까지 공유하면서 강하게 결합되는 데서 발생한다.</p>
</blockquote>
<blockquote>
<p>의존성 주입은 객체를 직접 생성하거나 찾아오지 않고 외부에서 전달받게 하여 모듈 간 결합도를 낮추는 방법이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 9장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-9%EC%9E%A5-%EC%A0%95%EB%A6%AC-u2nl6iya</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-9%EC%9E%A5-%EC%A0%95%EB%A6%AC-u2nl6iya</guid>
            <pubDate>Tue, 04 Aug 2026 14:57:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-8%EC%9E%A5-%EC%A0%95%EB%A6%AC">[대규모 시스템 설계 스터디] 8장 정리</a>
다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-10%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 10장 정리</a></p>
</blockquote>
<h1 id="9장-웹-크롤러-설계">9장. 웹 크롤러 설계</h1>
<p>웹 크롤러(Web Crawler)는 웹 페이지를 자동으로 방문하고, 페이지 안의 콘텐츠와 링크를 수집하는 프로그램이다.</p>
<p>검색 엔진의 Googlebot처럼 웹의 새로운 페이지를 찾아 검색 인덱스를 만드는 것이 대표적인 예다.</p>
<p>크롤러는 보통 몇 개의 시작 URL에서 출발한다.</p>
<pre><code class="language-text">Seed URL
   ↓
페이지 다운로드
   ↓
링크 추출
   ↓
새 URL 발견
   ↓
다시 다운로드
   ↓
반복</code></pre>
<p>겉으로 보면 단순한 반복 작업처럼 보인다.</p>
<p>하지만 수십억 개의 페이지를 대상으로 한다면</p>
<ul>
<li>어떤 URL부터 방문할 것인지</li>
<li>같은 페이지를 다시 방문하지 않을 방법</li>
<li>특정 사이트에 요청이 몰리지 않게 하는 방법</li>
<li>실패한 서버를 어떻게 처리할지</li>
<li>콘텐츠 중복을 어떻게 제거할지</li>
<li>수집한 데이터를 어디에 저장할지</li>
</ul>
<p>등을 모두 고려해야 한다.</p>
<p>결국 웹 크롤러는 단순한 링크 수집 프로그램이 아니라 <strong>대규모 분산 시스템</strong>으로 바라봐야 한다.</p>
<hr>
<h1 id="1-웹-크롤러는-어디에-사용할까">1. 웹 크롤러는 어디에 사용할까?</h1>
<h2 id="검색-엔진-인덱싱">검색 엔진 인덱싱</h2>
<p>웹 크롤러의 가장 대표적인 활용 사례다.</p>
<pre><code class="language-text">웹 페이지 수집
    ↓
페이지 분석
    ↓
검색 인덱스 생성
    ↓
검색 결과 제공</code></pre>
<p>크롤러가 웹 페이지를 미리 찾아 수집해두기 때문에 사용자가 검색할 때마다 전체 웹을 실시간으로 확인할 필요가 없다.</p>
<hr>
<h2 id="웹-아카이빙">웹 아카이빙</h2>
<p>웹 페이지는 언제든 수정되거나 삭제될 수 있다.</p>
<p>따라서 특정 시점의 웹 콘텐츠를 장기간 보존하기 위해 웹 크롤러를 사용할 수도 있다.</p>
<pre><code class="language-text">웹 페이지 수집
    ↓
특정 시점 상태 저장
    ↓
장기간 보관</code></pre>
<hr>
<h2 id="웹-마이닝">웹 마이닝</h2>
<p>웹에서 데이터를 수집한 뒤 분석하여 새로운 정보를 찾아내는 방식이다.</p>
<pre><code class="language-text">Web Crawling
→ 데이터 수집

Web Mining
→ 수집한 데이터에서 의미 발견</code></pre>
<p>예를 들어 기업 관련 보고서와 공시 자료 등을 수집하고 분석하는 데 활용할 수 있다.</p>
<hr>
<h2 id="웹-모니터링">웹 모니터링</h2>
<p>특정 콘텐츠가 새롭게 나타났거나 기존 내용이 변경되었는지 지속적으로 확인하는 방식이다.</p>
<p>예를 들면</p>
<ul>
<li>저작권 침해 콘텐츠 탐지</li>
<li>상표권 침해 확인</li>
<li>특정 페이지 변화 감지</li>
</ul>
<p>등에 활용할 수 있다.</p>
<p>즉 웹 마이닝이 <strong>수집된 데이터에서 의미를 찾는 것</strong>이라면, 웹 모니터링은 <strong>특정 조건이나 변화를 계속 추적하는 것</strong>에 가깝다.</p>
<hr>
<h1 id="2-웹-크롤러-요구사항-정리">2. 웹 크롤러 요구사항 정리</h1>
<p>웹 크롤러를 설계하기 전에 먼저 어떤 크롤러를 만들어야 하는지 범위를 확정해야 한다.</p>
<p>예를 들어 다음을 확인할 수 있다.</p>
<ul>
<li>검색 엔진용인가?</li>
<li>웹 아카이빙용인가?</li>
<li>HTML만 수집하는가?</li>
<li>이미지와 PDF도 수집하는가?</li>
<li>어느 정도 규모의 페이지를 수집하는가?</li>
<li>이미 수집한 페이지를 다시 방문하는가?</li>
</ul>
<p>이러한 조건에 따라 설계 방식이 달라진다.</p>
<hr>
<h1 id="3-좋은-웹-크롤러가-가져야-할-특성">3. 좋은 웹 크롤러가 가져야 할 특성</h1>
<h2 id="규모-확장성">규모 확장성</h2>
<p>웹의 규모가 매우 크기 때문에 한 서버에서 URL을 하나씩 처리하는 방식으로는 부족하다.</p>
<pre><code class="language-text">단일 Worker

URL1 → URL2 → URL3 → URL4</code></pre>
<p>대신 여러 작업자가 동시에 URL을 처리할 수 있어야 한다.</p>
<pre><code class="language-text">Worker 1 → URL1
Worker 2 → URL2
Worker 3 → URL3
Worker 4 → URL4</code></pre>
<p>이를 통해 전체 크롤링 처리량을 높인다.</p>
<hr>
<h2 id="안정성">안정성</h2>
<p>웹에는 정상적인 페이지뿐 아니라 다양한 문제가 존재한다.</p>
<ul>
<li>잘못된 HTML</li>
<li>응답하지 않는 서버</li>
<li>지나치게 느린 서버</li>
<li>매우 큰 파일</li>
<li>반복되는 URL</li>
<li>비정상적인 콘텐츠</li>
</ul>
<p>특정 URL 하나 때문에 전체 크롤러가 멈춰서는 안 된다.</p>
<pre><code class="language-text">URL 요청
   ↓
응답 없음
   ↓
Timeout
   ↓
해당 작업 종료
   ↓
다음 URL 처리</code></pre>
<hr>
<h2 id="예의-politeness">예의 Politeness</h2>
<p>웹 크롤러에서 말하는 예의는 <strong>수집 대상 서버에 지나친 부담을 주지 않는 것</strong>이다.</p>
<p>예를 들어 한 사이트에 수백 개의 요청을 동시에 보내면 해당 사이트의 정상 사용자에게까지 영향을 줄 수 있다.</p>
<p>따라서 같은 호스트에는 적절한 간격을 두고 요청해야 한다.</p>
<pre><code class="language-text">Request
   ↓
Wait
   ↓
Request
   ↓
Wait</code></pre>
<p>또한 해당 사이트가 제공하는 <code>robots.txt</code> 정책도 확인해야 한다.</p>
<hr>
<h2 id="확장성-extensibility">확장성 Extensibility</h2>
<p>처음에는 HTML만 수집하더라도 이후 다른 콘텐츠까지 지원해야 할 수 있다.</p>
<pre><code class="language-text">Crawler
 ├─ HTML Parser
 ├─ Image Downloader
 ├─ PDF Processor
 └─ Video Processor</code></pre>
<p>따라서 새로운 콘텐츠 형식을 추가할 때 전체 시스템을 수정하기보다 필요한 모듈만 추가할 수 있도록 설계하는 것이 좋다.</p>
<hr>
<h1 id="4-개략적인-규모-추정">4. 개략적인 규모 추정</h1>
<p>매달 10억 페이지를 다운로드한다고 가정한다.</p>
<p>평균 QPS는</p>
<pre><code class="language-text">1,000,000,000
÷ 30일
÷ 24시간
÷ 3,600초

≈ 400 Page / sec</code></pre>
<p>정도다.</p>
<p>Peak QPS를 평균의 2배로 가정하면</p>
<pre><code class="language-text">약 800 Page / sec</code></pre>
<p>수준을 처리해야 한다.</p>
<hr>
<h2 id="저장-공간">저장 공간</h2>
<p>웹 페이지 평균 크기를 500KB로 가정하면</p>
<pre><code class="language-text">10억 × 500KB
≈ 500TB / 월</code></pre>
<p>이 된다.</p>
<p>5년 동안 저장하면</p>
<pre><code class="language-text">500TB × 12 × 5
≈ 30PB</code></pre>
<p>정도의 저장 공간이 필요하다.</p>
<p>즉 대규모 웹 크롤러에서는 <strong>URL 처리뿐 아니라 저장소 자체도 대규모 시스템으로 설계해야 한다.</strong></p>
<hr>
<h1 id="5-웹-크롤러의-전체-구조">5. 웹 크롤러의 전체 구조</h1>
<p>전체적인 흐름은 다음과 같이 볼 수 있다.</p>
<pre><code class="language-text">Seed URLs
    ↓
URL Frontier
    ↓
HTML Downloader
    ↓
Content Parser
    ↓
중복 콘텐츠 검사
    ↓
Content Storage
    ↓
URL Extractor
    ↓
URL Filter
    ↓
방문 여부 확인
    ↓
URL Frontier</code></pre>
<p>새롭게 발견된 URL이 다시 URL Frontier로 들어가면서 이 과정이 계속 반복된다.</p>
<hr>
<h1 id="6-seed-url">6. Seed URL</h1>
<p>Seed URL은 크롤링을 시작하는 출발점이다.</p>
<p>예를 들어 특정 대학 사이트만 수집한다면 해당 대학 홈페이지의 주요 URL을 시작점으로 사용할 수 있다.</p>
<p>전체 웹을 대상으로 한다면 여러 종류의 Seed URL이 필요하다.</p>
<p>예를 들면</p>
<pre><code class="language-text">지역별 주요 사이트

한국
미국
일본
...

또는

주제별 주요 사이트

쇼핑
뉴스
스포츠
건강
...</code></pre>
<p>처럼 나눌 수 있다.</p>
<p>Seed URL을 정하는 완벽한 방법은 없으며 크롤러의 목적에 따라 달라진다.</p>
<hr>
<h1 id="7-url-frontier">7. URL Frontier</h1>
<p>앞으로 방문해야 할 URL을 저장하는 공간이다.</p>
<pre><code class="language-text">아직 방문하지 않은 URL
          ↓
      URL Frontier
          ↓
      Downloader</code></pre>
<p>단순하게 생각하면 Queue와 비슷하다.</p>
<p>하지만 실제 대규모 크롤러에서는 단순 FIFO Queue 이상의 역할을 담당한다.</p>
<p>대표적으로</p>
<ul>
<li>URL 우선순위 관리</li>
<li>같은 사이트에 대한 요청 간격 조절</li>
<li>재수집 일정 관리</li>
</ul>
<p>가 필요하다.</p>
<hr>
<h1 id="8-html-downloader">8. HTML Downloader</h1>
<p>URL Frontier에서 URL을 가져와 실제 웹 페이지를 다운로드한다.</p>
<pre><code class="language-text">URL Frontier
      ↓
HTML Downloader
      ↓
HTTP Request
      ↓
Website</code></pre>
<p>다운로더는 URL에 포함된 도메인을 실제 서버의 IP 주소로 변환하기 위해 DNS도 사용한다.</p>
<hr>
<h1 id="9-콘텐츠-파서">9. 콘텐츠 파서</h1>
<p>다운로드했다고 해서 곧바로 저장하는 것은 아니다.</p>
<p>HTML이 정상적인지 확인하고 필요한 데이터를 추출해야 한다.</p>
<pre><code class="language-text">Downloaded HTML
       ↓
Content Parser
       ↓
Validation</code></pre>
<p>예를 들어</p>
<ul>
<li>비정상적인 HTML</li>
<li>너무 큰 콘텐츠</li>
<li>잘못된 인코딩</li>
</ul>
<p>등을 확인할 수 있다.</p>
<p>파싱 작업을 별도 컴포넌트로 분리하면 다운로더가 페이지 다운로드에 집중할 수 있다.</p>
<hr>
<h1 id="10-중복-콘텐츠-검사">10. 중복 콘텐츠 검사</h1>
<p>웹에는 동일한 콘텐츠가 서로 다른 URL로 존재할 수 있다.</p>
<p>모든 문서를 문자열 전체로 비교하면 비용이 너무 크다.</p>
<p>따라서 콘텐츠의 Hash 또는 Checksum을 계산할 수 있다.</p>
<pre><code class="language-text">Content
   ↓
Hash
   ↓
기존 Hash와 비교</code></pre>
<p>이미 동일한 값이 존재한다면 중복 콘텐츠로 판단하여 저장하지 않는다.</p>
<p>이를 통해</p>
<ul>
<li>저장 공간</li>
<li>파싱 비용</li>
<li>후속 처리 비용</li>
</ul>
<p>을 줄일 수 있다.</p>
<hr>
<h1 id="11-콘텐츠-저장소">11. 콘텐츠 저장소</h1>
<p>중복 검사를 통과한 콘텐츠는 저장소에 보관한다.</p>
<p>크롤링 데이터는 규모가 매우 크기 때문에 모든 데이터를 메모리에 보관할 수는 없다.</p>
<p>따라서</p>
<pre><code class="language-text">대부분의 콘텐츠
→ Disk

자주 접근하는 콘텐츠
→ Memory / Cache</code></pre>
<p>와 같은 구조를 사용할 수 있다.</p>
<p>저장소를 결정할 때는</p>
<ul>
<li>데이터 크기</li>
<li>접근 빈도</li>
<li>콘텐츠 종류</li>
<li>저장 기간</li>
</ul>
<p>등을 고려한다.</p>
<hr>
<h1 id="12-url-추출기">12. URL 추출기</h1>
<p>HTML 페이지에는 다른 페이지로 연결되는 링크가 포함되어 있다.</p>
<p>URL Extractor는 이 링크들을 찾아낸다.</p>
<p>예를 들어</p>
<pre><code class="language-text">/wiki/Cong_Weixi</code></pre>
<p>라는 상대 경로가 있다면 현재 도메인과 결합한다.</p>
<pre><code class="language-text">https://en.wikipedia.org
+
/wiki/Cong_Weixi

↓

https://en.wikipedia.org/wiki/Cong_Weixi</code></pre>
<p>즉 추출된 링크를 다시 크롤링할 수 있도록 완전한 URL 형태로 만든다.</p>
<hr>
<h1 id="13-url-filter">13. URL Filter</h1>
<p>추출했다고 해서 모든 URL을 방문할 필요는 없다.</p>
<p>예를 들어</p>
<ul>
<li>크롤링 대상이 아닌 도메인</li>
<li>필요하지 않은 파일 형식</li>
<li>접근 금지 URL</li>
<li>반복적인 패턴</li>
<li>위험한 URL</li>
</ul>
<p>등은 미리 제거할 수 있다.</p>
<pre><code class="language-text">Extracted URL
      ↓
URL Filter
   ↙      ↘
Allow    Drop</code></pre>
<hr>
<h1 id="14-이미-방문한-url-확인">14. 이미 방문한 URL 확인</h1>
<p>동일한 URL을 계속 추가하면 같은 페이지를 반복적으로 수집하게 된다.</p>
<p>따라서 URL이</p>
<pre><code class="language-text">이미 방문했는가?
또는
이미 Frontier에 들어가 있는가?</code></pre>
<p>를 확인해야 한다.</p>
<p>중복 확인에는</p>
<ul>
<li>Hash Table</li>
<li>Bloom Filter</li>
</ul>
<p>등을 활용할 수 있다.</p>
<p>처음 발견한 URL만 Frontier로 다시 전달한다.</p>
<hr>
<h1 id="15-전체-크롤링-흐름">15. 전체 크롤링 흐름</h1>
<p>정리하면 다음과 같다.</p>
<pre><code class="language-text">1. Seed URL을 Frontier에 저장
          ↓
2. Downloader가 URL 조회
          ↓
3. DNS를 통해 서버 확인
          ↓
4. 페이지 다운로드
          ↓
5. Content Parsing
          ↓
6. 중복 콘텐츠 검사
          ↓
7. 콘텐츠 저장
          ↓
8. URL 추출
          ↓
9. URL Filtering
          ↓
10. 방문 여부 확인
          ↓
11. 새로운 URL만 Frontier에 추가
          ↓
12. 반복</code></pre>
<hr>
<h1 id="16-dfs와-bfs">16. DFS와 BFS</h1>
<p>웹 페이지는 그래프로 바라볼 수 있다.</p>
<pre><code class="language-text">Page = Node
Hyperlink = Edge</code></pre>
<p>따라서 웹 크롤링은 그래프 탐색 문제이기도 하다.</p>
<hr>
<h2 id="dfs">DFS</h2>
<p>하나의 경로를 가능한 깊게 따라가는 방식이다.</p>
<pre><code class="language-text">A
└─ B
   └─ C
      └─ D
         └─ ...</code></pre>
<p>웹은 사실상 끝을 예측하기 어려울 정도로 큰 그래프이기 때문에 특정 경로에 지나치게 깊게 들어갈 수 있다.</p>
<hr>
<h2 id="bfs">BFS</h2>
<p>현재 발견된 페이지 주변부터 넓게 탐색한다.</p>
<pre><code class="language-text">        A
      / | \
     B  C  D
    /      \
   E        F</code></pre>
<p>일반적으로 FIFO Queue를 사용한다.</p>
<p>웹 크롤링에서는 하나의 경로를 깊게 따라가기보다 여러 페이지를 넓게 탐색하는 것이 유리하기 때문에 BFS 기반 접근을 사용할 수 있다.</p>
<p>하지만 단순 BFS에도 문제가 있다.</p>
<hr>
<h1 id="17-단순-bfs의-문제-1-같은-서버에-요청-집중">17. 단순 BFS의 문제 1: 같은 서버에 요청 집중</h1>
<p>한 페이지에서 추출되는 링크 대부분이 같은 사이트 내부 링크일 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">wikipedia.org/page1
wikipedia.org/page2
wikipedia.org/page3
...</code></pre>
<p>가 Frontier에 연속해서 들어간다면 FIFO 순서대로 처리할 때 Wikipedia 서버에 요청이 집중될 수 있다.</p>
<pre><code class="language-text">Wikipedia
↑ ↑ ↑ ↑ ↑ ↑
Crawler Request</code></pre>
<p>이는 크롤러의 <strong>Politeness</strong> 문제로 이어진다.</p>
<hr>
<h1 id="18-단순-bfs의-문제-2-모든-페이지를-동일하게-취급">18. 단순 BFS의 문제 2: 모든 페이지를 동일하게 취급</h1>
<p>FIFO 방식은 먼저 발견한 URL을 먼저 처리할 뿐 페이지의 중요도는 고려하지 않는다.</p>
<p>하지만 실제 크롤링에서는 중요한 페이지를 먼저 방문하는 것이 효율적일 수 있다.</p>
<p>우선순위의 기준으로는</p>
<ul>
<li>PageRank</li>
<li>사용자 트래픽</li>
<li>갱신 빈도</li>
<li>페이지 중요도</li>
</ul>
<p>등을 사용할 수 있다.</p>
<p>따라서 URL Frontier는</p>
<pre><code class="language-text">Priority
+
Politeness</code></pre>
<p>두 가지를 동시에 처리해야 한다.</p>
<hr>
<h1 id="19-front-queue와-back-queue">19. Front Queue와 Back Queue</h1>
<p>URL Frontier를 두 종류의 Queue로 나누어 볼 수 있다.</p>
<pre><code class="language-text">Front Queue
→ Priority 담당

Back Queue
→ Politeness 담당</code></pre>
<p>즉</p>
<blockquote>
<p>Front Queue는 <strong>무엇을 먼저 수집할지</strong>, Back Queue는 <strong>어느 사이트를 지금 요청해도 되는지</strong>를 결정한다.</p>
</blockquote>
<hr>
<h1 id="20-front-queue">20. Front Queue</h1>
<p>URL의 중요도에 따라 서로 다른 Queue에 넣는다.</p>
<pre><code class="language-text">f1 → 낮은 Priority
f2 → 중간 Priority
f3 → 높은 Priority</code></pre>
<p>Prioritizer가 URL의 중요도를 계산한다.</p>
<p>그리고 Queue Selector는 높은 Priority Queue를 더 자주 선택한다.</p>
<pre><code class="language-text">High Priority
→ 자주 처리

Low Priority
→ 상대적으로 적게 처리</code></pre>
<p>다만 낮은 Priority URL이 영원히 처리되지 않는 상황은 방지해야 한다.</p>
<hr>
<h1 id="21-back-queue">21. Back Queue</h1>
<p>Back Queue는 Host별 요청 속도를 관리한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">wikipedia.org → Queue B1
apple.com     → Queue B2
nike.com      → Queue B3</code></pre>
<p>처럼 같은 Host에 속한 URL을 동일한 Queue에 넣는다.</p>
<hr>
<h1 id="22-back-queue-router">22. Back Queue Router</h1>
<p>URL에서 Host를 추출한다.</p>
<pre><code class="language-text">https://www.apple.com/mac
            ↓
        apple.com</code></pre>
<p>그리고 Mapping Table을 이용하여 해당 Host가 어느 Queue를 사용하는지 찾는다.</p>
<pre><code class="language-text">Host
   ↓
Mapping Table
   ↓
Back Queue</code></pre>
<p>이 구조를 통해 같은 웹사이트의 URL을 같은 Queue에서 관리할 수 있다.</p>
<hr>
<h1 id="23-queue-selector와-worker">23. Queue Selector와 Worker</h1>
<p>Queue Selector는 각 Host의 마지막 요청 시간을 확인한다.</p>
<p>현재 요청을 보내도 되는 Host의 Queue에서 URL 하나를 꺼내 Worker에게 전달한다.</p>
<pre><code class="language-text">Back Queues
    ↓
Queue Selector
    ↓
Worker
    ↓
Download</code></pre>
<p>Worker가 페이지를 내려받은 뒤 같은 사이트에는 일정 시간 후 다시 요청한다.</p>
<pre><code class="language-text">apple.com/page1
      ↓
     Wait
      ↓
apple.com/page2</code></pre>
<p>이를 통해 특정 서버에 대한 요청 폭주를 막을 수 있다.</p>
<hr>
<h1 id="24-신선도-freshness">24. 신선도 Freshness</h1>
<p>웹 페이지는 계속 변경된다.</p>
<p>따라서 한 번 수집한 페이지라고 해서 영원히 다시 방문하지 않는 것은 아니다.</p>
<pre><code class="language-text">Page Crawled
    ↓
시간 경과
    ↓
Page 변경
    ↓
Recrawl 필요</code></pre>
<p>하지만 모든 페이지를 같은 주기로 다시 수집하면 비용이 너무 커진다.</p>
<p>따라서</p>
<pre><code class="language-text">변경이 잦은 Page
→ 자주 Recrawl

중요한 Page
→ 자주 Recrawl

거의 바뀌지 않는 Page
→ 긴 주기로 Recrawl</code></pre>
<p>처럼 재수집 주기를 다르게 가져갈 수 있다.</p>
<hr>
<h1 id="25-url-frontier의-저장-방식">25. URL Frontier의 저장 방식</h1>
<p>대규모 크롤러는 수억~수십억 개의 URL을 관리할 수 있다.</p>
<p>모든 URL을 메모리에 올리면 공간이 부족하다.</p>
<p>하지만 모든 작업을 Disk에서 처리하면 I/O 때문에 느려질 수 있다.</p>
<p>따라서</p>
<pre><code class="language-text">Disk
→ 대부분의 URL 저장

Memory
→ 현재 처리에 필요한 URL Buffer</code></pre>
<p>를 함께 사용하는 방식이 가능하다.</p>
<p>그리고 메모리 상태는 주기적으로 디스크에 기록하여 장애 발생 시 복구할 수 있도록 한다.</p>
<hr>
<h1 id="26-robotstxt">26. robots.txt</h1>
<p>웹사이트에는 크롤러가 어떤 경로를 수집해도 되는지 알려주는 <code>robots.txt</code>가 존재할 수 있다.</p>
<p>일반적으로</p>
<pre><code class="language-text">https://example.com/robots.txt</code></pre>
<p>에서 확인한다.</p>
<p>대표적인 규칙으로는</p>
<pre><code class="language-text">User-agent
Disallow</code></pre>
<p>등이 있다.</p>
<p>크롤러는 이를 확인하여 사이트가 제외하도록 요청한 경로를 수집하지 않도록 한다.</p>
<p>다만 <code>robots.txt</code>는 보안 장치가 아니라 <strong>크롤러가 따라야 하는 정책을 알리는 방식</strong>이다.</p>
<hr>
<h1 id="27-robotstxt도-캐싱한다">27. robots.txt도 캐싱한다</h1>
<p>페이지를 요청할 때마다 <code>robots.txt</code>를 다시 다운로드하면 비효율적이다.</p>
<p>따라서</p>
<pre><code class="language-text">robots.txt 조회
      ↓
Cache 저장
      ↓
재사용
      ↓
일정 시간 후 갱신</code></pre>
<p>하는 방식으로 처리할 수 있다.</p>
<hr>
<h1 id="28-html-downloader-성능-최적화">28. HTML Downloader 성능 최적화</h1>
<h2 id="분산-크롤링">분산 크롤링</h2>
<p>하나의 Downloader가 모든 페이지를 처리하지 않는다.</p>
<pre><code class="language-text">URL Frontier
 ├─ Downloader 1
 ├─ Downloader 2
 ├─ Downloader 3
 └─ Downloader N</code></pre>
<p>각 서버 내부에서도 여러 Worker Thread를 사용할 수 있다.</p>
<p>이를 통해 전체 처리량을 높인다.</p>
<hr>
<h2 id="dns-결과-캐시">DNS 결과 캐시</h2>
<p>매번 DNS를 조회하면 그만큼 지연이 발생한다.</p>
<p>따라서</p>
<pre><code class="language-text">example.com
→ IP</code></pre>
<p>결과를 일정 시간 동안 캐시한다.</p>
<hr>
<h2 id="지역성">지역성</h2>
<p>가능하다면 크롤링 서버와 대상 서버 사이의 물리적 거리를 줄여 네트워크 Latency를 감소시킬 수 있다.</p>
<p>캐시, Queue, 저장소 역시 지역적으로 적절하게 배치할 수 있다.</p>
<hr>
<h2 id="짧은-timeout">짧은 Timeout</h2>
<p>특정 서버가 응답하지 않는다고 Worker 하나가 오랫동안 기다리면 처리량이 떨어진다.</p>
<pre><code class="language-text">Request
   ↓
Timeout 초과
   ↓
Abort
   ↓
다음 URL</code></pre>
<p>따라서 적절한 Timeout 정책이 필요하다.</p>
<hr>
<h1 id="29-장애가-발생해도-이어서-작업하기">29. 장애가 발생해도 이어서 작업하기</h1>
<p>대규모 크롤러는 언제든 중단될 수 있다.</p>
<p>따라서</p>
<ul>
<li>현재 처리 중인 URL</li>
<li>완료한 URL</li>
<li>Frontier 상태</li>
<li>수집한 데이터</li>
</ul>
<p>등을 지속성 저장소에 기록해둘 수 있다.</p>
<pre><code class="language-text">Crawler 장애
   ↓
재시작
   ↓
저장된 상태 조회
   ↓
중단 지점부터 재개</code></pre>
<hr>
<h1 id="30-안정-해시-활용">30. 안정 해시 활용</h1>
<p>Downloader 서버를 여러 대 운영한다면 URL을 여러 서버로 분배해야 한다.</p>
<p>이때 이전 장에서 배운 Consistent Hashing을 적용할 수 있다.</p>
<pre><code class="language-text">URL
 ↓ Hash
Downloader Server</code></pre>
<p>서버가 추가되거나 제거되어도 일부 URL만 재배치되므로 확장에 유리하다.</p>
<hr>
<h1 id="31-예외-처리">31. 예외 처리</h1>
<p>인터넷에는 예측할 수 없는 상황이 많다.</p>
<ul>
<li>DNS 실패</li>
<li>HTTP 오류</li>
<li>잘못된 HTML</li>
<li>다운로드 중 연결 종료</li>
<li>지원하지 않는 콘텐츠 형식</li>
</ul>
<p>따라서 하나의 URL에서 예외가 발생해도 전체 크롤링 작업은 계속되어야 한다.</p>
<pre><code class="language-text">URL A → Error

URL B → 계속 처리
URL C → 계속 처리</code></pre>
<hr>
<h1 id="32-데이터-검증">32. 데이터 검증</h1>
<p>다운로드한 데이터 역시 검증해야 한다.</p>
<p>예를 들어</p>
<ul>
<li>콘텐츠 크기</li>
<li>HTML 구조</li>
<li>인코딩</li>
<li>파일 형식</li>
</ul>
<p>등을 확인할 수 있다.</p>
<p>이 단계에서 비정상적인 콘텐츠를 걸러내면 이후 시스템의 안정성을 높일 수 있다.</p>
<hr>
<h1 id="33-새로운-콘텐츠-형식-추가">33. 새로운 콘텐츠 형식 추가</h1>
<p>웹에는 HTML뿐 아니라 다양한 콘텐츠가 존재한다.</p>
<p>따라서 크롤러를 모듈화하여 새로운 처리 방식을 추가할 수 있도록 한다.</p>
<pre><code class="language-text">Crawler Core
 ├─ HTML Parser
 ├─ URL Extractor
 ├─ PNG Downloader
 ├─ PDF Processor
 └─ Web Monitor</code></pre>
<p>새로운 요구사항이 생길 때 전체 크롤러를 수정하지 않고 새로운 모듈만 추가하는 것이 목표다.</p>
<hr>
<h1 id="34-문제-있는-콘텐츠-제거">34. 문제 있는 콘텐츠 제거</h1>
<p>웹에는 크롤러의 리소스를 낭비하게 만드는 콘텐츠도 많다.</p>
<p>대표적으로</p>
<pre><code class="language-text">중복 콘텐츠
Spider Trap
데이터 노이즈</code></pre>
<p>가 있다.</p>
<hr>
<h1 id="35-중복-콘텐츠">35. 중복 콘텐츠</h1>
<p>서로 다른 URL이 동일한 내용을 제공할 수 있다.</p>
<pre><code class="language-text">URL A ─┐
       ├→ 동일 Content
URL B ─┘</code></pre>
<p>이를 모두 저장하면 공간이 낭비된다.</p>
<p>따라서 콘텐츠의 Hash나 Checksum을 비교하여 중복 여부를 확인한다.</p>
<hr>
<h1 id="36-spider-trap">36. Spider Trap</h1>
<p>크롤러가 사실상 끝없이 새로운 URL을 발견하도록 만들어진 구조다.</p>
<p>예를 들어</p>
<pre><code class="language-text">/foo/bar
/foo/bar/foo/bar
/foo/bar/foo/bar/foo/bar
...</code></pre>
<p>처럼 URL이 계속 늘어날 수 있다.</p>
<p>크롤러 입장에서는 모두 서로 다른 URL처럼 보일 수 있다.</p>
<p>이런 상황에서는</p>
<ul>
<li>URL 최대 길이 제한</li>
<li>반복 패턴 감지</li>
<li>문제 사이트 차단</li>
<li>제외 목록 관리</li>
</ul>
<p>등을 활용할 수 있다.</p>
<p>다만 모든 Spider Trap을 자동으로 완벽하게 찾아내기는 어렵다.</p>
<hr>
<h1 id="37-데이터-노이즈">37. 데이터 노이즈</h1>
<p>크롤링 목적과 크게 관련 없는 데이터도 존재한다.</p>
<p>예를 들어</p>
<ul>
<li>광고</li>
<li>Spam</li>
<li>반복 콘텐츠</li>
<li>불필요한 코드</li>
</ul>
<p>등이다.</p>
<p>이런 데이터까지 전부 저장하면 저장 공간과 후속 처리 비용이 증가한다.</p>
<p>따라서 URL Filter와 Content Parser를 이용해 가능한 한 제거한다.</p>
<hr>
<h1 id="38-javascript-기반-페이지">38. JavaScript 기반 페이지</h1>
<p>현대 웹사이트는 HTML 안에 모든 콘텐츠가 처음부터 들어 있지 않을 수 있다.</p>
<p>JavaScript가 실행된 뒤 콘텐츠나 링크가 생성될 수 있다.</p>
<pre><code class="language-text">HTML 다운로드
    ↓
JavaScript 실행
    ↓
추가 Content 생성</code></pre>
<p>단순 HTTP 다운로드만 수행하면 이런 데이터를 확인하지 못할 수 있다.</p>
<p>필요한 경우 실제 렌더링 과정까지 수행하는 구조를 고려해야 한다.</p>
<hr>
<h1 id="39-저장소-확장">39. 저장소 확장</h1>
<p>크롤러가 수집하는 데이터가 계속 증가하면 저장 계층도 확장해야 한다.</p>
<h2 id="replication">Replication</h2>
<p>동일한 데이터를 여러 서버에 복제한다.</p>
<pre><code class="language-text">Data
 ├→ DB1
 └→ DB2</code></pre>
<p>장애 대응과 읽기 가용성을 높일 수 있다.</p>
<h2 id="sharding">Sharding</h2>
<p>데이터 자체를 여러 서버로 나눈다.</p>
<pre><code class="language-text">전체 데이터
   ↓
Shard 1
Shard 2
Shard 3</code></pre>
<p>저장 공간과 처리량을 수평으로 확장할 수 있다.</p>
<hr>
<h1 id="40-downloader-scale-out">40. Downloader Scale-out</h1>
<p>Downloader는 가능하면 Stateless하게 구성한다.</p>
<pre><code class="language-text">Downloader 1
Downloader 2
Downloader 3
...</code></pre>
<p>현재 크롤링 상태는 외부 저장소가 관리하고 Downloader는 전달받은 URL의 다운로드에 집중한다.</p>
<p>그러면 트래픽에 따라 서버를 쉽게 추가하거나 제거할 수 있다.</p>
<hr>
<h1 id="41-모니터링과-분석">41. 모니터링과 분석</h1>
<p>웹 크롤러도 실제로 어떻게 동작하는지 지속적으로 확인해야 한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Crawling Success Rate
Response Time
Duplicate Ratio
Error Rate
Page Update Frequency
Host Request Rate
Crawler Throughput</code></pre>
<p>등을 수집할 수 있다.</p>
<p>이 데이터를 분석하여</p>
<ul>
<li>URL Priority</li>
<li>Recrawl 주기</li>
<li>Timeout</li>
<li>Filtering</li>
<li>서버 수</li>
</ul>
<p>등을 조정할 수 있다.</p>
<hr>
<h1 id="전체-흐름-정리">전체 흐름 정리</h1>
<p>이번 장의 전체 흐름을 연결하면 다음과 같다.</p>
<pre><code class="language-text">Seed URL 선정
      ↓
URL Frontier 저장
      ↓
Priority에 따라 URL 선택
      ↓
Host별 요청 간격 확인
      ↓
HTML Downloader
      ↓
DNS 조회
      ↓
페이지 다운로드
      ↓
Content Parsing
      ↓
중복 Content 검사
      ↓
Content Storage
      ↓
URL Extract
      ↓
URL Filtering
      ↓
방문 여부 검사
      ↓
새로운 URL을 Frontier에 추가
      ↓
반복</code></pre>
<p>대규모 환경에서는 여기에</p>
<pre><code class="language-text">분산 Downloader
+
Consistent Hashing
+
Cache
+
Persistent Storage
+
Replication / Sharding
+
Failure Handling
+
Monitoring</code></pre>
<p>등이 추가된다.</p>
<p>결국 웹 크롤러 설계의 핵심은</p>
<blockquote>
<p><strong>가능한 많은 페이지를 빠르게 수집하는 것이 아니라, 가치 있는 페이지를 우선적으로 선택하고 대상 사이트에 부담을 주지 않으면서 안정적으로 수집하는 것</strong></p>
</blockquote>
<p>이라고 이해할 수 있다.</p>
<hr>
<h1 id="공부하면서-정리한-핵심">공부하면서 정리한 핵심</h1>
<p>처음에는 웹 크롤러를</p>
<pre><code class="language-text">페이지 방문
→ 링크 추출
→ 다음 페이지 방문</code></pre>
<p>정도의 단순한 반복 프로그램으로 생각했다.</p>
<p>하지만 대규모 환경에서는 URL을 발견하는 것보다 <strong>발견한 URL을 어떻게 관리할 것인가</strong>가 훨씬 중요한 문제였다.</p>
<p>특히 URL Frontier가 단순한 FIFO Queue 하나가 아니라</p>
<pre><code class="language-text">Front Queue
→ 중요한 페이지 우선

Back Queue
→ 동일 Host 요청 속도 제어</code></pre>
<p>처럼 두 가지 문제를 함께 해결한다는 점이 인상 깊었다.</p>
<p>또 웹 크롤러는 내 시스템의 성능만 생각하면 되는 것이 아니라 <strong>크롤링 대상 서버에 피해를 주지 않는 Politeness도 시스템 요구사항으로 고려해야 한다는 점</strong>도 새로웠다.</p>
<p>결국 이번 장은</p>
<pre><code class="language-text">BFS
→ Priority
→ Politeness
→ Recrawl
→ Distributed Crawling
→ Failure Handling</code></pre>
<p>처럼 단순한 그래프 탐색 알고리즘이 실제 대규모 웹 수집 시스템으로 확장되는 과정을 배운 장이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 8장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-8%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-8%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 04 Aug 2026 11:56:45 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-7%EC%9E%A5-%EC%A0%95%EB%A6%AC-38e8tq6r">[대규모 시스템 설계 스터디] 7장 정리</a>
다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-9%EC%9E%A5-%EC%A0%95%EB%A6%AC-u2nl6iya">[대규모 시스템 설계 스터디] 9장 정리</a></p>
</blockquote>
<h1 id="8장-url-단축기-설계">8장. URL 단축기 설계</h1>
<p>URL 단축 서비스는 긴 URL을 짧은 형태로 변환하고, 사용자가 단축 URL에 접근하면 다시 원래 주소로 이동시키는 서비스다.</p>
<p>예를 들어</p>
<pre><code class="language-text">https://example.com/very/long/address

↓

https://tinyurl.com/2TX</code></pre>
<p>처럼 긴 주소 대신 짧은 URL을 사용할 수 있도록 만든다.</p>
<p>겉으로 보면 단순하게 문자열 하나를 짧게 바꾸는 기능처럼 보이지만, 실제 대규모 서비스에서는</p>
<ul>
<li>단축 키의 유일성</li>
<li>빠른 조회</li>
<li>리디렉션</li>
<li>대량의 읽기 요청 처리</li>
<li>데이터 영구 저장</li>
<li>서버와 DB 확장</li>
</ul>
<p>등을 함께 고려해야 한다.</p>
<hr>
<h1 id="1-요구사항-정리">1. 요구사항 정리</h1>
<p>우선 URL 단축 서비스가 제공해야 할 핵심 기능을 정리한다.</p>
<h2 id="url-단축">URL 단축</h2>
<p>사용자가 긴 URL을 입력하면 짧은 URL을 생성한다.</p>
<pre><code class="language-text">Long URL
   ↓
URL Shortener
   ↓
Short URL</code></pre>
<hr>
<h2 id="url-리디렉션">URL 리디렉션</h2>
<p>사용자가 생성된 단축 URL에 접근하면 원본 URL로 이동시킨다.</p>
<pre><code class="language-text">Short URL
   ↓
URL Shortener
   ↓
Original URL</code></pre>
<hr>
<h2 id="높은-가용성과-확장성">높은 가용성과 확장성</h2>
<p>서비스 규모가 커지더라도 계속 요청을 처리할 수 있어야 한다.</p>
<p>즉</p>
<pre><code class="language-text">대량 Request
+
서버 장애
+
데이터 증가</code></pre>
<p>상황에서도 서비스가 정상적으로 동작하도록 설계해야 한다.</p>
<hr>
<h1 id="2-시스템-규모-추정">2. 시스템 규모 추정</h1>
<p>설계 전에 어느 정도 규모의 시스템인지 계산해본다.</p>
<p>이번 장에서는 다음과 같이 가정한다.</p>
<pre><code class="language-text">하루 URL 생성
= 1억 개</code></pre>
<p>이를 초당 요청으로 환산하면 약</p>
<pre><code class="language-text">100,000,000 / 86,400
≈ 1,160 Write / sec</code></pre>
<p>이다.</p>
<hr>
<h2 id="읽기-요청">읽기 요청</h2>
<p>URL 단축 서비스는 URL을 만드는 요청보다 기존 URL을 조회하는 요청이 훨씬 많다고 가정한다.</p>
<p>읽기와 쓰기의 비율을</p>
<pre><code class="language-text">Read : Write
= 10 : 1</code></pre>
<p>로 잡으면</p>
<pre><code class="language-text">Read QPS
≈ 11,600</code></pre>
<p>정도가 된다.</p>
<p>즉 URL 단축 서비스는 <strong>쓰기보다 읽기가 많은 시스템</strong>이다.</p>
<p>이 특징은 뒤에서 캐시를 도입하는 중요한 근거가 된다.</p>
<hr>
<h1 id="3-저장-공간-추정">3. 저장 공간 추정</h1>
<p>하루에 1억 개의 URL을 만들고 이를 10년 동안 보관한다고 가정한다.</p>
<pre><code class="language-text">1억 × 365 × 10
≈ 3,650억 Records</code></pre>
<p>원본 URL 평균 크기를 약 100Byte로 잡으면</p>
<pre><code class="language-text">3,650억 × 100Byte
≈ 36.5TB</code></pre>
<p>정도의 저장 공간이 필요하다.</p>
<p>실제 시스템에서는 추가적인 메타데이터와 인덱스 등의 공간도 필요하겠지만, 개략적인 규모를 파악하기 위한 계산이다.</p>
<hr>
<h1 id="4-api-설계">4. API 설계</h1>
<p>URL 단축 서비스에서는 크게 두 가지 API가 필요하다.</p>
<pre><code class="language-text">URL 생성
+
URL 조회</code></pre>
<p>REST 형태로 생각해볼 수 있다.</p>
<hr>
<h2 id="url-단축-api">URL 단축 API</h2>
<p>사용자가 원본 URL을 전달하면 새로운 단축 URL을 생성한다.</p>
<pre><code class="language-http">POST /api/v1/data/shorten</code></pre>
<p>요청 예시</p>
<pre><code class="language-json">{
  &quot;longUrl&quot;: &quot;https://example.com/articles/2026/very-long-address&quot;
}</code></pre>
<p>서버는 이에 대응하는 단축 URL을 반환한다.</p>
<pre><code class="language-text">https://tinyurl.com/abc1234</code></pre>
<hr>
<h1 id="5-url-리디렉션-api">5. URL 리디렉션 API</h1>
<p>사용자가 단축 URL에 접근하면 서버에서 원본 URL을 조회한다.</p>
<p>개념적으로는</p>
<pre><code class="language-http">GET /{shortKey}</code></pre>
<p>와 같이 단축 키를 이용한 요청으로 볼 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">https://tinyurl.com/abc1234</code></pre>
<p>에 접근하면 서버는</p>
<pre><code class="language-text">abc1234
   ↓
Original URL 조회
   ↓
https://example.com/...</code></pre>
<p>과정을 거친다.</p>
<p>이후 브라우저에 Redirect 응답을 반환한다.</p>
<hr>
<h1 id="6-url-리디렉션">6. URL 리디렉션</h1>
<p>사용자가 단축 URL을 클릭하면 다음과 같은 흐름이 발생한다.</p>
<pre><code class="language-text">Short URL 요청
     ↓
URL Shortener
     ↓
Original URL 조회
     ↓
Redirect Response
     ↓
Original Website</code></pre>
<p>Redirect 응답에서는 대표적으로 <code>301</code> 또는 <code>302</code> 상태 코드를 사용할 수 있다.</p>
<hr>
<h1 id="7-301과-302의-차이">7. 301과 302의 차이</h1>
<h2 id="301-moved-permanently">301 Moved Permanently</h2>
<p>해당 URL이 새로운 URL로 <strong>영구적으로 이동했다</strong>는 의미다.</p>
<pre><code class="language-text">Short URL
   ↓
301
   ↓
Original URL</code></pre>
<p>브라우저나 중간 캐시가 Redirect 결과를 저장할 수 있다.</p>
<p>따라서 이후 동일한 단축 URL에 접근할 때 URL 단축 서버를 거치지 않고 기존에 저장된 Redirect 정보를 활용할 가능성이 있다.</p>
<h3 id="장점">장점</h3>
<pre><code class="language-text">URL Shortener 요청 감소
→ 서버 부하 감소</code></pre>
<p>하지만 사용자가 URL 단축 서버를 다시 방문하지 않는다면 클릭 횟수를 서버에서 직접 집계하기 어려워질 수 있다.</p>
<hr>
<h2 id="302-found">302 Found</h2>
<p>일시적인 Redirect를 나타낸다.</p>
<pre><code class="language-text">Short URL
   ↓
302
   ↓
Original URL</code></pre>
<p>단축 URL을 계속 유지하면서 목적지로 보내야 하는 상황에서 활용할 수 있다.</p>
<p>요청이 URL Shortener를 거치는 구조라면</p>
<ul>
<li>클릭 수 분석</li>
<li>트래픽 분석</li>
<li>목적지 URL 변경</li>
</ul>
<p>등을 처리하기 편하다.</p>
<p>다만 <code>302</code>라고 해서 항상 절대로 캐시되지 않는다고 단정할 수는 없으며, 실제 캐시 동작은 응답 헤더 등의 정책에도 영향을 받을 수 있다.</p>
<hr>
<h1 id="8-url-매핑은-어떻게-저장할까">8. URL 매핑은 어떻게 저장할까?</h1>
<p>URL Shortener가 기억해야 할 핵심 데이터는 다음과 같다.</p>
<pre><code class="language-text">Short Key → Long URL</code></pre>
<p>예를 들어</p>
<pre><code class="language-text">2TX
→
https://example.com/very/long/address</code></pre>
<p>형태다.</p>
<p>이를 Key-Value 구조로 바라보면 매우 단순하다.</p>
<pre><code class="language-text">Key   = 2TX
Value = Original URL</code></pre>
<hr>
<h1 id="9-해시-값으로-url을-복원하는-것은-아니다">9. 해시 값으로 URL을 복원하는 것은 아니다</h1>
<p>처음에는 단축 문자열 자체에서 원본 URL을 다시 복원하는 방식이라고 생각할 수도 있다.</p>
<p>하지만 여기서 다루는 구조는 그렇지 않다.</p>
<pre><code class="language-text">zn9edcu
   ↓
Database 조회
   ↓
Original URL</code></pre>
<p>즉 <code>zn9edcu</code>라는 단축 키를 이용해서 DB에 저장해둔 원본 URL을 찾는 방식이다.</p>
<p>따라서 서버는 다음과 같은 매핑 정보를 저장해야 한다.</p>
<pre><code class="language-text">short_key
↔
long_url</code></pre>
<hr>
<h1 id="10-해시-테이블만-사용하면-안-될까">10. 해시 테이블만 사용하면 안 될까?</h1>
<p>애플리케이션 내부의 Hash Table을 사용하면 매우 빠르게 데이터를 조회할 수 있다.</p>
<p>하지만 일반적인 Hash Table은 메모리에 존재하기 때문에 대규모 URL 데이터를 영구적으로 저장하는 용도로는 한계가 있다.</p>
<h3 id="문제점">문제점</h3>
<ul>
<li>서버 종료 시 데이터 손실 가능</li>
<li>RAM 비용이 높음</li>
<li>저장 가능한 데이터 양에 한계</li>
<li>여러 서버가 데이터를 공유하기 어려움</li>
<li>백업 및 복구가 어려움</li>
</ul>
<p>따라서 원본 URL 매핑 정보는 데이터베이스에 저장하고, 자주 조회하는 데이터만 캐시에 보관하는 구조를 생각할 수 있다.</p>
<pre><code class="language-text">Database
→ 영구 저장

Cache
→ 빠른 조회</code></pre>
<hr>
<h1 id="11-데이터-모델">11. 데이터 모델</h1>
<p>단순한 테이블을 만든다면 다음과 같은 구조를 생각할 수 있다.</p>
<pre><code class="language-text">ID
short_key
long_url</code></pre>
<p>예를 들어</p>
<table>
<thead>
<tr>
<th align="right">ID</th>
<th>short_key</th>
<th>long_url</th>
</tr>
</thead>
<tbody><tr>
<td align="right">1001</td>
<td>abc1234</td>
<td><a href="https://example.com/">https://example.com/</a>...</td>
</tr>
</tbody></table>
<p>와 같은 형태다.</p>
<p>이때 <code>short_key</code>는 중복되면 안 된다.</p>
<p>서로 다른 두 원본 URL이 동일한 단축 키를 가지면 어떤 URL로 Redirect해야 할지 판단할 수 없기 때문이다.</p>
<p>따라서 DB 수준에서도 <code>UNIQUE</code> 제약조건을 사용할 수 있다.</p>
<pre><code class="language-sql">CREATE UNIQUE INDEX idx_short_key
ON url_mapping(short_key);</code></pre>
<p>즉 애플리케이션 로직뿐 아니라 데이터베이스에서도 최종적으로 중복을 막는다.</p>
<hr>
<h1 id="12-단축-키는-어떻게-만들까">12. 단축 키는 어떻게 만들까?</h1>
<p>핵심 문제는 다음과 같다.</p>
<blockquote>
<p><strong>긴 URL을 어떤 짧은 문자열과 연결할 것인가?</strong></p>
</blockquote>
<p>단축 키는 다음 문자들을 사용한다고 가정한다.</p>
<pre><code class="language-text">0 ~ 9  → 10개
a ~ z  → 26개
A ~ Z  → 26개</code></pre>
<p>총</p>
<pre><code class="language-text">10 + 26 + 26
= 62개</code></pre>
<p>의 문자를 사용할 수 있다.</p>
<hr>
<h1 id="13-단축-키-길이">13. 단축 키 길이</h1>
<p>약 3,650억 개 이상의 URL을 표현해야 한다고 가정한다.</p>
<p>6자리 Base62 문자열은 표현 가능한 경우의 수가 부족하지만</p>
<pre><code class="language-text">62^7</code></pre>
<p>은 수조 개의 조합을 만들 수 있다.</p>
<p>따라서 약 7자리 정도의 단축 키를 사용할 수 있다.</p>
<pre><code class="language-text">abc123A
z9K20Qa
...</code></pre>
<hr>
<h1 id="14-단축-키를-만드는-두-가지-방법">14. 단축 키를 만드는 두 가지 방법</h1>
<p>이번 장에서는 크게 두 가지 방법을 비교한다.</p>
<pre><code class="language-text">1. Hash + Collision Resolution

2. Unique ID + Base62</code></pre>
<p>두 방법 모두 짧은 키를 만들 수 있지만 접근 방식이 다르다.</p>
<hr>
<h1 id="15-hash-방식">15. Hash 방식</h1>
<p>원본 URL에 해시 함수를 적용한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Long URL
   ↓
MD5 / SHA / CRC32
   ↓
Hash Value</code></pre>
<p>와 같은 구조다.</p>
<p>문제는 일반적인 Hash 결과가 단축 URL에 사용하기에는 너무 길다는 것이다.</p>
<p>따라서 결과 중 일부만 사용한다.</p>
<pre><code class="language-text">전체 Hash
↓
앞 7글자 추출
↓
abc1234</code></pre>
<hr>
<h1 id="16-hash-충돌-문제">16. Hash 충돌 문제</h1>
<p>Hash 결과를 일부만 사용하면 서로 다른 URL이 같은 단축 키를 만들 가능성이 생긴다.</p>
<p>예를 들어</p>
<pre><code class="language-text">URL A
→ abc1234

URL B
→ abc1234</code></pre>
<p>가 발생할 수 있다.</p>
<p>따라서 충돌을 확인하고 다시 생성하는 과정이 필요하다.</p>
<hr>
<h1 id="17-충돌-해결-과정">17. 충돌 해결 과정</h1>
<p>예를 들어 다음과 같이 처리할 수 있다.</p>
<pre><code class="language-text">Long URL
   ↓
Hash
   ↓
abc1234
   ↓
DB 확인
   ↓
이미 존재
   ↓
Salt 추가
   ↓
다시 Hash
   ↓
k9x82Qa</code></pre>
<p>과정은 다음과 같다.</p>
<ol>
<li>원본 URL Hash</li>
<li>결과 일부를 Short Key로 선택</li>
<li>DB에서 해당 Key 존재 여부 확인</li>
<li>없으면 저장</li>
<li>이미 있다면 Salt 등의 값을 추가</li>
<li>다시 Hash</li>
<li>충돌이 없어질 때까지 반복</li>
</ol>
<hr>
<h1 id="18-db의-unique-제약조건-활용">18. DB의 UNIQUE 제약조건 활용</h1>
<p>애플리케이션에서 미리 DB를 조회했다고 해서 충돌이 완전히 사라지는 것은 아니다.</p>
<p>여러 서버가 동시에 같은 Key가 비어 있다고 판단할 수도 있기 때문이다.</p>
<p>따라서 실제 저장 단계에서도</p>
<pre><code class="language-text">Short Key 생성
      ↓
INSERT
      ↓
UNIQUE 충돌?
  ↙         ↘
No          Yes
↓             ↓
완료        새 Key 생성</code></pre>
<p>형태로 처리할 수 있다.</p>
<p>즉 DB의 UNIQUE 제약조건이 마지막 안전장치 역할을 한다.</p>
<hr>
<h1 id="19-bloom-filter-활용">19. Bloom Filter 활용</h1>
<p>Short Key의 존재 가능성을 빠르게 판단하기 위해 Bloom Filter와 같은 자료구조를 보조적으로 고려할 수도 있다.</p>
<pre><code class="language-text">새 Short Key
    ↓
Bloom Filter
    ↓
절대 없음
→ 저장 시도

있을 수도 있음
→ 추가 확인</code></pre>
<p>Bloom Filter는</p>
<pre><code class="language-text">&quot;없다&quot;
→ 확실하게 없음

&quot;있다&quot;
→ 실제로는 없을 수도 있음</code></pre>
<p>이라는 특징을 가진다.</p>
<p>따라서 불필요한 조회를 줄이기 위한 보조 수단으로 활용할 수 있다.</p>
<p>다만 최종적인 중복 여부는 실제 저장소의 상태를 확인해야 한다.</p>
<hr>
<h1 id="20-hash-방식의-특징">20. Hash 방식의 특징</h1>
<h2 id="장점-1">장점</h2>
<ul>
<li>원본 URL을 기반으로 키 생성 가능</li>
<li>별도의 순차 숫자 ID가 없어도 됨</li>
<li>여러 서버에서 Hash 계산 가능</li>
</ul>
<h2 id="단점">단점</h2>
<ul>
<li>Hash 충돌 가능</li>
<li>충돌 검사 및 재시도 로직 필요</li>
<li>동일한 URL이 여러 번 들어왔을 때 처리 정책 필요</li>
<li>일부 Hash 값만 사용하므로 추가적인 충돌 관리 필요</li>
</ul>
<hr>
<h1 id="21-base62-방식">21. Base62 방식</h1>
<p>두 번째 방법은 원본 URL을 바로 Base62로 변환하는 것이 아니다.</p>
<p>먼저 각 URL에 <strong>전역적으로 유일한 숫자 ID</strong>를 부여한다.</p>
<pre><code class="language-text">Long URL
   ↓
Unique ID Generator
   ↓
2009215674938</code></pre>
<p>그리고 이 숫자를 Base62 문자열로 변환한다.</p>
<pre><code class="language-text">2009215674938
   ↓
Base62
   ↓
zn9edcu</code></pre>
<p>이 <code>zn9edcu</code>가 Short Key가 된다.</p>
<hr>
<h1 id="22-base62-변환-원리">22. Base62 변환 원리</h1>
<p>Base62는 62개의 문자를 숫자처럼 사용하는 방식이다.</p>
<p>예를 들어</p>
<pre><code class="language-text">0~9
a~z
A~Z</code></pre>
<p>를 하나의 숫자 체계로 사용한다.</p>
<p>10진수를 62로 계속 나누면서 나머지를 문자로 변환한다.</p>
<p>예를 들어 <code>11157</code>을 변환하면</p>
<pre><code class="language-text">11157 ÷ 62
→ 몫 179
→ 나머지 59

179 ÷ 62
→ 몫 2
→ 나머지 55

2 ÷ 62
→ 몫 0
→ 나머지 2</code></pre>
<p>나머지를 대응되는 문자로 바꾸고 역순으로 읽으면 Short Key가 만들어진다.</p>
<p>예시에서는</p>
<pre><code class="language-text">2TX</code></pre>
<p>와 같은 결과를 얻을 수 있다.</p>
<hr>
<h1 id="23-base62-방식의-전체-흐름">23. Base62 방식의 전체 흐름</h1>
<pre><code class="language-text">Long URL
   ↓
Unique Numeric ID 생성
   ↓
Base62 변환
   ↓
Short Key
   ↓
DB 저장</code></pre>
<p>예를 들어</p>
<pre><code class="language-text">Original URL
https://en.wikipedia.org/wiki/Systems_design</code></pre>
<p>에</p>
<pre><code class="language-text">ID
2009215674938</code></pre>
<p>을 발급한다.</p>
<p>이 숫자를 Base62로 변환해</p>
<pre><code class="language-text">zn9edcu</code></pre>
<p>라는 Short Key를 만든다.</p>
<p>그러면 DB에는 다음과 같이 저장된다.</p>
<table>
<thead>
<tr>
<th align="right">ID</th>
<th>short_key</th>
<th>long_url</th>
</tr>
</thead>
<tbody><tr>
<td align="right">2009215674938</td>
<td>zn9edcu</td>
<td><a href="https://en.wikipedia.org/wiki/Systems_design">https://en.wikipedia.org/wiki/Systems_design</a></td>
</tr>
</tbody></table>
<p>최종 URL은</p>
<pre><code class="language-text">https://tinyurl.com/zn9edcu</code></pre>
<p>가 된다.</p>
<hr>
<h1 id="24-hash-방식과-base62-방식-비교">24. Hash 방식과 Base62 방식 비교</h1>
<h2 id="hash-방식">Hash 방식</h2>
<pre><code class="language-text">Long URL
   ↓
Hash
   ↓
일부 문자 추출
   ↓
충돌 검사
   ↓
Short Key</code></pre>
<p>URL 자체를 이용해 Key를 만든다.</p>
<p>하지만 충돌 관리가 필요하다.</p>
<hr>
<h2 id="base62-방식">Base62 방식</h2>
<pre><code class="language-text">Long URL
   ↓
Unique Numeric ID
   ↓
Base62
   ↓
Short Key</code></pre>
<p>유일한 숫자 ID가 보장된다면 Base62 결과 역시 유일하게 만들 수 있다.</p>
<p>대신 <strong>유일한 숫자 ID를 생성하는 시스템</strong>이 필요하다.</p>
<p>지난 장에서 학습했던 분산 ID 생성기와도 연결할 수 있는 부분이다.</p>
<hr>
<h1 id="25-url-생성-전체-흐름">25. URL 생성 전체 흐름</h1>
<p>이번에는 Base62 방식을 이용한다고 가정한다.</p>
<p>전체적인 URL 생성 과정은 다음과 같다.</p>
<pre><code class="language-text">Long URL 입력
      ↓
기존 등록 여부 확인
  ↙             ↘
있음             없음
 ↓                ↓
기존 URL 반환   Unique ID 생성
                  ↓
               Base62
                  ↓
               DB 저장
                  ↓
            Short URL 반환</code></pre>
<p>조금 더 구체적으로 보면</p>
<ol>
<li>클라이언트가 Long URL 전송</li>
<li>기존에 등록된 URL인지 확인</li>
<li>존재한다면 기존 Short URL 반환</li>
<li>없다면 Unique ID 생성</li>
<li>ID를 Base62로 변환</li>
<li>ID, Short Key, Long URL 저장</li>
<li>Short URL 반환</li>
</ol>
<p>과정이다.</p>
<hr>
<h1 id="26-url-redirect-상세-설계">26. URL Redirect 상세 설계</h1>
<p>URL 단축 서비스는 쓰기보다 읽기 요청이 훨씬 많은 서비스다.</p>
<p>앞에서</p>
<pre><code class="language-text">Read : Write
= 10 : 1</code></pre>
<p>이라고 가정했다.</p>
<p>따라서 사용자가 단축 URL을 클릭할 때마다 항상 DB를 조회하면 DB 부하가 커질 수 있다.</p>
<p>이를 줄이기 위해 Cache를 사용할 수 있다.</p>
<hr>
<h1 id="27-캐시를-활용한-redirect">27. 캐시를 활용한 Redirect</h1>
<p>자주 사용되는</p>
<pre><code class="language-text">Short Key
→
Original URL</code></pre>
<p>매핑을 Redis 등의 캐시에 저장한다.</p>
<p>전체적인 흐름은 다음과 같다.</p>
<pre><code class="language-text">Short URL 요청
      ↓
Load Balancer
      ↓
Web Server
      ↓
Cache 조회
  ↙          ↘
Hit           Miss
 ↓              ↓
Redirect       DB 조회
                ↓
             Cache 저장
                ↓
             Redirect</code></pre>
<hr>
<h1 id="28-cache-hit">28. Cache Hit</h1>
<p>캐시에 Short Key가 존재한다면</p>
<pre><code class="language-text">zn9edcu
   ↓
Cache
   ↓
Original URL</code></pre>
<p>즉시 원본 URL을 찾아 Redirect할 수 있다.</p>
<p>DB 조회가 필요하지 않기 때문에 응답 속도가 빠르고 DB 부하도 줄어든다.</p>
<hr>
<h1 id="29-cache-miss">29. Cache Miss</h1>
<p>캐시에 데이터가 없다면 DB에서 조회한다.</p>
<pre><code class="language-text">Cache Miss
     ↓
Database
     ↓
Original URL 발견
     ↓
Cache 저장
     ↓
Redirect</code></pre>
<p>다음번 동일한 URL 요청에서는 캐시에서 바로 처리할 수 있다.</p>
<hr>
<h1 id="30-존재하지-않는-short-url">30. 존재하지 않는 Short URL</h1>
<p>Cache에도 없고 DB에도 해당 Short Key가 존재하지 않는다면 정상적인 URL이 아니다.</p>
<pre><code class="language-text">Short Key
   ↓
Cache 없음
   ↓
DB 없음
   ↓
오류 반환</code></pre>
<p>잘못 입력했거나 존재하지 않는 단축 URL일 수 있으므로 적절한 오류 응답을 반환한다.</p>
<hr>
<h1 id="31-load-balancer의-역할">31. Load Balancer의 역할</h1>
<p>여기서 Load Balancer가 Cache를 조회한다고 생각하면 안 된다.</p>
<p>Load Balancer의 역할은 <strong>요청을 여러 웹 서버로 분산하는 것</strong>이다.</p>
<pre><code class="language-text">               Client
                  ↓
           Load Balancer
             ↙        ↘
       Web Server 1   Web Server 2
             ↓             ↓
             └── Cache / DB ──┘</code></pre>
<p>실제 Short Key 조회는 웹 서버나 관련 서비스가 수행한다.</p>
<hr>
<h1 id="32-전체-아키텍처">32. 전체 아키텍처</h1>
<p>지금까지의 내용을 하나로 연결하면 다음과 같은 구조를 생각할 수 있다.</p>
<pre><code class="language-text">              Client
                 ↓
          Load Balancer
                 ↓
        ┌────────────────┐
        │   Web Servers  │
        └────────────────┘
           ↓          ↓
        Cache      Database</code></pre>
<p>URL 생성 요청에서는</p>
<pre><code class="language-text">Client
  ↓
Web Server
  ↓
ID Generator
  ↓
Base62
  ↓
Database</code></pre>
<p>과정을 사용하고,</p>
<p>URL 조회 요청에서는</p>
<pre><code class="language-text">Client
  ↓
Web Server
  ↓
Cache
  ↓ Miss
Database</code></pre>
<p>구조를 사용할 수 있다.</p>
<hr>
<h1 id="33-서비스가-더-커진다면">33. 서비스가 더 커진다면?</h1>
<p>기본적인 URL Shortener를 만들었다면 다음 단계는 확장성을 고려하는 것이다.</p>
<hr>
<h2 id="rate-limiter">Rate Limiter</h2>
<p>악의적인 사용자나 Bot이 짧은 시간에 매우 많은 URL 생성 요청을 보낼 수 있다.</p>
<pre><code class="language-text">Bot
↓↓↓↓↓↓↓↓↓↓
URL 생성 Request</code></pre>
<p>이 경우 앞에서 학습한 Rate Limiter를 이용해 사용자 또는 IP 단위로 URL 생성 횟수를 제한할 수 있다.</p>
<hr>
<h2 id="web-server-scale-out">Web Server Scale-out</h2>
<p>웹 계층을 Stateless하게 구성하면 트래픽에 따라 웹 서버를 추가할 수 있다.</p>
<pre><code class="language-text">Web Server 1
Web Server 2
Web Server 3
...</code></pre>
<p>Load Balancer는 이 서버들에 요청을 분산한다.</p>
<hr>
<h2 id="database-확장">Database 확장</h2>
<p>데이터가 계속 증가한다면 하나의 DB로 처리하기 어려워질 수 있다.</p>
<p>이 경우</p>
<pre><code class="language-text">Replication
또는
Sharding</code></pre>
<p>등의 방법을 고려할 수 있다.</p>
<hr>
<h2 id="분석-시스템">분석 시스템</h2>
<p>URL 단축 서비스에서는 단순히 Redirect만 하는 것이 아니라 클릭 데이터를 분석할 수도 있다.</p>
<p>예를 들어</p>
<ul>
<li>클릭 수</li>
<li>시간대</li>
<li>유입 위치</li>
<li>디바이스</li>
<li>인기 URL</li>
</ul>
<p>등을 수집하면 서비스 분석에도 활용할 수 있다.</p>
<hr>
<h1 id="34-안정성과-가용성">34. 안정성과 가용성</h1>
<p>URL Shortener가 대규모 서비스라면</p>
<pre><code class="language-text">Web Server 장애
Database 장애
Cache 장애</code></pre>
<p>등의 상황도 고려해야 한다.</p>
<p>따라서</p>
<ul>
<li>서버 다중화</li>
<li>DB 복제</li>
<li>캐시 장애 대응</li>
<li>모니터링</li>
<li>데이터 백업</li>
</ul>
<p>등을 함께 설계해야 한다.</p>
<hr>
<h1 id="전체-흐름-정리">전체 흐름 정리</h1>
<p>이번 장의 전체 흐름을 연결하면 다음과 같다.</p>
<pre><code class="language-text">긴 URL을 짧게 만들고 싶음
        ↓
Short Key 필요
        ↓
어떻게 생성할 것인가?
        ↓
Hash
vs
Unique ID + Base62
        ↓
Short Key 생성
        ↓
DB에
Short Key ↔ Long URL 저장
        ↓
사용자가 Short URL 클릭
        ↓
Load Balancer
        ↓
Web Server
        ↓
Cache 조회
        ↓
Cache Hit
→ 바로 Redirect
        ↓
Cache Miss
→ DB 조회
→ Cache 저장
→ Redirect
        ↓
트래픽 증가
        ↓
Rate Limiter
Web Server Scale-out
DB Replication / Sharding</code></pre>
<p>결국 URL 단축 서비스의 핵심은 단순히 문자열을 짧게 만드는 것이 아니다.</p>
<blockquote>
<p><strong>원본 URL에 대응하는 짧고 유일한 식별자를 생성하고, 이를 안정적으로 저장한 뒤 대량의 Redirect 요청을 빠르게 처리하는 시스템을 만드는 것</strong></p>
</blockquote>
<p>이라고 이해할 수 있다.</p>
<hr>
<h1 id="공부하면서-정리한-핵심">공부하면서 정리한 핵심</h1>
<p>처음에는 URL 단축 서비스라고 하면</p>
<pre><code class="language-text">긴 URL
↓
Hash
↓
짧은 URL</code></pre>
<p>정도로 생각했다.</p>
<p>하지만 실제로는 Hash 결과에서 원래 URL을 복원하는 것이 아니라</p>
<pre><code class="language-text">Short Key
↓
DB / Cache 조회
↓
Original URL</code></pre>
<p>이라는 구조라는 점이 가장 먼저 정리되었다.</p>
<p>또 Short Key를 만드는 방법도 하나만 존재하는 것이 아니었다.</p>
<pre><code class="language-text">Hash 방식
→ URL 자체를 이용
→ Collision 처리 필요

Base62 방식
→ Unique ID를 먼저 생성
→ 짧은 문자열로 인코딩</code></pre>
<p>처럼 서로 다른 접근 방법이 존재한다.</p>
<p>특히 지난 장에서 배운 <strong>분산 ID 생성기</strong>를 이용하면 이번 장의 Base62 방식에서 필요한 Unique Numeric ID를 생성할 수 있다는 점에서 이전 내용과 연결되는 느낌이 들었다.</p>
<p>또 URL 단축 서비스는 생성보다 Redirect 요청이 훨씬 많기 때문에</p>
<pre><code class="language-text">Database
+
Cache</code></pre>
<p>구조를 사용하여 읽기 요청을 빠르게 처리한다는 점도 중요했다.</p>
<p>결국 이번 장 역시 하나의 기능만 보는 것이 아니라</p>
<pre><code class="language-text">API
→ ID 생성
→ DB
→ Cache
→ Load Balancer
→ Rate Limiter
→ Scale-out</code></pre>
<p>처럼 이전에 학습한 여러 시스템 구성 요소들이 하나의 실제 서비스 안에서 어떻게 연결되는지를 확인할 수 있었던 장이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 7장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-7%EC%9E%A5-%EC%A0%95%EB%A6%AC-38e8tq6r</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-7%EC%9E%A5-%EC%A0%95%EB%A6%AC-38e8tq6r</guid>
            <pubDate>Thu, 30 Jul 2026 02:51:02 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 6장 정리</a>
다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-8%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 8장 정리</a></p>
</blockquote>
<h1 id="7장-분산-시스템을-위한-유일-id-생성기-설계">7장. 분산 시스템을 위한 유일 ID 생성기 설계</h1>
<p>데이터베이스에서 새로운 데이터를 생성할 때는 각 데이터를 구분하기 위한 고유한 ID가 필요하다.</p>
<p>일반적인 관계형 데이터베이스에서는 <code>auto_increment</code>를 이용할 수 있다.</p>
<pre><code class="language-text">1
2
3
4
5
...</code></pre>
<p>새로운 Row가 추가될 때마다 이전 값보다 1 큰 값을 자동으로 부여하는 방식이다.</p>
<p>하나의 데이터베이스만 사용하는 환경이라면 매우 간단한 해결책이다.</p>
<p>하지만 시스템 규모가 커져 여러 서버에서 동시에 ID를 생성해야 한다면 이야기가 달라진다.</p>
<p>이번 장에서 해결하려는 문제는</p>
<blockquote>
<p><strong>여러 서버가 동시에 ID를 만들어도 중복되지 않고, 빠르게 생성할 수 있으며, 시간순 정렬까지 가능한 ID 생성기를 어떻게 만들 것인가?</strong></p>
</blockquote>
<p>이다.</p>
<hr>
<h1 id="1-왜-auto_increment만-사용하면-안-될까">1. 왜 auto_increment만 사용하면 안 될까?</h1>
<p>가장 먼저 떠오르는 방법은 하나의 데이터베이스에서 ID를 계속 증가시키는 것이다.</p>
<pre><code class="language-text">INSERT
  ↓
DB
  ↓
AUTO_INCREMENT
  ↓
1001</code></pre>
<p>하지만 대규모 분산 환경에서는 몇 가지 문제가 생긴다.</p>
<hr>
<h2 id="1-하나의-db에-요청이-집중된다">1. 하나의 DB에 요청이 집중된다</h2>
<p>모든 서비스가 ID를 얻기 위해 하나의 데이터베이스를 거쳐야 한다면 ID 생성 요청이 해당 DB로 집중된다.</p>
<pre><code class="language-text">Server A ─┐
Server B ─┼→ ID Database
Server C ─┤
Server D ─┘</code></pre>
<p>트래픽이 계속 증가하면 ID 생성 기능 자체가 병목 지점이 될 수 있다.</p>
<hr>
<h2 id="2-여러-db에서-만들면-중복-문제가-생긴다">2. 여러 DB에서 만들면 중복 문제가 생긴다</h2>
<p>이번에는 DB 자체를 여러 개로 늘렸다고 생각해보자.</p>
<pre><code class="language-text">DB A → 1, 2, 3 ...
DB B → 1, 2, 3 ...</code></pre>
<p>두 서버가 서로 독립적으로 ID를 생성하면 동일한 ID가 만들어질 수 있다.</p>
<p>이를 방지하기 위한 추가적인 규칙이나 동기화가 필요해진다.</p>
<hr>
<h2 id="3-수평-확장이-어려워진다">3. 수평 확장이 어려워진다</h2>
<p>대규모 시스템에서는 서버를 계속 추가하거나 제거할 수 있어야 한다.</p>
<p>그런데 ID 발급을 하나의 중앙 DB에 의존하면 ID 생성기 자체가 시스템 확장을 방해할 수 있다.</p>
<p>따라서 분산 환경에서는 단순한 증가 숫자가 아니라</p>
<pre><code class="language-text">Unique
+
Scalable
+
Low Latency</code></pre>
<p>를 만족하는 별도의 ID 생성 전략이 필요하다.</p>
<hr>
<h1 id="2-요구사항부터-정리">2. 요구사항부터 정리</h1>
<p>이번에 설계할 ID 생성기의 조건을 먼저 정리한다.</p>
<ul>
<li>모든 ID는 유일해야 함</li>
<li>숫자로만 구성</li>
<li>64비트 안에서 표현 가능</li>
<li>대체로 생성 시간 순서대로 정렬 가능</li>
<li>초당 10,000개 이상의 ID 생성 가능</li>
</ul>
<p>중요한 점은 ID가 반드시</p>
<pre><code class="language-text">1001
1002
1003
1004</code></pre>
<p>처럼 정확하게 1씩 증가해야 하는 것은 아니라는 것이다.</p>
<p>대신</p>
<pre><code class="language-text">아침에 생성한 ID
&lt;
저녁에 생성한 ID</code></pre>
<p>처럼 <strong>시간이 흐를수록 대체로 더 큰 값을 가지면 된다.</strong></p>
<hr>
<h1 id="3-어떤-방법을-사용할-수-있을까">3. 어떤 방법을 사용할 수 있을까?</h1>
<p>유일 ID를 만드는 방법은 하나가 아니다.</p>
<p>이번 장에서는 크게 네 가지 방법을 비교한다.</p>
<pre><code class="language-text">Multi-Master Replication
UUID
Ticket Server
Snowflake</code></pre>
<p>각각의 특징을 확인한 뒤 요구사항에 가장 적합한 방법을 선택한다.</p>
<hr>
<h1 id="4-multi-master-replication">4. Multi-Master Replication</h1>
<p>여러 데이터베이스의 <code>auto_increment</code>를 활용하는 방법이다.</p>
<p>단순하게 모두 1씩 증가시키면 ID가 겹치기 때문에 서버마다 증가 규칙을 다르게 만든다.</p>
<p>예를 들어 DB가 2대라면</p>
<pre><code class="language-text">DB A
1, 3, 5, 7 ...

DB B
2, 4, 6, 8 ...</code></pre>
<p>처럼 생성할 수 있다.</p>
<p>서버가 <code>K</code>대라면 각 서버가 ID를 <code>K</code>씩 증가시키도록 구성하는 방식이다.</p>
<hr>
<h2 id="장점">장점</h2>
<p>기존 데이터베이스의 <code>auto_increment</code>를 활용할 수 있기 때문에 비교적 단순하다.</p>
<p>또 여러 DB가 ID 생성 작업을 나누어 처리할 수 있다.</p>
<hr>
<h2 id="단점">단점</h2>
<p>서버 수가 변경되면 증가 규칙도 영향을 받을 수 있다.</p>
<p>또 여러 데이터 센터와 서버에서 생성된 ID를 완벽한 시간순으로 유지하기 어렵다.</p>
<pre><code class="language-text">DB 추가
또는
DB 제거
   ↓
기존 ID 생성 규칙 변경 필요</code></pre>
<p>따라서 서버가 자주 증가하거나 감소하는 대규모 분산 환경에서는 관리하기 까다롭다.</p>
<hr>
<h1 id="5-uuid">5. UUID</h1>
<p>또 다른 방법은 UUID를 사용하는 것이다.</p>
<p>UUID는 매우 큰 ID 공간을 이용하여 서로 다른 서버에서도 독립적으로 식별자를 생성할 수 있도록 한다.</p>
<pre><code class="language-text">Server A → UUID 생성
Server B → UUID 생성
Server C → UUID 생성</code></pre>
<p>별도의 중앙 ID 서버가 필요하지 않다는 것이 큰 특징이다.</p>
<hr>
<h2 id="장점-1">장점</h2>
<h3 id="서버끼리-동기화할-필요가-없다">서버끼리 동기화할 필요가 없다</h3>
<p>각 서버가 독립적으로 ID를 만들 수 있다.</p>
<pre><code class="language-text">Server A ─→ ID
Server B ─→ ID
Server C ─→ ID</code></pre>
<p>따라서 서버를 추가하기 쉽다.</p>
<h3 id="구현이-비교적-단순하다">구현이 비교적 단순하다</h3>
<p>별도의 중앙 ID 생성기를 운영할 필요가 없다.</p>
<hr>
<h2 id="하지만-이번-문제에는-맞지-않는다">하지만 이번 문제에는 맞지 않는다</h2>
<p>이번 요구사항은</p>
<pre><code class="language-text">64bit
+
숫자
+
시간순 정렬</code></pre>
<p>이다.</p>
<p>일반적인 UUID는 128비트이고 표현 방식에 문자와 <code>-</code> 등이 들어갈 수 있으며, 이번 문제에서 요구하는 형태와 맞지 않는다.</p>
<p>따라서 UUID 역시 이번 설계에서는 제외한다.</p>
<hr>
<h1 id="6-ticket-server">6. Ticket Server</h1>
<p>ID를 발급하는 별도의 중앙 서버를 두는 방법이다.</p>
<pre><code class="language-text">Server A ─┐
Server B ─┼→ Ticket Server
Server C ─┘
               ↓
          Unique ID</code></pre>
<p>Ticket Server 내부에서는 <code>auto_increment</code>와 같은 방식을 이용하여 숫자를 발급할 수 있다.</p>
<hr>
<h2 id="장점-2">장점</h2>
<p>모든 ID가 하나의 서버에서 만들어지므로 유일성을 관리하기 쉽다.</p>
<pre><code class="language-text">1001
1002
1003
1004</code></pre>
<p>형태의 단순한 숫자 ID도 만들 수 있다.</p>
<p>구조 역시 비교적 이해하기 쉽다.</p>
<hr>
<h2 id="문제점">문제점</h2>
<p>가장 큰 문제는 <strong>SPOF(Single Point of Failure)</strong>다.</p>
<pre><code class="language-text">Ticket Server 장애
        ↓
ID 생성 불가능
        ↓
새로운 데이터 생성 불가능</code></pre>
<p>서비스의 여러 기능이 새로운 ID 발급에 의존하고 있다면 ID 서버 장애가 시스템 전체에 큰 영향을 줄 수 있다.</p>
<p>Ticket Server 자체를 여러 대로 만들면 다시 서버 간 ID 동기화 문제를 해결해야 한다.</p>
<hr>
<h1 id="7-snowflake-방식">7. Snowflake 방식</h1>
<p>앞의 방법들을 비교하면 각각 한계가 있다.</p>
<pre><code class="language-text">Multi-Master
→ 확장과 시간순 정렬 문제

UUID
→ 128bit, 요구 형식과 불일치

Ticket Server
→ SPOF 문제</code></pre>
<p>따라서 이번 요구사항에서는 <strong>Snowflake 방식</strong>을 이용한다.</p>
<p>Snowflake의 핵심 아이디어는 굉장히 흥미롭다.</p>
<blockquote>
<p>하나의 64비트 숫자 안에 ID를 유일하게 만들기 위해 필요한 여러 정보를 나누어 저장한다.</p>
</blockquote>
<p>즉 단순한 무작위 숫자가 아니라, 각 비트 영역에 의미를 부여한다.</p>
<hr>
<h1 id="8-snowflake-id-구조">8. Snowflake ID 구조</h1>
<p>구조를 단순화하면 다음과 같다.</p>
<pre><code class="language-text">┌──────┬─────────────┬─────────────┬───────────┬────────────┐
│ Sign │ Timestamp   │ Data Center │ Worker ID │ Sequence   │
│ 1bit │ 41bit       │ 5bit        │ 5bit      │ 12bit      │
└──────┴─────────────┴─────────────┴───────────┴────────────┘</code></pre>
<p>합치면</p>
<pre><code class="language-text">1 + 41 + 5 + 5 + 12
= 64bit</code></pre>
<p>가 된다.</p>
<p>각 영역은 서로 다른 역할을 담당한다.</p>
<hr>
<h1 id="9-sign-bit">9. Sign Bit</h1>
<p>가장 앞의 1비트다.</p>
<pre><code class="language-text">0 | ...</code></pre>
<p>부호를 표현할 수 있는 위치지만 Snowflake ID에서는 일반적으로 양수 값을 사용하기 위해 남겨둔다고 이해할 수 있다.</p>
<p>실제로 중요한 정보는 뒤의 63비트에 들어간다.</p>
<hr>
<h1 id="10-timestamp---41bit">10. Timestamp - 41bit</h1>
<p>가장 많은 공간을 차지하는 부분이다.</p>
<pre><code class="language-text">Timestamp = 41bit</code></pre>
<p>특정 기준 시각인 <strong>Epoch</strong>부터 현재까지 몇 밀리초가 지났는지를 기록한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Custom Epoch
     ↓
─────┬─────────────── 현재
     │
     └→ 경과 Milliseconds</code></pre>
<p>형태다.</p>
<hr>
<h2 id="timestamp가-중요한-이유">Timestamp가 중요한 이유</h2>
<p>시간이 계속 흐르기 때문에 Timestamp 값도 증가한다.</p>
<pre><code class="language-text">10:00 생성
Timestamp = X

10:01 생성
Timestamp = X + ...</code></pre>
<p>그리고 Timestamp가 ID의 상위 비트를 차지하기 때문에 전체 ID 역시 시간의 흐름에 따라 대체로 증가하게 된다.</p>
<p>즉 요구사항이었던</p>
<blockquote>
<p>생성 시간에 따른 정렬 가능</p>
</blockquote>
<p>을 만족시킬 수 있다.</p>
<hr>
<h1 id="11-data-center-id---5bit">11. Data Center ID - 5bit</h1>
<p>다음 5비트는 ID가 어느 데이터 센터에서 생성되었는지 구분하는 영역이다.</p>
<p>5비트로 표현할 수 있는 경우의 수는</p>
<pre><code class="language-text">2^5 = 32</code></pre>
<p>이므로 0부터 31까지 총 32개의 값을 표현할 수 있다.</p>
<pre><code class="language-text">Data Center 0
Data Center 1
...
Data Center 31</code></pre>
<hr>
<h1 id="12-worker-id---5bit">12. Worker ID - 5bit</h1>
<p>그 다음 5비트는 데이터 센터 내부의 Worker 또는 서버를 구분한다.</p>
<p>마찬가지로</p>
<pre><code class="language-text">2^5 = 32</code></pre>
<p>개의 값을 사용할 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Data Center 3

Worker 0
Worker 1
Worker 2
...
Worker 31</code></pre>
<p>처럼 사용할 수 있다.</p>
<p>결국</p>
<pre><code class="language-text">Data Center ID
+
Worker ID</code></pre>
<p>조합을 통해 <strong>어느 위치의 어느 서버가 ID를 생성했는지 구분할 수 있다.</strong></p>
<p>그래서 여러 서버가 동시에 ID를 생성하더라도 서로 다른 Worker 정보를 가지면 ID가 겹치지 않는다.</p>
<hr>
<h1 id="13-sequence-number---12bit">13. Sequence Number - 12bit</h1>
<p>마지막 12비트는 같은 서버에서 <strong>같은 밀리초 안에 여러 ID가 생성되는 상황</strong>을 처리한다.</p>
<p>12비트는</p>
<pre><code class="language-text">2^12 = 4096</code></pre>
<p>개의 값을 표현할 수 있다.</p>
<p>즉</p>
<pre><code class="language-text">0 ~ 4095</code></pre>
<p>범위를 사용할 수 있다.</p>
<hr>
<h1 id="14-왜-sequence가-필요할까">14. 왜 Sequence가 필요할까?</h1>
<p>Timestamp는 밀리초 단위다.</p>
<p>그런데 하나의 서버가 같은 1ms 안에 ID를 두 개 생성한다고 생각해보자.</p>
<p>첫 번째 ID가</p>
<pre><code class="language-text">Timestamp = 1000
Worker = 3</code></pre>
<p>이고 두 번째 ID 역시 같은 밀리초라면</p>
<pre><code class="language-text">Timestamp = 1000
Worker = 3</code></pre>
<p>으로 동일하다.</p>
<p>Timestamp와 Worker만으로는 두 ID를 구분할 수 없다.</p>
<p>그래서 Sequence를 증가시킨다.</p>
<pre><code class="language-text">첫 번째
Timestamp = 1000
Sequence = 0

두 번째
Timestamp = 1000
Sequence = 1

세 번째
Timestamp = 1000
Sequence = 2</code></pre>
<p>이를 통해 같은 서버가 같은 밀리초에 여러 ID를 만들어도 서로 다른 값을 만들 수 있다.</p>
<hr>
<h1 id="15-시간이-바뀌면-sequence는">15. 시간이 바뀌면 Sequence는?</h1>
<p>밀리초가 달라졌다면 이미 Timestamp 자체가 달라진다.</p>
<p>따라서 Sequence를 다시 0부터 시작할 수 있다.</p>
<pre><code class="language-text">10:00:00.001

Sequence
0
1
2

     ↓ 다음 ms

10:00:00.002

Sequence
0
1
...</code></pre>
<p>즉 Sequence가 0보다 큰 값을 가지는 것은 <strong>같은 밀리초 동안 여러 개의 ID를 생성할 때</strong>다.</p>
<hr>
<h1 id="16-snowflake가-유일한-id를-만드는-원리">16. Snowflake가 유일한 ID를 만드는 원리</h1>
<p>Snowflake의 구조를 다시 보면 이유가 명확해진다.</p>
<pre><code class="language-text">Timestamp
+
Data Center
+
Worker
+
Sequence</code></pre>
<h3 id="서로-다른-시간">서로 다른 시간</h3>
<p>Timestamp가 다르다.</p>
<pre><code class="language-text">→ ID 중복 X</code></pre>
<h3 id="같은-시간이지만-다른-data-center">같은 시간이지만 다른 Data Center</h3>
<p>Data Center ID가 다르다.</p>
<pre><code class="language-text">→ ID 중복 X</code></pre>
<h3 id="같은-data-center지만-다른-worker">같은 Data Center지만 다른 Worker</h3>
<p>Worker ID가 다르다.</p>
<pre><code class="language-text">→ ID 중복 X</code></pre>
<h3 id="같은-서버에서-같은-밀리초">같은 서버에서 같은 밀리초</h3>
<p>Sequence가 다르다.</p>
<pre><code class="language-text">→ ID 중복 X</code></pre>
<p>즉 여러 정보를 하나의 숫자 안에 조합하여 분산 환경에서도 ID를 구분한다.</p>
<hr>
<h1 id="17-네-가지-방법-비교">17. 네 가지 방법 비교</h1>
<p>지금까지 살펴본 방법을 정리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>Multi-Master</td>
<td>기존 DB 활용 가능</td>
<td>서버 변화 및 정렬 관리 어려움</td>
</tr>
<tr>
<td>UUID</td>
<td>독립 생성, 확장 쉬움</td>
<td>128bit, 요구 형식과 불일치</td>
</tr>
<tr>
<td>Ticket Server</td>
<td>단순, 숫자 ID 생성</td>
<td>중앙 서버가 SPOF</td>
</tr>
<tr>
<td>Snowflake</td>
<td>분산 생성, 64bit, 시간순 정렬</td>
<td>시간 및 Worker 관리 필요</td>
</tr>
</tbody></table>
<p>이번 요구사항에서는</p>
<pre><code class="language-text">Unique
+
64bit
+
Numeric
+
Time Sortable
+
Distributed</code></pre>
<p>를 만족해야 하므로 Snowflake 방식이 가장 적합하다.</p>
<hr>
<h1 id="18-data-center-id와-worker-id-관리">18. Data Center ID와 Worker ID 관리</h1>
<p>Snowflake에서 중요한 점은 Data Center ID와 Worker ID가 서로 겹치지 않도록 관리하는 것이다.</p>
<p>예를 들어 두 서버가 실수로 동일한 값을 사용한다면</p>
<pre><code class="language-text">Server A
DataCenter = 1
Worker = 5

Server B
DataCenter = 1
Worker = 5</code></pre>
<p>같은 시간과 같은 Sequence가 만들어졌을 때 ID 충돌 가능성이 생길 수 있다.</p>
<p>따라서 Worker를 추가하거나 설정을 변경할 때 ID 할당을 엄격하게 관리해야 한다.</p>
<hr>
<h1 id="19-가장-중요한-운영-문제-시간">19. 가장 중요한 운영 문제: 시간</h1>
<p>Snowflake는 Timestamp를 ID 구성에 사용한다.</p>
<p>따라서 서버 시간이 잘못되면 문제가 발생할 수 있다.</p>
<p>특히 시간이 이전으로 돌아가는 <strong>Clock Rollback</strong> 문제가 중요하다.</p>
<pre><code class="language-text">현재 시간
10:00:01

        ↓ Clock 문제

09:59:59</code></pre>
<p>이미 미래 Timestamp를 이용해 ID를 생성한 상태에서 더 작은 시간이 들어오면 시간순 정렬뿐만 아니라 ID 생성 로직에도 문제가 생길 수 있다.</p>
<p>원래 Twitter Snowflake 구현 역시 현재 시간이 직전 ID 생성 시간보다 과거로 이동한 경우 ID 생성을 그대로 진행하지 않고 오류로 처리하도록 구현되어 있다.</p>
<p>따라서 분산 환경에서는 서버 시간 관리가 중요하다.</p>
<hr>
<h1 id="20-비트-배분은-반드시-고정일까">20. 비트 배분은 반드시 고정일까?</h1>
<p>Snowflake의 핵심 아이디어는 <strong>하나의 정수 공간을 여러 영역으로 나누는 것</strong>이다.</p>
<p>따라서 서비스 특성에 따라 비트 구성을 조절한 변형 설계도 생각할 수 있다.</p>
<p>예를 들어 서버 수가 많다면</p>
<pre><code class="language-text">Worker ID Bit ↑</code></pre>
<p>가 필요할 수 있다.</p>
<p>반대로 하나의 서버에서 매우 많은 ID를 만들어야 한다면</p>
<pre><code class="language-text">Sequence Bit ↑</code></pre>
<p>를 고려할 수 있다.</p>
<p>서비스를 아주 오랜 기간 운영해야 한다면 Timestamp 영역 역시 중요하다.</p>
<p>즉 비트 배분에서도</p>
<pre><code class="language-text">서비스 수명
vs
서버 개수
vs
밀리초당 생성량</code></pre>
<p>사이의 Trade-off가 존재한다.</p>
<hr>
<h1 id="21-고가용성">21. 고가용성</h1>
<p>ID 생성기는 얼핏 보면 작은 기능처럼 보이지만 실제로는 많은 시스템의 시작점이 될 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">회원 생성
주문 생성
게시글 생성
메시지 생성</code></pre>
<p>등에서 새로운 ID가 필요하다.</p>
<p>ID 생성기가 멈추면 새로운 데이터를 만들 수 없는 서비스도 생길 수 있다.</p>
<p>따라서</p>
<blockquote>
<p><strong>ID Generator 자체 역시 높은 가용성을 갖도록 설계해야 한다.</strong></p>
</blockquote>
<p>Snowflake 방식의 장점 중 하나는 하나의 중앙 서버에만 ID 생성 책임을 집중하지 않고 여러 Worker가 ID를 생성할 수 있다는 점이다.</p>
<hr>
<h1 id="전체-흐름-정리">전체 흐름 정리</h1>
<p>이번 장의 설계 과정을 연결하면 다음과 같다.</p>
<pre><code class="language-text">Unique ID 필요
       ↓
DB auto_increment?
       ↓
단일 DB 병목
+
분산 DB 중복 문제
       ↓
다른 방법 비교
       ↓
Multi-Master
UUID
Ticket Server
Snowflake
       ↓
요구사항 확인
       ↓
64bit
Numeric
Unique
Time Sortable
Distributed
       ↓
Snowflake 선택
       ↓
64bit 공간 분할
       ↓
Timestamp
+
Data Center
+
Worker
+
Sequence
       ↓
여러 서버가 독립적으로
Unique ID 생성
       ↓
Clock / Worker ID 관리
       ↓
고가용성 확보</code></pre>
<p>결국 Snowflake의 핵심은</p>
<blockquote>
<p><strong>중앙 데이터베이스에서 번호를 하나씩 받아오는 대신, 시간과 서버 정보 그리고 일련번호를 조합하여 각 서버가 독립적으로 유일한 ID를 만들도록 하는 것</strong></p>
</blockquote>
<p>이라고 이해할 수 있다.</p>
<hr>
<h1 id="공부하면서-정리한-핵심">공부하면서 정리한 핵심</h1>
<p>처음에는 유일한 ID를 만드는 것이 그렇게 어려운 문제라고 생각하지 않았다.</p>
<p>데이터베이스에서</p>
<pre><code class="language-text">AUTO_INCREMENT</code></pre>
<p>를 사용하면 끝나는 문제라고 생각했기 때문이다.</p>
<p>하지만 서버가 여러 대가 되는 순간</p>
<pre><code class="language-text">동시에 같은 ID가 만들어지면?
ID 서버가 죽으면?
서버를 계속 추가하면?
ID를 시간순으로 정렬해야 한다면?</code></pre>
<p>같은 문제가 발생한다.</p>
<p>Snowflake는 이 문제를 해결하기 위해 ID를 단순한 숫자가 아니라 <strong>여러 정보를 압축해서 넣은 하나의 구조체처럼 사용한다는 점</strong>이 가장 흥미로웠다.</p>
<pre><code class="language-text">64bit ID

= 시간
+ 데이터 센터
+ 서버
+ 같은 밀리초의 순서</code></pre>
<p>라는 구조를 이해하고 나니 왜 분산 환경에서 별도의 ID 생성 전략이 필요한지 훨씬 명확해졌다.</p>
<p>결국 이번 장에서 중요했던 것은 Snowflake의 비트 수 자체를 외우는 것보다</p>
<blockquote>
<p><strong>중앙 서버에 의존하지 않으면서도 각 서버가 충돌 없이 ID를 생성하려면 어떤 정보를 조합해야 하는가</strong></p>
</blockquote>
<p>라는 설계 아이디어였던 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 6장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 26 Jul 2026 08:12:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-5%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 5장 정리</a>
다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-7%EC%9E%A5-%EC%A0%95%EB%A6%AC-38e8tq6r">[ 대규모 시스템 설계 스터디 ] 7장 정리</a></p>
</blockquote>
<h1 id="6장-키-값-저장소-설계">6장. 키-값 저장소 설계</h1>
<p>키-값 저장소(Key-Value Store)는 데이터를</p>
<pre><code class="language-text">Key → Value</code></pre>
<p>형태로 저장하는 비관계형 데이터베이스다.</p>
<p>예를 들어</p>
<pre><code class="language-text">&quot;user:1001&quot; → 사용자 정보
&quot;session:abc&quot; → 세션 데이터
&quot;product:15&quot; → 상품 정보</code></pre>
<p>처럼 고유한 Key를 이용해 Value를 저장하고 조회한다.</p>
<p>키-값 저장소는 NoSQL의 한 종류다.</p>
<pre><code class="language-text">Database
│
├─ RDBMS
│   ├─ MySQL
│   ├─ PostgreSQL
│   └─ Oracle
│
└─ NoSQL
    ├─ Document
    │   └─ MongoDB
    │
    ├─ Key-Value
    │   ├─ Redis
    │   └─ DynamoDB
    │
    ├─ Column-Family
    │   ├─ Cassandra
    │   └─ HBase
    │
    └─ Graph
        └─ Neo4j</code></pre>
<p>이번 장에서는 단순한 Key-Value 저장소에서 출발해, 이를 <strong>대규모 분산 환경에서도 동작하는 저장소로 확장하는 과정</strong>을 살펴본다.</p>
<hr>
<h1 id="1-먼저-요구사항부터-정리">1. 먼저 요구사항부터 정리</h1>
<p>설계할 시스템의 요구사항은 다음과 같다.</p>
<ul>
<li>Key-Value 데이터 크기는 10KB 이하</li>
<li>많은 데이터를 저장할 수 있어야 함</li>
<li>높은 가용성</li>
<li>서버 장애가 발생해도 빠른 응답 제공</li>
<li>트래픽에 따라 서버를 추가하거나 제거할 수 있는 확장성</li>
<li>일관성 수준 조정 가능</li>
<li>낮은 Latency</li>
</ul>
<p>즉 단순하게</p>
<pre><code class="language-text">put(key, value)
get(key)</code></pre>
<p>만 구현하는 것이 아니라,</p>
<blockquote>
<p><strong>서버가 여러 대이고 장애가 발생하는 상황에서도 빠르고 안정적으로 데이터를 저장하고 조회할 수 있어야 한다.</strong></p>
</blockquote>
<hr>
<h1 id="2-단일-서버에서-시작">2. 단일 서버에서 시작</h1>
<p>가장 단순하게는 한 대의 서버 메모리에 Hash Table을 만들 수 있다.</p>
<pre><code class="language-text">Key
 ↓
Hash Table
 ↓
Value</code></pre>
<p>메모리 기반이므로 매우 빠르게 데이터를 조회할 수 있다.</p>
<p>하지만 명확한 한계가 있다.</p>
<blockquote>
<p>모든 데이터를 한 서버의 메모리에 넣을 수는 없다.</p>
</blockquote>
<p>따라서 다음과 같은 방법을 사용할 수 있다.</p>
<pre><code class="language-text">자주 사용하는 데이터
→ Memory

나머지 데이터
→ Disk</code></pre>
<p>또는 데이터를 압축하여 메모리 사용량을 줄일 수도 있다.</p>
<p>하지만 데이터가 계속 증가하면 결국 한 대의 서버로는 감당하기 어렵다.</p>
<p>그래서 여러 서버를 사용하는 <strong>분산 Key-Value Store</strong>가 필요하다.</p>
<hr>
<h1 id="3-분산-시스템에서-가장-먼저-생각할-cap">3. 분산 시스템에서 가장 먼저 생각할 CAP</h1>
<p>여러 서버에 데이터를 저장하게 되면 새로운 문제가 발생한다.</p>
<p>대표적인 것이</p>
<pre><code class="language-text">Consistency
Availability
Partition Tolerance</code></pre>
<p>이다.</p>
<p>이를 설명하는 개념이 <strong>CAP 정리</strong>다.</p>
<hr>
<h2 id="consistency">Consistency</h2>
<p>어떤 서버에서 데이터를 읽더라도 동일한 값을 확인할 수 있어야 한다.</p>
<pre><code class="language-text">Server A → value = 10
Server B → value = 10
Server C → value = 10</code></pre>
<p>즉 사용자가 어느 노드에 접속하든 일관된 데이터를 볼 수 있는 상태다.</p>
<hr>
<h2 id="availability">Availability</h2>
<p>일부 서버에 장애가 발생하더라도 요청에 응답할 수 있어야 한다.</p>
<pre><code class="language-text">Server A ✕
Server B ○
Server C ○

→ 서비스 계속 제공</code></pre>
<hr>
<h2 id="partition-tolerance">Partition Tolerance</h2>
<p>분산 시스템에서는 서버 간 네트워크 연결에 문제가 생길 수 있다.</p>
<pre><code class="language-text">Server A  ✕──── Server B</code></pre>
<p>이처럼 서버끼리 통신할 수 없는 상황을 Network Partition이라고 볼 수 있다.</p>
<p>Partition Tolerance는 이런 상황에서도 시스템이 계속 동작할 수 있는 특성을 의미한다.</p>
<hr>
<h1 id="4-cp와-ap">4. CP와 AP</h1>
<p>분산 환경에서는 네트워크 장애를 완전히 피하기 어렵기 때문에 Partition Tolerance를 고려해야 한다.</p>
<p>따라서 실제 선택은 주로</p>
<pre><code class="language-text">CP
vs
AP</code></pre>
<p>사이에서 이루어진다.</p>
<hr>
<h2 id="cp">CP</h2>
<pre><code class="language-text">Consistency
+
Partition Tolerance</code></pre>
<p>데이터의 일관성을 우선한다.</p>
<p>Partition이 발생하여 최신 데이터를 보장할 수 없다면 일부 요청을 거절할 수도 있다.</p>
<hr>
<h2 id="ap">AP</h2>
<pre><code class="language-text">Availability
+
Partition Tolerance</code></pre>
<p>서비스 응답을 우선한다.</p>
<p>일부 서버 데이터가 아직 최신 상태가 아니더라도 사용자에게 응답을 반환할 수 있다.</p>
<p>이 경우 일시적으로 서로 다른 데이터가 보일 수 있다.</p>
<hr>
<h1 id="5-상황에-따라-선택이-달라진다">5. 상황에 따라 선택이 달라진다</h1>
<p>처음에는 데이터가 항상 최신 상태여야 하므로 CP가 무조건 더 중요하다고 생각했다.</p>
<p>하지만 실제 시스템에서는 <strong>서비스 중단 자체가 큰 문제</strong>가 될 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">좋아요 수
조회수
SNS Feed</code></pre>
<p>처럼 몇 초 정도 데이터가 다르더라도 큰 문제가 없는 서비스에서는 가용성을 높이고 이후 데이터를 맞추는 방법을 선택할 수 있다.</p>
<p>반면</p>
<pre><code class="language-text">결제
계좌 잔액
재고 차감</code></pre>
<p>처럼 데이터의 정확성이 중요한 영역에서는 더욱 강한 일관성이 필요하다.</p>
<p>결국 핵심은</p>
<blockquote>
<p><strong>Consistency와 Availability 중 무엇을 더 중요하게 볼지는 서비스 특성에 따라 결정된다.</strong></p>
</blockquote>
<hr>
<h1 id="6-분산-key-value-store의-구성-요소">6. 분산 Key-Value Store의 구성 요소</h1>
<p>이번 장에서 살펴보는 주요 요소는 다음과 같다.</p>
<pre><code class="language-text">Data Partitioning
Data Replication
Consistency
Conflict Resolution
Failure Detection
Failure Handling
Write Path
Read Path</code></pre>
<p>하나씩 연결해서 살펴보자.</p>
<hr>
<h1 id="7-데이터-파티션">7. 데이터 파티션</h1>
<p>데이터가 너무 많아서 하나의 서버에 저장할 수 없다면 여러 서버로 나눈다.</p>
<pre><code class="language-text">전체 데이터
    ↓
┌───────────────┐
│ Partition 1   │
│ Partition 2   │
│ Partition 3   │
└───────────────┘</code></pre>
<p>이때 중요한 조건은 두 가지다.</p>
<h3 id="데이터가-서버에-비교적-균등하게-분산되어야-한다">데이터가 서버에 비교적 균등하게 분산되어야 한다.</h3>
<p>특정 서버에 데이터가 몰리면 해당 서버만 병목이 된다.</p>
<h3 id="서버-추가·삭제-시-데이터-이동량이-적어야-한다">서버 추가·삭제 시 데이터 이동량이 적어야 한다.</h3>
<p>서버 하나가 추가될 때 전체 데이터를 다시 배치하면 비용이 너무 크다.</p>
<p>이 문제를 해결하기 위해 지난 장에서 배운 <strong>Consistent Hashing</strong>을 사용할 수 있다.</p>
<hr>
<h1 id="8-hash-ring-활용">8. Hash Ring 활용</h1>
<p>서버와 Key를 Hash Ring에 배치한다.</p>
<pre><code class="language-text">       S0
   ↗       ↘
 S3         S1
   ↖       ↙
       S2</code></pre>
<p>Key 위치에서 시계 방향으로 처음 만나는 서버가 해당 데이터를 담당한다.</p>
<p>이 구조의 장점은 서버가 추가되거나 제거되더라도 일부 구간의 데이터만 이동한다는 것이다.</p>
<p>또 Virtual Node를 활용하면 서버별 성능에 따라 담당 범위를 조정할 수도 있다.</p>
<p>예를 들어 고성능 서버에는 더 많은 Virtual Node를 할당할 수 있다.</p>
<hr>
<h1 id="9-데이터-다중화-replication">9. 데이터 다중화 Replication</h1>
<p>서버 하나에만 데이터를 저장하면 그 서버가 죽었을 때 데이터를 사용할 수 없다.</p>
<p>따라서 동일한 데이터를 여러 서버에 복제한다.</p>
<p>복제본의 개수를 <code>N</code>이라고 하자.</p>
<pre><code class="language-text">N = 3

Key A
 ↓
Server 1
Server 2
Server 3</code></pre>
<p>즉 같은 데이터를 세 개의 서버에 저장한다.</p>
<hr>
<h1 id="10-어느-서버에-복제할까">10. 어느 서버에 복제할까?</h1>
<p>Key를 Hash Ring에 배치하고 시계 방향으로 이동하면서 서로 다른 서버를 선택할 수 있다.</p>
<pre><code class="language-text">Key
 ↓
Server 1 → 원본
Server 2 → Replica
Server 3 → Replica</code></pre>
<p>Virtual Node를 사용한다면 여러 Virtual Node가 같은 실제 서버를 가리킬 수도 있다.</p>
<p>따라서 복제본을 선택할 때는 <strong>실제 물리 서버가 중복되지 않도록 확인해야 한다.</strong></p>
<p>또 데이터 센터 장애까지 고려한다면 복제본 일부를 다른 데이터 센터에 저장하는 방법도 사용할 수 있다.</p>
<hr>
<h1 id="11-복제했더니-새로운-문제가-생긴다">11. 복제했더니 새로운 문제가 생긴다</h1>
<p>복제본이 많아지면 가용성은 좋아진다.</p>
<p>하지만 동시에</p>
<blockquote>
<p><strong>모든 Replica가 항상 동일한 데이터를 가지고 있는가?</strong></p>
</blockquote>
<p>라는 문제가 생긴다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Server A → value = 10
Server B → value = 10
Server C → value = 9</code></pre>
<p>처럼 업데이트가 아직 전달되지 않은 서버가 있을 수 있다.</p>
<p>그래서 읽기와 쓰기를 몇 대의 서버에서 확인해야 할지 결정하는 <strong>Quorum</strong> 개념이 등장한다.</p>
<hr>
<h1 id="12-quorum">12. Quorum</h1>
<p>주요 값은 세 가지다.</p>
<pre><code class="language-text">N = 전체 Replica 수
W = 쓰기 성공에 필요한 Replica 수
R = 읽기에 필요한 Replica 수</code></pre>
<p>예를 들어</p>
<pre><code class="language-text">N = 3
W = 2
R = 2</code></pre>
<p>라면 데이터는 세 서버에 복제되고,</p>
<p>쓰기 요청은 최소 두 서버가 성공해야 완료로 판단하며,</p>
<p>읽을 때도 두 서버의 값을 확인한다.</p>
<hr>
<h1 id="13-coordinator">13. Coordinator</h1>
<p>클라이언트가 모든 Replica를 직접 관리하면 구조가 복잡해진다.</p>
<pre><code class="language-text">Client
 ├→ Server A
 ├→ Server B
 └→ Server C</code></pre>
<p>서버가 추가되거나 삭제될 때마다 클라이언트가 모든 정보를 알아야 하기 때문이다.</p>
<p>그래서 중간에 <strong>Coordinator</strong>를 둘 수 있다.</p>
<pre><code class="language-text">Client
   ↓
Coordinator
 ↙   ↓   ↘
S1   S2   S3</code></pre>
<p>Coordinator는 클라이언트를 대신하여</p>
<ul>
<li>적절한 노드 탐색</li>
<li>Read / Write 요청 전달</li>
<li>필요한 응답 수 확인</li>
</ul>
<p>등을 처리한다.</p>
<p>즉 클라이언트와 저장 노드 사이에서 프록시 역할을 한다.</p>
<hr>
<h1 id="14-n-w-r을-어떻게-정할까">14. N, W, R을 어떻게 정할까?</h1>
<p>설정에 따라 읽기와 쓰기의 성능 및 일관성이 달라진다.</p>
<h3 id="빠른-읽기">빠른 읽기</h3>
<pre><code class="language-text">R = 1
W = N</code></pre>
<p>쓰기 시 모든 Replica를 맞추기 때문에 읽을 때 하나만 확인해도 된다.</p>
<hr>
<h3 id="빠른-쓰기">빠른 쓰기</h3>
<pre><code class="language-text">W = 1
R = N</code></pre>
<p>한 서버만 쓰기에 성공해도 완료시킨다.</p>
<p>대신 읽을 때 여러 Replica를 확인해야 할 수 있다.</p>
<hr>
<h3 id="강한-일관성을-높이고-싶다면">강한 일관성을 높이고 싶다면</h3>
<pre><code class="language-text">W + R &gt; N</code></pre>
<p>이 되도록 설정하는 방법을 사용할 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">N = 3
W = 2
R = 2</code></pre>
<p>와 같은 형태다.</p>
<p>결국</p>
<blockquote>
<p><strong>Read 성능, Write 성능, Consistency 요구사항 사이에서 값을 조정한다.</strong></p>
</blockquote>
<hr>
<h1 id="15-일관성-모델">15. 일관성 모델</h1>
<p>분산 시스템에서는 데이터가 언제 동일해지는가에 따라 여러 일관성 모델을 생각할 수 있다.</p>
<h2 id="strong-consistency">Strong Consistency</h2>
<p>읽기 요청은 항상 최신 데이터를 반환한다.</p>
<pre><code class="language-text">Write
 ↓
모든 필요한 동기화 완료
 ↓
Read 허용</code></pre>
<p>최신 데이터를 보장하는 대신 가용성이나 성능 측면에서 비용이 발생할 수 있다.</p>
<hr>
<h2 id="weak-consistency">Weak Consistency</h2>
<p>읽기 요청이 항상 최신 데이터를 반환한다는 보장이 없다.</p>
<hr>
<h2 id="eventual-consistency">Eventual Consistency</h2>
<p>약한 일관성의 한 형태다.</p>
<p>Replica들이 즉시 같은 값을 가지지는 않더라도 일정 시간이 지나면 결국 동일한 상태에 도달한다.</p>
<pre><code class="language-text">t0
A = 10
B = 9

시간 경과

t1
A = 10
B = 10</code></pre>
<hr>
<h1 id="16-동시에-서로-다른-데이터가-수정된다면">16. 동시에 서로 다른 데이터가 수정된다면?</h1>
<p>Replica를 여러 개 운영하면 서로 다른 서버에서 동일한 데이터를 동시에 수정할 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">D1
 ↓
Client A → D2
Client B → D3</code></pre>
<p>처럼 하나의 데이터에서 서로 다른 버전이 만들어질 수 있다.</p>
<p>어느 값이 최신인지 단순한 Timestamp만으로 판단하기 어려운 상황도 있다.</p>
<p>이를 해결하기 위한 방법 중 하나가 <strong>Versioning과 Vector Clock</strong>이다.</p>
<hr>
<h1 id="17-versioning">17. Versioning</h1>
<p>데이터가 변경될 때마다 새로운 버전으로 관리한다.</p>
<pre><code class="language-text">D1
 ↓
D2
 ↓
D3</code></pre>
<p>각 버전을 독립적으로 관리하면서 어떤 버전이 다른 버전보다 먼저 만들어졌는지 확인할 수 있다.</p>
<p>하지만 동시에 수정되면</p>
<pre><code class="language-text">   D2
  ↙  ↘
D3    D4</code></pre>
<p>처럼 갈라질 수 있다.</p>
<p>이 경우 D3와 D4 사이에 충돌이 존재한다.</p>
<hr>
<h1 id="18-vector-clock">18. Vector Clock</h1>
<p>Vector Clock은 데이터에</p>
<pre><code class="language-text">[Server, Version]</code></pre>
<p>쌍을 함께 저장하여 데이터의 선후 관계를 추적하는 방식이다.</p>
<p>예를 들어</p>
<pre><code class="language-text">D1 [(Sx, 1)]</code></pre>
<p>에서 Sx가 다시 데이터를 변경하면</p>
<pre><code class="language-text">D2 [(Sx, 2)]</code></pre>
<p>가 된다.</p>
<p>다른 서버 Sy에서 D2를 수정한다면</p>
<pre><code class="language-text">D3 [(Sx, 2), (Sy, 1)]</code></pre>
<p>이 된다.</p>
<hr>
<h1 id="19-동시에-수정되면">19. 동시에 수정되면?</h1>
<p>D2에서 두 개의 업데이트가 동시에 발생했다고 하자.</p>
<pre><code class="language-text">D3 [(Sx,2), (Sy,1)]

D4 [(Sx,2), (Sz,1)]</code></pre>
<p>두 Vector Clock을 비교하면</p>
<pre><code class="language-text">D3에는 Sy 버전 존재
D4에는 Sz 버전 존재</code></pre>
<p>어느 한쪽이 다른 한쪽을 완전히 포함하지 않는다.</p>
<p>따라서</p>
<blockquote>
<p><strong>두 버전 사이에 선후관계를 결정할 수 없는 충돌 상태</strong></p>
</blockquote>
<p>라고 판단할 수 있다.</p>
<p>이후 충돌을 해결해 새로운 데이터를 만들면</p>
<pre><code class="language-text">D5 [(Sx,3), (Sy,1), (Sz,1)]</code></pre>
<p>와 같은 새로운 버전으로 저장할 수 있다.</p>
<hr>
<h1 id="20-vector-clock의-단점">20. Vector Clock의 단점</h1>
<p>Vector Clock 역시 완벽하지 않다.</p>
<h3 id="클라이언트-로직이-복잡해진다">클라이언트 로직이 복잡해진다.</h3>
<p>충돌 데이터를 확인하고 병합하는 기능이 필요할 수 있다.</p>
<h3 id="vector-정보가-계속-증가한다">Vector 정보가 계속 증가한다.</h3>
<pre><code class="language-text">(S1,1)
(S2,3)
(S3,5)
(S4,2)
...</code></pre>
<p>처럼 서버 정보가 계속 추가될 수 있다.</p>
<p>이를 완화하기 위해 일정 임계치를 정하고 오래된 항목을 제거할 수 있다.</p>
<p>다만 이 경우 일부 정확한 버전 관계를 잃을 수 있다.</p>
<p>즉 여기에도 Trade-off가 존재한다.</p>
<hr>
<h1 id="21-장애-처리">21. 장애 처리</h1>
<p>분산 시스템에서는 서버 장애가 반드시 발생한다고 가정해야 한다.</p>
<p>장애 처리는 크게</p>
<pre><code class="language-text">Failure Detection
+
Failure Resolution</code></pre>
<p>두 단계로 볼 수 있다.</p>
<p>먼저 어떤 서버가 장애 상태인지 알아내야 한다.</p>
<hr>
<h1 id="22-장애-감지">22. 장애 감지</h1>
<p>서버 A와 잠시 통신되지 않았다고 해서 곧바로 장애로 판단하는 것은 위험할 수 있다.</p>
<p>네트워크가 순간적으로 느렸을 수도 있기 때문이다.</p>
<p>따라서 여러 노드가 정보를 공유하여 장애 여부를 판단하는 방식을 사용할 수 있다.</p>
<p>노드 수가 많아질수록 모든 서버끼리 서로 상태를 확인하는 방식은 비효율적이다.</p>
<p>이때 사용할 수 있는 방법 중 하나가 <strong>Gossip Protocol</strong>이다.</p>
<hr>
<h1 id="23-gossip-protocol">23. Gossip Protocol</h1>
<p>각 노드는 다른 노드들의 상태 정보를 가진 Membership List를 관리한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Server A → Heartbeat 10
Server B → Heartbeat 14
Server C → Heartbeat 9</code></pre>
<p>형태다.</p>
<p>각 노드는 자신의 Heartbeat 값을 주기적으로 증가시킨다.</p>
<p>그리고 임의의 다른 노드에 자신이 가진 Membership 정보를 전달한다.</p>
<pre><code class="language-text">Server A
 ↓
Server C에게 상태 정보 전달</code></pre>
<p>정보를 받은 서버는 더 최신 Heartbeat 값을 반영한다.</p>
<hr>
<h1 id="24-장애-판단">24. 장애 판단</h1>
<p>특정 서버의 Heartbeat 값이 오랜 시간 동안 갱신되지 않는다면 해당 서버가 비정상 상태일 가능성이 높다고 판단한다.</p>
<pre><code class="language-text">Server B

Heartbeat = 14

오랜 시간 변화 없음
 ↓
장애 의심</code></pre>
<p>이 정보가 Gossip을 통해 여러 노드로 퍼지면서 전체 클러스터가 장애 정보를 공유할 수 있다.</p>
<hr>
<h1 id="25-일시적인-장애라면">25. 일시적인 장애라면?</h1>
<p>Replica 하나가 잠시 장애가 났다고 가정해보자.</p>
<p>원래 해당 서버에 데이터를 써야 하지만 지금은 사용할 수 없다.</p>
<p>이때 사용할 수 있는 방법이 <strong>Sloppy Quorum</strong>이다.</p>
<p>정상적인 Replica만 고집하지 않고 Hash Ring에서 현재 사용 가능한 서버를 찾아 요청을 처리한다.</p>
<pre><code class="language-text">원래 Server B에 저장

Server B 장애
 ↓
임시로 Server D에 저장</code></pre>
<p>그리고 Server D에는</p>
<blockquote>
<p>이 데이터는 원래 Server B의 데이터다.</p>
</blockquote>
<p>라는 Hint를 남긴다.</p>
<hr>
<h1 id="26-hinted-handoff">26. Hinted Handoff</h1>
<p>장애가 복구되면 임시 서버가 데이터를 원래 서버에 전달한다.</p>
<pre><code class="language-text">Server B 장애

Data
 ↓
Server D 임시 저장
 + Hint

Server B 복구

Server D
 ↓
Server B로 데이터 전달</code></pre>
<p>이러한 방식을 <strong>Hinted Handoff</strong>라고 한다.</p>
<p>이를 통해 일시적인 장애 상황에서도 쓰기 요청을 계속 받을 수 있다.</p>
<hr>
<h1 id="27-영구적인-데이터-불일치는">27. 영구적인 데이터 불일치는?</h1>
<p>장애가 길어지거나 Replica 데이터 자체가 달라졌다면 서로 다른 서버의 데이터를 비교해야 한다.</p>
<p>이 과정을 <strong>Anti-Entropy</strong>라고 한다.</p>
<p>하지만 수많은 데이터를 하나씩 전부 비교하면 비용이 너무 크다.</p>
<p>그래서 사용할 수 있는 구조가 <strong>Merkle Tree</strong>다.</p>
<hr>
<h1 id="28-merkle-tree">28. Merkle Tree</h1>
<p>Merkle Tree는 데이터의 Hash를 이용하여 서버 간 데이터 차이를 효율적으로 찾는 구조다.</p>
<p>먼저 전체 Key 공간을 여러 Bucket으로 나눈다.</p>
<pre><code class="language-text">Key Space

Bucket 1
Bucket 2
Bucket 3
Bucket 4</code></pre>
<p>각 Bucket 내부 데이터에 Hash를 적용한다.</p>
<p>그리고 이 Hash 값들을 다시 묶어 상위 Hash를 만든다.</p>
<pre><code class="language-text">          Root Hash
          /       \
      Hash A      Hash B
      /   \       /   \
    H1    H2    H3    H4</code></pre>
<hr>
<h1 id="29-merkle-tree를-사용하면">29. Merkle Tree를 사용하면?</h1>
<p>두 서버의 Root Hash부터 비교한다.</p>
<pre><code class="language-text">Server A Root = X
Server B Root = X</code></pre>
<p>같다면 전체 데이터가 동일하다고 볼 수 있다.</p>
<p>반대로</p>
<pre><code class="language-text">Server A Root ≠ Server B Root</code></pre>
<p>라면 하위 노드로 내려가면서 어떤 영역의 데이터가 다른지 확인한다.</p>
<p>즉 전체 데이터를 전부 전송하지 않고 <strong>차이가 있는 부분만 찾아 동기화할 수 있다.</strong></p>
<hr>
<h1 id="30-데이터-센터-장애">30. 데이터 센터 장애</h1>
<p>서버 하나가 아니라 데이터 센터 전체에 문제가 생길 수도 있다.</p>
<pre><code class="language-text">Data Center A ✕</code></pre>
<p>따라서 복제 데이터를 여러 데이터 센터에 분산하여 저장하는 방식도 필요하다.</p>
<pre><code class="language-text">Data Center A
     +
Data Center B</code></pre>
<p>한 지역에 장애가 발생해도 다른 지역에서 서비스를 이어갈 수 있도록 구성한다.</p>
<hr>
<h1 id="31-전체-아키텍처">31. 전체 아키텍처</h1>
<p>지금까지의 내용을 연결하면 다음과 같은 분산 Key-Value Store를 생각할 수 있다.</p>
<pre><code class="language-text">Client
   ↓
Coordinator
   ↓
Consistent Hash Ring
   ↓
┌───────────────┐
│ Server A      │
│ Server B      │
│ Server C      │
│ Server D      │
└───────────────┘</code></pre>
<p>각 서버는</p>
<ul>
<li>데이터를 저장하고</li>
<li>Replica를 관리하며</li>
<li>장애 정보를 공유한다.</li>
</ul>
<p>시스템은 특정 하나의 중앙 서버에만 모든 책임을 맡기지 않고 여러 노드로 분산할 수 있다.</p>
<hr>
<h1 id="32-write-path">32. Write Path</h1>
<p>이제 실제 데이터를 저장하는 과정을 살펴보자.</p>
<p>클라이언트가</p>
<pre><code class="language-text">put(key, value)</code></pre>
<p>를 호출한다고 가정한다.</p>
<p>쓰기 요청은 대략 다음 순서로 처리된다.</p>
<hr>
<h2 id="1-commit-log-기록">1. Commit Log 기록</h2>
<p>먼저 디스크의 Commit Log에 쓰기 작업을 기록한다.</p>
<pre><code class="language-text">Write Request
 ↓
Commit Log</code></pre>
<p>장애가 발생했을 때 데이터를 복구하기 위한 기록이다.</p>
<hr>
<h2 id="2-memory에-저장">2. Memory에 저장</h2>
<p>그 다음 데이터를 메모리 영역에 저장한다.</p>
<pre><code class="language-text">Commit Log
 ↓
Memory Cache</code></pre>
<p>메모리 기반이므로 빠르게 쓰기 요청을 처리할 수 있다.</p>
<hr>
<h2 id="3-sstable-생성">3. SSTable 생성</h2>
<p>Memory 영역이 일정 크기 이상 차면 데이터를 디스크의 SSTable로 저장한다.</p>
<pre><code class="language-text">Memory Full
   ↓
SSTable</code></pre>
<hr>
<h1 id="33-sstable">33. SSTable</h1>
<p>SSTable은 <strong>Sorted String Table</strong>의 약자다.</p>
<p>Key-Value 데이터를 Key 기준으로 정렬하여 저장하는 변경 불가능한 파일 형태다.</p>
<pre><code class="language-text">apple → 10
banana → 20
cat → 30
dog → 40</code></pre>
<p>이미 생성된 SSTable의 내용을 직접 수정하는 대신 새로운 데이터를 별도로 기록하는 구조를 사용할 수 있다.</p>
<hr>
<h1 id="34-read-path">34. Read Path</h1>
<p>이번에는</p>
<pre><code class="language-text">get(key)</code></pre>
<p>요청을 생각해보자.</p>
<p>먼저 메모리를 확인한다.</p>
<pre><code class="language-text">Key 존재?
  ↓
Memory</code></pre>
<p>메모리에 존재한다면 바로 반환할 수 있다.</p>
<p>하지만 메모리에 없다면 디스크의 SSTable을 확인해야 한다.</p>
<p>문제는 SSTable이 여러 개 존재할 수 있다는 것이다.</p>
<pre><code class="language-text">SSTable 1
SSTable 2
SSTable 3
SSTable 4</code></pre>
<p>모든 파일을 하나씩 확인하면 느리다.</p>
<p>이때 <strong>Bloom Filter</strong>를 이용할 수 있다.</p>
<hr>
<h1 id="35-bloom-filter">35. Bloom Filter</h1>
<p>Bloom Filter는</p>
<blockquote>
<p><strong>특정 값이 존재하지 않는다는 것은 확실하게 판단할 수 있지만, 존재한다고 판단해도 실제로는 없을 수 있는 자료구조</strong></p>
</blockquote>
<p>다.</p>
<p>즉 결과는 크게 두 가지다.</p>
<pre><code class="language-text">&quot;없음&quot;
→ 진짜 없음

&quot;있을 수도 있음&quot;
→ 실제 데이터를 확인해야 함</code></pre>
<p>이를 이용하면 불필요한 디스크 조회를 줄일 수 있다.</p>
<hr>
<h1 id="36-bloom-filter를-이용한-조회-흐름">36. Bloom Filter를 이용한 조회 흐름</h1>
<p>Key가 메모리에 없다면 Bloom Filter를 확인한다.</p>
<pre><code class="language-text">Client
 ↓
Memory 확인
 ↓
없음
 ↓
Bloom Filter 확인</code></pre>
<p>Bloom Filter가</p>
<pre><code class="language-text">&quot;이 SSTable에는 절대 없음&quot;</code></pre>
<p>이라고 판단하면 해당 파일을 읽지 않는다.</p>
<p>반면</p>
<pre><code class="language-text">&quot;있을 수도 있음&quot;</code></pre>
<p>이라고 판단하면 실제 SSTable을 확인한다.</p>
<p>전체 흐름은 다음과 같다.</p>
<pre><code class="language-text">GET(key)
   ↓
Memory 확인
   ↓
없음
   ↓
Bloom Filter
   ↓
가능성 있는 SSTable 확인
   ↓
Data 조회
   ↓
Client 반환</code></pre>
<p>이를 통해 여러 SSTable을 모두 검색해야 하는 비용을 줄일 수 있다.</p>
<hr>
<h1 id="전체-흐름-정리">전체 흐름 정리</h1>
<p>이번 장의 전체 내용을 연결하면 다음과 같다.</p>
<pre><code class="language-text">Key-Value Store 필요
        ↓
단일 서버로 구현
        ↓
데이터 증가
        ↓
분산 시스템 필요
        ↓
CAP 고려
        ↓
Consistent Hashing으로
데이터 파티셔닝
        ↓
Replication으로
가용성 확보
        ↓
N / W / R로
일관성 수준 조정
        ↓
Replica 간 데이터 충돌
        ↓
Versioning + Vector Clock
        ↓
서버 장애 발생
        ↓
Gossip Protocol로 장애 감지
        ↓
일시 장애
→ Sloppy Quorum
→ Hinted Handoff
        ↓
영구적인 불일치
→ Anti-Entropy
→ Merkle Tree
        ↓
Write
→ Commit Log
→ Memory
→ SSTable
        ↓
Read
→ Memory
→ Bloom Filter
→ SSTable</code></pre>
<p>결국 이번 장은 단순한</p>
<pre><code class="language-text">Key → Value</code></pre>
<p>저장 방식을 만드는 이야기가 아니었다.</p>
<blockquote>
<p><strong>Key-Value 저장소를 여러 서버로 분산했을 때 발생하는 데이터 분산, 복제, 일관성, 장애와 복구 문제를 어떻게 해결할 것인가</strong></p>
</blockquote>
<p>를 다루는 장이었다.</p>
<hr>
<h1 id="공부하면서-정리한-핵심">공부하면서 정리한 핵심</h1>
<p>처음에는 Key-Value Store라고 하면 Redis처럼</p>
<pre><code class="language-text">Key
→
Value</code></pre>
<p>를 빠르게 조회하는 단순한 데이터베이스 정도로 생각했다.</p>
<p>하지만 데이터가 많아지고 여러 서버를 사용하는 순간부터 이야기가 완전히 달라졌다.</p>
<pre><code class="language-text">데이터 분산
+
복제
+
일관성
+
동시 수정
+
서버 장애
+
데이터 복구</code></pre>
<p>를 전부 고려해야 했다.</p>
<p>특히 지난 장에서 배운 <strong>Consistent Hashing이 이번 장의 Data Partitioning에 바로 사용된다는 점</strong>이 흥미로웠다.</p>
<p>단순히 안정 해시 하나를 독립적으로 배우는 것이 아니라</p>
<pre><code class="language-text">Consistent Hashing
→ 서버에 데이터 분산
→ Replication
→ Quorum
→ Consistency</code></pre>
<p>처럼 이전 개념이 다음 시스템 설계에 계속 연결되고 있었다.</p>
<p>또 N, W, R 값을 바꾸는 것만으로도 읽기 성능, 쓰기 성능, 데이터 일관성의 특성이 달라진다는 점도 중요하게 느껴졌다.</p>
<p>결국 분산 시스템에서는 모든 것을 완벽하게 얻으려 하기보다</p>
<blockquote>
<p><strong>현재 서비스에서 무엇을 더 중요하게 볼 것인지 정하고 적절한 Trade-off를 선택하는 것</strong></p>
</blockquote>
<p>이 중요하다는 것을 다시 확인한 장이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 5장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-5%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-5%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 21 Jul 2026 02:36:53 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-4%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 ] 4장 정리</a>
다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-6%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 ] 6장 정리</a></p>
</blockquote>
<h1 id="5장-안정-해시-설계">5장. 안정 해시 설계</h1>
<p>대규모 시스템에서는 서버 한 대가 모든 데이터를 처리하도록 두기보다 여러 서버에 데이터와 요청을 분산한다.</p>
<p>이때 중요한 문제는</p>
<blockquote>
<p><strong>어떤 데이터를 어떤 서버에 배치할 것인가?</strong></p>
</blockquote>
<p>이다.</p>
<p>단순히 데이터를 나누는 것뿐만 아니라, 서버가 추가되거나 제거되더라도 기존 데이터의 이동을 최소화해야 한다.</p>
<p>이 문제를 해결하기 위해 사용하는 방법이 <strong>안정 해시(Consistent Hashing)</strong>다.</p>
<hr>
<h1 id="1-기존-해싱-방식의-문제">1. 기존 해싱 방식의 문제</h1>
<p>여러 서버에 데이터를 분산하는 가장 단순한 방법 중 하나는 나머지 연산을 사용하는 것이다.</p>
<p>서버가 <code>N</code>개라면 다음과 같이 계산한다.</p>
<pre><code class="language-text">serverIndex = hash(key) % N</code></pre>
<p>예를 들어 서버가 4개라면</p>
<pre><code class="language-text">hash(key) % 4</code></pre>
<p>의 결과에 따라</p>
<pre><code class="language-text">0 → Server 0
1 → Server 1
2 → Server 2
3 → Server 3</code></pre>
<p>중 하나에 데이터를 저장한다.</p>
<p>이 방식은 서버 수가 고정되어 있을 때는 간단하고 잘 동작한다.</p>
<p>문제는 <strong>서버 개수가 변경되는 순간</strong> 발생한다.</p>
<hr>
<h1 id="2-서버-수가-바뀌면-어떻게-될까">2. 서버 수가 바뀌면 어떻게 될까?</h1>
<p>기존 서버가 4개라고 가정해보자.</p>
<pre><code class="language-text">hash(key) % 4</code></pre>
<p>를 이용해 모든 데이터가 서버에 배치되어 있다.</p>
<p>그런데 트래픽이 증가하여 서버를 하나 추가한다면 이제 계산식은</p>
<pre><code class="language-text">hash(key) % 5</code></pre>
<p>로 바뀐다.</p>
<p>문제는 나누는 값 자체가 달라졌기 때문에 기존 데이터 대부분의 결과도 함께 달라진다는 것이다.</p>
<pre><code class="language-text">기존

hash(key) % 4
→ Server 1

서버 추가 후

hash(key) % 5
→ Server 3</code></pre>
<p>이런 변화가 여러 키에서 동시에 발생한다.</p>
<p>결국 서버 하나를 추가했을 뿐인데 <strong>대부분의 데이터를 다른 서버로 다시 옮겨야 하는 상황</strong>이 발생할 수 있다.</p>
<p>캐시 서버라면 기존 캐시를 찾지 못하는 Cache Miss가 대량으로 발생할 수도 있다.</p>
<p>즉 기존 방식의 문제는</p>
<blockquote>
<p><strong>서버 수 변화가 전체 데이터 배치에 너무 큰 영향을 준다는 것</strong></p>
</blockquote>
<p>이다.</p>
<hr>
<h1 id="3-안정-해시-consistent-hashing">3. 안정 해시 Consistent Hashing</h1>
<p>안정 해시는 서버가 추가되거나 제거되었을 때 <strong>전체 키를 다시 배치하지 않고 일부 데이터만 이동하도록 만드는 해싱 방식</strong>이다.</p>
<p>키가 총 <code>K</code>개이고 서버가 <code>N</code>개라면 서버 하나가 변경될 때 평균적으로 약</p>
<pre><code class="language-text">K / N</code></pre>
<p>수준의 키만 영향을 받도록 하는 것이 기본적인 아이디어다.</p>
<p>즉</p>
<pre><code class="language-text">일반적인 나머지 연산

서버 변경
→ 대부분의 Key 재배치</code></pre>
<p>에서</p>
<pre><code class="language-text">Consistent Hashing

서버 변경
→ 일부 Key만 재배치</code></pre>
<p>가 되도록 만드는 것이다.</p>
<hr>
<h1 id="4-해시-공간">4. 해시 공간</h1>
<p>먼저 일정한 범위의 해시 공간을 만든다.</p>
<p>예를 들어 해시 함수가 다음 범위의 값을 만든다고 해보자.</p>
<pre><code class="language-text">0 ~ 2^32 - 1</code></pre>
<p>그러면 서버와 데이터의 키를 모두 동일한 해시 공간 위에 배치할 수 있다.</p>
<pre><code class="language-text">0 -------------------------------------- 2^32 - 1</code></pre>
<p>그리고 이 해시 공간의 양 끝을 연결한다고 생각하면 원형 구조가 만들어진다.</p>
<p>이것을 <strong>Hash Ring</strong>이라고 한다.</p>
<pre><code class="language-text">       0
    ↗     ↘
   ○       ○
   ↑       ↓
   ○       ○
    ↖     ↙
    2^32-1</code></pre>
<p>핵심은 서버의 개수와 상관없이 <strong>항상 동일한 해시 공간을 사용한다는 점</strong>이다.</p>
<hr>
<h1 id="5-서버를-해시-링에-배치">5. 서버를 해시 링에 배치</h1>
<p>각 서버 역시 해시 함수를 적용하여 링 위의 특정 위치에 배치한다.</p>
<p>예를 들어 서버 네 대가 있다면</p>
<pre><code class="language-text">Server 0
Server 1
Server 2
Server 3</code></pre>
<p>각 서버의 이름이나 IP 주소 등을 해싱하여 링 위에 위치시킬 수 있다.</p>
<pre><code class="language-text">        S0
    ↗        ↘
 S3            S1
    ↖        ↙
        S2</code></pre>
<p>이제 데이터의 Key 역시 동일한 해시 함수를 사용해 링 위의 위치로 변환한다.</p>
<hr>
<h1 id="6-key는-어떤-서버에-저장될까">6. Key는 어떤 서버에 저장될까?</h1>
<p>안정 해시에서는 Key가 위치한 곳에서 <strong>시계 방향으로 이동했을 때 가장 먼저 만나는 서버</strong>에 데이터를 저장한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Key A
  ↓

Key A → → → Server 1</code></pre>
<p>이라면 Key A는 Server 1에 저장한다.</p>
<p>또 다른 Key가 Server 2 바로 앞에 있다면</p>
<pre><code class="language-text">Key B → Server 2</code></pre>
<p>가 된다.</p>
<p>즉 기본 규칙은 매우 단순하다.</p>
<blockquote>
<p><strong>Key 위치에서 시계 방향으로 가장 가까운 서버를 찾는다.</strong></p>
</blockquote>
<hr>
<h1 id="7-서버가-추가된다면">7. 서버가 추가된다면?</h1>
<p>새로운 <code>Server 4</code>가 Server 3과 Server 0 사이에 추가되었다고 가정해보자.</p>
<p>기존에는 해당 구간의 Key들이 Server 0에 저장되고 있었다.</p>
<pre><code class="language-text">Server 3
   ↓

Key A
Key B
Key C

   ↓
Server 0</code></pre>
<p>그런데 중간에 Server 4가 들어오면</p>
<pre><code class="language-text">Server 3
   ↓

Key A
Key B
   ↓
Server 4

Key C
   ↓
Server 0</code></pre>
<p>처럼 <strong>Server 4가 새롭게 담당하게 된 구간의 Key만 이동한다.</strong></p>
<p>다른 서버에 저장되어 있던 데이터는 그대로 유지된다.</p>
<p>이것이 나머지 연산 방식과 가장 큰 차이다.</p>
<hr>
<h1 id="8-서버가-제거된다면">8. 서버가 제거된다면?</h1>
<p>이번에는 Server 1에 장애가 발생하여 링에서 제거되었다고 해보자.</p>
<p>기존에 Server 1이 담당하고 있던 Key들은 시계 방향으로 다음에 있는 서버로 이동한다.</p>
<pre><code class="language-text">Server 1 제거
      ↓

Server 1이 담당했던 Key
      ↓
다음 Server로 이동</code></pre>
<p>역시 다른 구간의 데이터는 영향을 받지 않는다.</p>
<p>결과적으로 서버 하나가 추가되거나 제거되어도 전체 데이터가 아니라 <strong>해당 서버와 관련된 일부 구간만 재배치</strong>된다.</p>
<hr>
<h1 id="9-그런데-기본적인-안정-해시에도-문제가-있다">9. 그런데 기본적인 안정 해시에도 문제가 있다</h1>
<p>여기까지만 보면 안정 해시가 모든 문제를 해결한 것처럼 보인다.</p>
<p>하지만 서버를 링에 한 번씩만 배치하면 두 가지 문제가 발생할 수 있다.</p>
<pre><code class="language-text">1. 서버가 담당하는 구간이 불균등함
2. 데이터가 특정 서버에 몰릴 수 있음</code></pre>
<hr>
<h1 id="10-문제-1-서버별-담당-구간이-다르다">10. 문제 1: 서버별 담당 구간이 다르다</h1>
<p>서버들이 Hash Ring에 완벽하게 같은 간격으로 배치된다는 보장은 없다.</p>
<p>예를 들어 다음처럼 배치될 수 있다.</p>
<pre><code class="language-text">S0 ----- S1 -------------------- S2 --- S3</code></pre>
<p>이 경우 Server 2가 담당하는 구간은 다른 서버보다 훨씬 커질 수 있다.</p>
<p>특히 서버 하나가 삭제되면 인접한 서버가 기존 서버의 영역까지 넘겨받으면서 담당 범위가 크게 증가할 수도 있다.</p>
<pre><code class="language-text">S1 삭제

기존 S1 영역
+
기존 S2 영역
→ 모두 S2 담당</code></pre>
<p>결과적으로 서버별 부하가 불균등해질 수 있다.</p>
<hr>
<h1 id="11-문제-2-key-역시-균등하게-퍼진다는-보장이-없다">11. 문제 2: Key 역시 균등하게 퍼진다는 보장이 없다</h1>
<p>해시 함수가 데이터를 분산하더라도 실제 Key의 위치와 서버의 위치 조합에 따라 특정 서버가 많은 Key를 담당할 수 있다.</p>
<p>예를 들면</p>
<pre><code class="language-text">Server 1 → Key 0개
Server 2 → Key 다수
Server 3 → Key 0개</code></pre>
<p>처럼 일부 서버는 거의 사용되지 않고 특정 서버에 데이터가 집중되는 문제가 발생할 수 있다.</p>
<p>이 문제를 완화하기 위해 등장하는 개념이 <strong>가상 노드(Virtual Node)</strong>다.</p>
<hr>
<h1 id="12-가상-노드-virtual-node">12. 가상 노드 Virtual Node</h1>
<p>가상 노드는 실제 서버 하나를 Hash Ring 위에 여러 번 배치하는 방식이다.</p>
<p>예를 들어 실제 서버가</p>
<pre><code class="language-text">Server 0
Server 1
Server 2</code></pre>
<p>세 대 있다고 하자.</p>
<p>서버 하나를 링에 한 번만 배치하지 않고</p>
<pre><code class="language-text">Server 0
→ S0_0
→ S0_1
→ S0_2

Server 1
→ S1_0
→ S1_1
→ S1_2

Server 2
→ S2_0
→ S2_1
→ S2_2</code></pre>
<p>처럼 여러 개의 가상 노드로 나누어 배치한다.</p>
<p>각 가상 노드는 링에서 서로 다른 위치를 가진다.</p>
<pre><code class="language-text">S0_0 → S1_2 → S2_0 → S0_2 → S1_0 → S2_1 ...</code></pre>
<p>하지만 실제 요청은 결국 해당 가상 노드가 가리키는 실제 서버로 전달된다.</p>
<hr>
<h1 id="13-가상-노드를-사용하는-이유">13. 가상 노드를 사용하는 이유</h1>
<p>서버 하나가 링의 한 위치만 담당하면 우연히 매우 넓은 구간을 맡을 수 있다.</p>
<p>하지만 서버 하나를 여러 위치에 분산하면 서버가 담당하는 범위도 여러 곳으로 나뉜다.</p>
<pre><code class="language-text">가상 노드 적음
→ 서버별 데이터 편차 큼

가상 노드 많음
→ 서버별 데이터 편차 감소</code></pre>
<p>따라서 가상 노드 개수가 증가할수록 서버마다 할당되는 데이터의 양이 평균적으로 더 균등해질 수 있다.</p>
<p>이를 통계적으로 보면 서버별 데이터 개수의 <strong>표준편차가 감소하는 방향</strong>으로 볼 수 있다.</p>
<hr>
<h1 id="14-가상-노드를-무한히-늘리면-좋을까">14. 가상 노드를 무한히 늘리면 좋을까?</h1>
<p>그렇지는 않다.</p>
<p>가상 노드가 증가하면 관리해야 하는 링 정보 역시 많아진다.</p>
<pre><code class="language-text">가상 노드 ↑
→ 데이터 분산 균등성 ↑
→ 관리할 Node 정보 ↑
→ 메모리 / 관리 비용 ↑</code></pre>
<p>따라서</p>
<blockquote>
<p><strong>데이터 분산의 균등성과 관리 비용 사이에 Trade-off가 존재한다.</strong></p>
</blockquote>
<p>실제 시스템에서는</p>
<ul>
<li>서버 수</li>
<li>데이터 규모</li>
<li>서버 성능</li>
<li>요구되는 부하 분산 수준</li>
</ul>
<p>등을 고려하여 적절한 가상 노드 개수를 결정해야 한다.</p>
<hr>
<h1 id="15-어떤-key를-재배치해야-할까">15. 어떤 Key를 재배치해야 할까?</h1>
<p>안정 해시에서 서버가 추가되거나 삭제되었을 때 전체 데이터를 다시 옮길 필요는 없다.</p>
<h2 id="새로운-서버가-추가되는-경우">새로운 서버가 추가되는 경우</h2>
<p>예를 들어 Server 3과 Server 4 사이에 새로운 서버가 추가되었다고 가정한다.</p>
<p>새로운 서버가 담당하게 될 구간은 <strong>이전 서버와 새로운 서버 사이의 Key들</strong>이다.</p>
<p>즉 해당 구간의 데이터만 새로운 서버로 이동하면 된다.</p>
<hr>
<h2 id="기존-서버가-제거되는-경우">기존 서버가 제거되는 경우</h2>
<p>Server 1이 제거된다면 Server 1이 담당하던 구간의 데이터는 시계 방향의 다음 서버가 넘겨받는다.</p>
<pre><code class="language-text">Server 1 담당 Key
       ↓
다음 Server로 이동</code></pre>
<p>따라서 안정 해시의 핵심은 결국</p>
<blockquote>
<p><strong>서버 변화와 직접적으로 관련된 Hash Ring 구간만 다시 배치하는 것</strong></p>
</blockquote>
<p>이라고 정리할 수 있다.</p>
<hr>
<h1 id="16-기존-나머지-연산-방식-직접-구현">16. 기존 나머지 연산 방식 직접 구현</h1>
<p>개념을 이해하기 위해 Java로 기존 방식부터 간단하게 구현해보았다.</p>
<pre><code class="language-java">import java.util.LinkedHashMap;
import java.util.Map;

public class SimpleHashMapper {

    public static void main(String[] args) {

        int serverCount = 4;

        Map&lt;String, Integer&gt; keyHashes = new LinkedHashMap&lt;&gt;();

        keyHashes.put(&quot;key0&quot;, 18358617);
        keyHashes.put(&quot;key1&quot;, 26143584);
        keyHashes.put(&quot;key2&quot;, 18131146);
        keyHashes.put(&quot;key3&quot;, 35863496);
        keyHashes.put(&quot;key4&quot;, 34085809);
        keyHashes.put(&quot;key5&quot;, 27581703);
        keyHashes.put(&quot;key6&quot;, 38164978);
        keyHashes.put(&quot;key7&quot;, 22530351);

        for (Map.Entry&lt;String, Integer&gt; entry : keyHashes.entrySet()) {

            String key = entry.getKey();
            int hash = entry.getValue();

            int serverIndex = hash % serverCount;

            System.out.println(
                key + &quot; → Server &quot; + serverIndex
            );
        }
    }
}</code></pre>
<p>핵심 부분은 다음 한 줄이다.</p>
<pre><code class="language-java">int serverIndex = hash % serverCount;</code></pre>
<p>구현 자체는 매우 간단하다.</p>
<p>하지만 <code>serverCount</code>가 변경되면 모든 Key에 대해 계산 결과가 다시 달라질 가능성이 크다.</p>
<p>즉 코드가 단순한 대신 <strong>서버 추가·삭제에 취약한 구조</strong>다.</p>
<hr>
<h1 id="17-java-treemap으로-hash-ring-구현">17. Java TreeMap으로 Hash Ring 구현</h1>
<p>안정 해시 역시 Java 자료구조를 활용하여 구현할 수 있다.</p>
<p>여기서는 Hash Ring을 표현하기 위해 <code>TreeMap</code>을 사용했다.</p>
<p>TreeMap은 Key가 정렬된 상태로 유지되기 때문에</p>
<blockquote>
<p><strong>현재 Key의 Hash 값보다 크거나 같은 첫 번째 Server를 찾는 작업</strong></p>
</blockquote>
<p>을 구현하기 편리하다.</p>
<pre><code class="language-java">import java.util.SortedMap;
import java.util.TreeMap;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class ConsistentHashRing&lt;T&gt; {

    private final SortedMap&lt;Long, T&gt; ring = new TreeMap&lt;&gt;();

    public void addServer(T server) {
        long hash = hash(server.toString());
        ring.put(hash, server);
    }

    public void removeServer(T server) {
        long hash = hash(server.toString());
        ring.remove(hash);
    }

    public T getServer(String key) {

        if (ring.isEmpty()) {
            return null;
        }

        long hash = hash(key);

        if (!ring.containsKey(hash)) {

            SortedMap&lt;Long, T&gt; tailMap = ring.tailMap(hash);

            hash = tailMap.isEmpty()
                    ? ring.firstKey()
                    : tailMap.firstKey();
        }

        return ring.get(hash);
    }

    private long hash(String key) {

        try {

            MessageDigest md = MessageDigest.getInstance(&quot;MD5&quot;);
            byte[] digest = md.digest(key.getBytes());

            long result = 0;

            for (int i = 0; i &lt; 4; i++) {
                result = (result &lt;&lt; 8) | (digest[i] &amp; 0xFF);
            }

            return result &amp; 0xFFFFFFFFL;

        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}</code></pre>
<hr>
<h1 id="18-treemap을-사용한-이유">18. TreeMap을 사용한 이유</h1>
<p>이번 구현에서는 해시 공간을</p>
<pre><code class="language-text">0 ~ 2^32 - 1</code></pre>
<p>범위로 정의했다.</p>
<p>그리고 서버의 Hash 값을 TreeMap에 저장한다.</p>
<p>TreeMap은 자동으로 Key를 오름차순으로 정렬한다.</p>
<pre><code class="language-text">Server Hash

100
420
950
1500
...</code></pre>
<p>따라서 어떤 Key의 Hash 값이 <code>700</code>이라면</p>
<pre><code class="language-text">700보다 크거나 같은 첫 Server
→ 950</code></pre>
<p>을 찾으면 된다.</p>
<p>이 역할을 <code>tailMap()</code>을 이용해 구현했다.</p>
<hr>
<h1 id="19-hash-ring의-순환-구조-구현">19. Hash Ring의 순환 구조 구현</h1>
<p>Hash Ring은 원형이기 때문에 현재 Key보다 큰 서버가 없다면 다시 맨 처음 서버로 돌아가야 한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Server Hash

100
500
900</code></pre>
<p>인데 Key의 Hash가</p>
<pre><code class="language-text">950</code></pre>
<p>이라면 950보다 큰 서버는 없다.</p>
<p>따라서 링을 한 바퀴 돌았다고 생각하고</p>
<pre><code class="language-text">100</code></pre>
<p>에 위치한 서버를 선택해야 한다.</p>
<p>코드에서는 다음 부분이다.</p>
<pre><code class="language-java">hash = tailMap.isEmpty()
        ? ring.firstKey()
        : tailMap.firstKey();</code></pre>
<p>즉</p>
<pre><code class="language-text">뒤에 Server 존재
→ 가장 가까운 Server

뒤에 Server 없음
→ Hash Ring 처음으로 이동</code></pre>
<p>하는 구조다.</p>
<hr>
<h1 id="20-서버-제거-테스트">20. 서버 제거 테스트</h1>
<p>예를 들어 기존 결과가 다음과 같다고 하자.</p>
<pre><code class="language-text">=== Before removing Server-B ===

user:1001 → Server-C
user:1002 → Server-C
order:9001 → Server-C
image:cat.png → Server-B
session:abc → Server-C</code></pre>
<p>여기서 Server-B를 제거한다.</p>
<pre><code class="language-text">=== After removing Server-B ===

user:1001 → Server-C
user:1002 → Server-C
order:9001 → Server-C
image:cat.png → Server-C
session:abc → Server-C</code></pre>
<p>기존에 Server-C에 저장되어 있던 Key들은 그대로 유지된다.</p>
<p>Server-B가 담당하던</p>
<pre><code class="language-text">image:cat.png</code></pre>
<p>만 다음 서버로 이동했다.</p>
<p>이것이 안정 해시를 사용하는 가장 큰 이유다.</p>
<hr>
<h1 id="21-안정-해시의-장점">21. 안정 해시의 장점</h1>
<h2 id="1-데이터-재배치-최소화">1. 데이터 재배치 최소화</h2>
<p>서버가 추가되거나 제거되더라도 모든 데이터를 다시 배치할 필요가 없다.</p>
<pre><code class="language-text">Server 변화
    ↓
일부 Hash 구간만 변경
    ↓
일부 Key만 이동</code></pre>
<p>따라서 데이터 이동량과 캐시 미스를 줄일 수 있다.</p>
<hr>
<h2 id="2-수평-확장에-유리">2. 수평 확장에 유리</h2>
<p>트래픽이 증가했을 때 서버를 추가하더라도 기존 데이터 구조에 미치는 영향을 줄일 수 있다.</p>
<pre><code class="language-text">Server 4 추가
→ 전체 데이터 재배치 X
→ 일부 구간만 이동</code></pre>
<p>따라서 서버의 추가·제거가 발생하는 분산 환경과 잘 맞는다.</p>
<hr>
<h2 id="3-데이터-분산-개선">3. 데이터 분산 개선</h2>
<p>특히 가상 노드를 함께 사용하면 실제 서버 하나를 Hash Ring 여러 위치에 분산할 수 있다.</p>
<p>이를 통해 서버별 데이터 편차를 줄이고 부하를 보다 균등하게 나누는 데 도움을 줄 수 있다.</p>
<p>다만 안정 해시는 <strong>특정 하나의 인기 Key 자체에 요청이 몰리는 문제를 완전히 해결하는 기술은 아니다.</strong></p>
<p>하나의 콘텐츠에 트래픽이 집중된다면 캐시 복제 등의 추가적인 방법이 필요할 수 있다.</p>
<hr>
<h1 id="22-안정-해시가-필요한-시스템">22. 안정 해시가 필요한 시스템</h1>
<p>안정 해시는 특히 다음과 같은 상황에서 유용하다.</p>
<pre><code class="language-text">서버가 여러 대 존재
+
데이터 또는 요청을 분산해야 함
+
서버 추가 / 제거가 발생할 수 있음</code></pre>
<p>따라서</p>
<ul>
<li>분산 데이터베이스</li>
<li>분산 캐시</li>
<li>CDN</li>
<li>로드밸런싱</li>
<li>대규모 분산 서비스</li>
</ul>
<p>등에서 활용할 수 있는 중요한 개념이다.</p>
<hr>
<h1 id="전체-흐름-정리">전체 흐름 정리</h1>
<p>이번 장의 내용을 하나의 과정으로 연결하면 다음과 같다.</p>
<pre><code class="language-text">여러 서버에 데이터를 분산해야 함
        ↓
hash(key) % N 사용
        ↓
서버 수가 변경됨
        ↓
대부분의 Key 위치가 변경됨
        ↓
대량 데이터 재배치 발생
        ↓
Consistent Hashing 도입
        ↓
Server와 Key를 Hash Ring에 배치
        ↓
시계 방향의 첫 번째 Server 선택
        ↓
Server 추가 / 제거
        ↓
일부 구간의 Key만 이동
        ↓
하지만 데이터 분포가 불균등할 수 있음
        ↓
Virtual Node 도입
        ↓
각 서버를 Ring 여러 위치에 배치
        ↓
더 균등한 데이터 분산</code></pre>
<p>결국 안정 해시의 핵심은</p>
<blockquote>
<p><strong>서버의 구성이 변경되더라도 기존 데이터 배치를 최대한 유지하면서 필요한 데이터만 이동시키는 것</strong></p>
</blockquote>
<p>이라고 이해할 수 있다.</p>
<hr>
<h1 id="공부하면서-정리한-핵심">공부하면서 정리한 핵심</h1>
<p>처음에는 안정 해시가 단순히 데이터를 서버에 균등하게 나누기 위한 해시 방법이라고 생각했다.</p>
<p>하지만 이번 장에서 더 중요했던 것은 <strong>데이터를 처음 어떻게 나누는가보다 서버가 변했을 때 어떻게 대응하는가</strong>였다.</p>
<p>기존 나머지 연산은</p>
<pre><code class="language-text">hash(key) % 서버 수</code></pre>
<p>라는 매우 간단한 방식으로 데이터를 분산할 수 있지만 서버 개수가 바뀌는 순간 대부분의 매핑이 달라진다는 문제가 있다.</p>
<p>반면 안정 해시는</p>
<pre><code class="language-text">고정된 Hash 공간
+
Hash Ring
+
시계 방향 Server 탐색</code></pre>
<p>이라는 구조를 사용하여 서버 변화가 전체 데이터에 미치는 영향을 최소화한다.</p>
<p>그리고 기본적인 Hash Ring에서도 데이터 분포가 불균등해질 수 있기 때문에 <strong>Virtual Node를 통해 하나의 실제 서버를 여러 위치에 분산한다는 점</strong>도 중요했다.</p>
<p>결국 이번 장에서 가장 중요하게 느낀 흐름은</p>
<pre><code class="language-text">단순 Hash
→ Rehash 문제
→ Consistent Hash
→ Hash Ring
→ Virtual Node</code></pre>
<p>였다.</p>
<p>이전에는 <code>TreeMap</code>, Hash Function, 원형 구조 같은 개념을 각각 따로 배웠다면, 이번에는 이런 자료구조와 알고리즘이 실제 대규모 시스템에서 <strong>서버와 데이터를 분산하는 문제를 해결하는 데 어떻게 연결되는지</strong> 확인할 수 있어서 더 재미있었던 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 4장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-4%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-4%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 21 Jul 2026 02:34:06 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-3%EC%9E%A5-%EC%A0%95%EB%A6%AC-11xsjbs6">[ 대규모 시스템 설계 스터디 ] 3장 정리</a>
다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-5%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 5장 정리</a></p>
</blockquote>
<h1 id="4장-처리율-제한-장치의-설계">4장. 처리율 제한 장치의 설계</h1>
<p>서비스에 요청이 들어온다고 해서 모든 요청을 무조건 처리해야 하는 것은 아니다.</p>
<p>서버가 감당할 수 있는 수준보다 많은 요청이 들어오거나, 특정 사용자가 지나치게 많은 API를 호출한다면 시스템 전체에 영향을 줄 수 있다.</p>
<p>이때 일정 시간 동안 처리할 수 있는 요청량을 제한하는 장치가 <strong>처리율 제한 장치(Rate Limiter)</strong>다.</p>
<p>예를 들면 다음과 같은 정책을 만들 수 있다.</p>
<pre><code class="language-text">사용자당 초당 API 요청 2회

하나의 IP에서
하루 최대 계정 생성 10회

하나의 디바이스에서
주당 리워드 요청 최대 5회</code></pre>
<p>즉 Rate Limiter의 핵심은</p>
<blockquote>
<p><strong>누가, 어떤 요청을, 일정 시간 동안 몇 번까지 수행할 수 있는지를 제어하는 것</strong></p>
</blockquote>
<p>이라고 이해할 수 있다.</p>
<hr>
<h1 id="1-rate-limiter는-실제로-어디에-있을까">1. Rate Limiter는 실제로 어디에 있을까?</h1>
<p>처음 Rate Limiter라는 용어를 봤을 때는 별도의 네트워크 장비가 존재하는 것처럼 느껴졌다.</p>
<p>하지만 실제로는 하나의 특정한 물리 장비를 의미하기보다 <strong>여러 시스템에 포함될 수 있는 트래픽 제어 기능</strong>에 가깝다.</p>
<p>예를 들어 다음 위치에 적용할 수 있다.</p>
<table>
<thead>
<tr>
<th>위치</th>
<th>주로 제한하는 대상</th>
</tr>
</thead>
<tbody><tr>
<td>라우터 / 스위치</td>
<td>패킷 수, 네트워크 대역폭</td>
</tr>
<tr>
<td>방화벽 / L4 Load Balancer</td>
<td>연결 수, 동시 접속</td>
</tr>
<tr>
<td>WAF / CDN</td>
<td>IP별 HTTP 요청, 비정상 트래픽</td>
</tr>
<tr>
<td>API Gateway</td>
<td>사용자별 API 호출</td>
</tr>
<tr>
<td>애플리케이션</td>
<td>특정 기능 실행 횟수</td>
</tr>
<tr>
<td>Service Mesh</td>
<td>서비스 간 호출</td>
</tr>
</tbody></table>
<p>즉</p>
<pre><code class="language-text">Network Traffic 제한
→ Router / Firewall

HTTP API 제한
→ API Gateway / Middleware

특정 비즈니스 기능 제한
→ Application</code></pre>
<p>처럼 <strong>무엇을 제한하고 싶은지에 따라 Rate Limiter의 위치가 달라질 수 있다.</strong></p>
<p>기본적인 동작 자체는 비교적 단순하다.</p>
<pre><code class="language-text">요청 도착
   ↓
요청자 식별
   ↓
현재 사용량 확인
   ↓
제한 정책과 비교
   ↓
허용 / 거절</code></pre>
<p>HTTP API에서 제한을 초과한 요청에는 일반적으로 <code>429 Too Many Requests</code>를 반환할 수 있다.</p>
<hr>
<h1 id="2-rate-limiter를-사용하는-이유">2. Rate Limiter를 사용하는 이유</h1>
<h2 id="1-서버-보호">1. 서버 보호</h2>
<p>특정 사용자가 실수 또는 악의적으로 대량의 요청을 전송하면 서버 자원이 빠르게 소진될 수 있다.</p>
<pre><code class="language-text">요청 폭증
   ↓
CPU / Memory / DB Connection 사용 증가
   ↓
응답 지연
   ↓
전체 서비스 장애</code></pre>
<p>Rate Limiter를 사용하면 일정 수준 이상의 요청을 미리 차단하여 시스템을 보호할 수 있다.</p>
<hr>
<h2 id="2-비용-관리">2. 비용 관리</h2>
<p>요청 하나를 처리하는 데도 비용이 발생한다.</p>
<p>특히</p>
<ul>
<li>외부 API 호출</li>
<li>AI API</li>
<li>이미지 처리</li>
<li>SMS 발송</li>
<li>이메일 발송</li>
</ul>
<p>처럼 요청당 직접적인 비용이 발생하는 기능이라면 호출 횟수를 통제하는 것이 더욱 중요하다.</p>
<pre><code class="language-text">불필요한 요청 감소
→ 외부 API 호출 감소
→ 인프라 및 서비스 비용 감소</code></pre>
<hr>
<h2 id="3-비정상적인-트래픽-제어">3. 비정상적인 트래픽 제어</h2>
<p>Rate Limiter는 다음과 같은 요청을 걸러내는 데도 사용할 수 있다.</p>
<ul>
<li>Bot 요청</li>
<li>반복 로그인 시도</li>
<li>크롤링</li>
<li>API 오남용</li>
<li>비정상적인 사용자 행동</li>
</ul>
<p>따라서 단순한 성능 문제뿐만 아니라 시스템 보호 측면에서도 중요한 기능이다.</p>
<hr>
<h1 id="3-설계-요구사항-정리">3. 설계 요구사항 정리</h1>
<p>이번에는 서버 측 API를 보호하는 Rate Limiter를 설계한다고 가정한다.</p>
<p>우선 요구사항을 정리하면 다음과 같다.</p>
<h3 id="정확한-요청-제한">정확한 요청 제한</h3>
<p>설정된 제한을 초과하는 요청은 정확하게 차단해야 한다.</p>
<hr>
<h3 id="낮은-응답-지연">낮은 응답 지연</h3>
<p>Rate Limiter 자체가 모든 요청 앞에 위치할 수 있기 때문에 제한 여부를 확인하는 과정이 오래 걸리면 전체 API 응답 시간이 증가한다.</p>
<p>따라서 최대한 빠르게 판단해야 한다.</p>
<hr>
<h3 id="적은-메모리-사용">적은 메모리 사용</h3>
<p>사용자마다 요청 기록을 모두 보관하면 데이터가 매우 커질 수 있다.</p>
<p>따라서 제한 정책에 필요한 정보만 효율적으로 저장해야 한다.</p>
<hr>
<h3 id="분산-환경-지원">분산 환경 지원</h3>
<p>실제 대규모 시스템에서는 API 서버도 여러 대이기 때문에 Rate Limiter 역시 여러 서버에서 동시에 사용할 수 있어야 한다.</p>
<pre><code class="language-text">Server 1 ─┐
Server 2 ─┼→ 동일한 Rate Limit 상태
Server 3 ─┘</code></pre>
<hr>
<h3 id="제한-여부-전달">제한 여부 전달</h3>
<p>요청이 차단되었다면 사용자가 그 이유를 알 수 있어야 한다.</p>
<p>대표적으로</p>
<pre><code class="language-http">429 Too Many Requests</code></pre>
<p>와 같은 응답을 사용할 수 있다.</p>
<hr>
<h3 id="높은-장애-내성">높은 장애 내성</h3>
<p>Rate Limiter 자체에 문제가 생겼다고 해서 전체 서비스가 함께 중단되어서는 안 된다.</p>
<hr>
<h1 id="4-rate-limiter는-어디에-둘까">4. Rate Limiter는 어디에 둘까?</h1>
<p>크게 클라이언트 측과 서버 측을 생각해볼 수 있다.</p>
<h2 id="클라이언트-측">클라이언트 측</h2>
<pre><code class="language-text">Client
 ↓ Rate Limiting
Server</code></pre>
<p>하지만 클라이언트는 사용자가 직접 제어할 수 있기 때문에 제한 로직을 수정하거나 우회할 수 있다.</p>
<p>따라서 서버를 보호하기 위한 핵심 Rate Limiting을 클라이언트에만 의존하는 것은 적절하지 않다.</p>
<hr>
<h2 id="서버-측">서버 측</h2>
<p>서버 쪽에서 직접 제한하는 구조다.</p>
<pre><code class="language-text">Client
   ↓
Rate Limiter
   ↓
API Server</code></pre>
<p>또는 API 서버와 분리된 미들웨어나 API Gateway를 둘 수도 있다.</p>
<pre><code class="language-text">Client
   ↓
Rate Limiting Middleware
   ↓
API Server</code></pre>
<p>예를 들어 API가 초당 두 번의 요청만 허용한다고 가정해보자.</p>
<pre><code class="language-text">Request 1 → 허용
Request 2 → 허용
Request 3 → 차단</code></pre>
<p>세 번째 요청은 API 서버까지 전달되지 않고 Rate Limiter에서 차단할 수 있다.</p>
<hr>
<h1 id="5-api-gateway를-이용하는-방법">5. API Gateway를 이용하는 방법</h1>
<p>마이크로서비스나 클라우드 환경에서는 Rate Limiting을 API Gateway에서 처리하는 방법도 있다.</p>
<pre><code class="language-text">Client
  ↓
API Gateway
  ↓
Services</code></pre>
<p>API Gateway는 Rate Limiting 외에도</p>
<ul>
<li>사용자 인증</li>
<li>SSL 처리</li>
<li>IP 접근 제어</li>
<li>API Routing</li>
</ul>
<p>등의 기능을 함께 담당할 수 있다.</p>
<p>따라서 회사가 이미 API Gateway를 사용하고 있다면 Rate Limiter 역시 해당 계층에 두는 방법을 고려할 수 있다.</p>
<p>반대로 세밀한 제한 알고리즘이 필요하거나 서비스 특화 정책이 많다면 애플리케이션 내부에서 직접 구현할 수도 있다.</p>
<p>결국 위치를 결정할 때는</p>
<pre><code class="language-text">현재 기술 스택
+
운영 인력
+
제어해야 하는 범위
+
필요한 자유도</code></pre>
<p>를 함께 고려해야 한다.</p>
<hr>
<h1 id="6-처리율-제한-알고리즘">6. 처리율 제한 알고리즘</h1>
<p>Rate Limiter의 핵심은</p>
<blockquote>
<p><strong>현재 요청을 허용할 것인가, 거절할 것인가</strong></p>
</blockquote>
<p>를 결정하는 알고리즘이다.</p>
<p>대표적으로 다음과 같은 방식이 있다.</p>
<pre><code class="language-text">Token Bucket
Leaky Bucket
Fixed Window Counter
Sliding Window Log
Sliding Window Counter</code></pre>
<p>각 방법마다 처리 방식과 장단점이 다르다.</p>
<hr>
<h1 id="7-token-bucket">7. Token Bucket</h1>
<p>토큰 버킷은 가장 직관적인 Rate Limiting 알고리즘 중 하나다.</p>
<p>기본 구조는 다음과 같다.</p>
<pre><code class="language-text">      Token 공급
          ↓
   ┌───────────┐
   │ ● ● ● ●   │
   │   Bucket  │
   └───────────┘
          ↓
       요청 처리</code></pre>
<p>버킷에는 일정한 속도로 토큰이 추가된다.</p>
<p>요청 하나를 처리하려면 토큰 하나를 사용한다고 가정할 수 있다.</p>
<pre><code class="language-text">Request
   ↓
Token 존재?
  ↙    ↘
Yes    No
 ↓      ↓
허용    거절</code></pre>
<hr>
<h2 id="token-bucket의-두-가지-설정값">Token Bucket의 두 가지 설정값</h2>
<h3 id="bucket-size">Bucket Size</h3>
<p>버킷에 저장할 수 있는 최대 토큰 수</p>
<p>예:</p>
<pre><code class="language-text">Bucket Size = 10</code></pre>
<p>이라면 최대 10개의 토큰을 저장할 수 있다.</p>
<hr>
<h3 id="refill-rate">Refill Rate</h3>
<p>일정 시간마다 새롭게 추가되는 토큰의 수</p>
<p>예:</p>
<pre><code class="language-text">분당 4 Token</code></pre>
<p>버킷이 이미 가득 차 있다면 새롭게 추가되는 토큰은 저장되지 않는다.</p>
<hr>
<h1 id="8-token-bucket의-특징">8. Token Bucket의 특징</h1>
<p>토큰이 미리 쌓여 있을 수 있다는 것이 가장 중요한 특징이다.</p>
<p>예를 들어 버킷에 토큰 10개가 있다면 순간적으로 요청 10개가 한꺼번에 들어와도 모두 처리할 수 있다.</p>
<pre><code class="language-text">평소
● ● ● ● ● ● ● ● ● ●

갑자기 요청 10개 발생
↓↓↓↓↓↓↓↓↓↓

모두 처리 가능</code></pre>
<p>따라서 일정 수준의 <strong>Burst Traffic</strong>을 허용하면서 장기적인 평균 처리율은 제한할 수 있다.</p>
<hr>
<h2 id="버킷은-어떤-기준으로-만들까">버킷은 어떤 기준으로 만들까?</h2>
<p>제한하려는 대상에 따라 각각 다른 버킷을 만들 수 있다.</p>
<h3 id="사용자별">사용자별</h3>
<pre><code class="language-text">User A → Bucket A
User B → Bucket B</code></pre>
<h3 id="ip별">IP별</h3>
<pre><code class="language-text">IP A → Bucket A
IP B → Bucket B</code></pre>
<h3 id="api별">API별</h3>
<pre><code class="language-text">/login → Bucket
/payment → Bucket</code></pre>
<h3 id="전체-시스템">전체 시스템</h3>
<pre><code class="language-text">모든 Request
     ↓
Global Bucket</code></pre>
<p>예를 들어 전체 서버에서 초당 최대 10,000개의 요청만 처리하고 싶다면 모든 요청이 하나의 전역 제한 상태를 공유하도록 설계할 수 있다.</p>
<hr>
<h2 id="장점">장점</h2>
<ul>
<li>비교적 단순한 구조</li>
<li>저장해야 하는 상태가 적음</li>
<li>메모리 효율이 좋음</li>
<li>일정 수준의 Burst Traffic 허용 가능</li>
</ul>
<h2 id="단점">단점</h2>
<p>가장 중요한 문제는 설정값이다.</p>
<pre><code class="language-text">Bucket Size
+
Refill Rate</code></pre>
<p>를 실제 서비스 트래픽과 맞지 않게 설정하면 문제가 생긴다.</p>
<p>너무 낮으면 정상 사용자까지 차단하고,</p>
<p>너무 높으면 사실상 Rate Limiting의 효과가 없어질 수 있다.</p>
<hr>
<h1 id="9-leaky-bucket">9. Leaky Bucket</h1>
<p>누출 버킷은 Token Bucket과 비슷해 보이지만 목적이 조금 다르다.</p>
<p>Token Bucket이 일정 범위의 Burst를 허용한다면 Leaky Bucket은 <strong>요청을 일정한 속도로 처리하는 것</strong>에 더 가깝다.</p>
<pre><code class="language-text">Request
 ↓↓↓↓↓
┌─────────┐
│  Queue  │
└─────────┘
     ↓
 일정 속도로 처리</code></pre>
<p>일반적으로 FIFO Queue 방식으로 이해할 수 있다.</p>
<hr>
<h2 id="동작-과정">동작 과정</h2>
<pre><code class="language-text">요청 도착
   ↓
Queue 공간 확인
   ↓
공간 있음 → Queue 저장
공간 없음 → 요청 거절</code></pre>
<p>Queue에서는 일정한 속도로 요청을 하나씩 꺼낸다.</p>
<hr>
<h2 id="설정값">설정값</h2>
<h3 id="bucket-size-1">Bucket Size</h3>
<p>Queue에 보관할 수 있는 최대 요청 수</p>
<h3 id="outflow-rate">Outflow Rate</h3>
<p>일정 시간 동안 처리하는 요청 수</p>
<p>예를 들어</p>
<pre><code class="language-text">10 Request / second</code></pre>
<p>라면 요청이 아무리 빠르게 들어오더라도 밖으로 나가는 속도는 초당 10개로 유지된다.</p>
<hr>
<h2 id="장점-1">장점</h2>
<ul>
<li>메모리 사용량을 예측하기 쉬움</li>
<li>처리 속도가 일정함</li>
<li>순간적인 요청을 Queue에 임시 저장 가능</li>
</ul>
<h2 id="단점-1">단점</h2>
<p>짧은 시간에 매우 많은 요청이 들어오면 Queue가 가득 찰 수 있다.</p>
<pre><code class="language-text">기존 요청
██████████

새로운 요청
→ Queue Full
→ Drop</code></pre>
<p>또한 오래된 요청이 Queue를 차지하면서 새로운 요청이 거절될 수도 있다.</p>
<hr>
<h1 id="10-fixed-window-counter">10. Fixed Window Counter</h1>
<p>고정 윈도 카운터는 시간을 일정한 구간으로 나누어 요청 수를 계산하는 방법이다.</p>
<p>예를 들어 초당 최대 3개의 요청을 허용한다고 가정한다.</p>
<pre><code class="language-text">12:00:00 ~ 12:00:01
Request ≤ 3

12:00:01 ~ 12:00:02
Request ≤ 3</code></pre>
<p>각 시간 구간마다 카운터를 하나 둔다.</p>
<pre><code class="language-text">Request 도착
   ↓
Counter +1
   ↓
Limit 초과?
   ↓
허용 / 거절</code></pre>
<p>새로운 윈도가 시작되면 카운터를 다시 초기화한다.</p>
<hr>
<h2 id="장점-2">장점</h2>
<ul>
<li>구조가 단순함</li>
<li>메모리 사용량이 적음</li>
<li>구현하기 쉬움</li>
</ul>
<hr>
<h2 id="가장-큰-문제-window-boundary">가장 큰 문제: Window Boundary</h2>
<p>고정 윈도 방식은 시간 경계에서 문제가 발생할 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">분당 최대 5개</code></pre>
<p>라고 해보자.</p>
<p>첫 번째 윈도 마지막 30초에 5개가 몰리고</p>
<pre><code class="language-text">00:30 ~ 01:00
→ 5 Request</code></pre>
<p>다음 윈도 첫 30초에 다시 5개가 들어오면</p>
<pre><code class="language-text">01:00 ~ 01:30
→ 5 Request</code></pre>
<p>각 윈도만 보면 제한을 지켰다.</p>
<p>하지만 실제로 <code>00:30 ~ 01:30</code>이라는 연속된 1분 구간에는 10개의 요청이 발생한다.</p>
<pre><code class="language-text">실제 1분
→ 10 Request</code></pre>
<p>즉 <strong>윈도 경계 근처에서 허용량보다 훨씬 많은 요청이 처리될 수 있다는 문제</strong>가 있다.</p>
<hr>
<h1 id="11-sliding-window-log">11. Sliding Window Log</h1>
<p>이동 윈도 로그는 Fixed Window의 경계 문제를 해결하기 위해 실제 요청 시간을 기록한다.</p>
<p>예를 들어 분당 최대 2개의 요청만 허용한다고 해보자.</p>
<p>Rate Limiter는 요청의 Timestamp를 저장한다.</p>
<pre><code class="language-text">1:00:01
1:00:30</code></pre>
<p>새로운 요청이 들어오면 현재 시점에서 1분보다 오래된 기록을 제거하고, 현재 윈도 안의 요청 수를 확인한다.</p>
<hr>
<h2 id="일반적인-동작-흐름">일반적인 동작 흐름</h2>
<pre><code class="language-text">새 Request
   ↓
Window 밖 Timestamp 제거
   ↓
현재 요청 수 확인
   ↓
Limit 미만?
 ↙       ↘
Yes       No
 ↓         ↓
Timestamp 저장
Request 허용   Request 거절</code></pre>
<p>예를 들어</p>
<pre><code class="language-text">1:00:01 → 허용
1:00:30 → 허용
1:00:50 → 거절</code></pre>
<p>이후 <code>1:01:02</code>가 되면 <code>1:00:01</code> 기록은 최근 1분 범위에서 벗어난다.</p>
<p>따라서 다시 새로운 요청을 받을 수 있다.</p>
<hr>
<h2 id="거절된-요청도-로그에-저장할까">거절된 요청도 로그에 저장할까?</h2>
<p>이 부분은 처음 공부하면서 가장 헷갈렸던 부분이다.</p>
<p>일반적인 API Rate Limiter의 Sliding Window Log 구현에서는 <strong>허용된 요청을 기준으로 제한 상태를 계산하는 방식</strong>을 사용할 수 있다.</p>
<p>반면 로그인 공격 방어처럼</p>
<blockquote>
<p>&quot;실패한 시도 자체를 몇 번 했는가?&quot;</p>
</blockquote>
<p>가 중요한 보안 정책에서는 거절된 시도까지 별도로 추적할 수 있다.</p>
<p>따라서</p>
<pre><code class="language-text">일반적인 API 호출량 제한
→ 허용 요청 중심의 Rate Limit 상태

보안 / Abuse 방지
→ 모든 Attempt를 별도로 추적할 수도 있음</code></pre>
<p>처럼 목적에 따라 구분해서 이해하는 것이 좋다.</p>
<hr>
<h2 id="장점-3">장점</h2>
<p>최근 실제 요청 시간을 기준으로 계산하기 때문에 Window Boundary 문제가 없다.</p>
<p>즉 어느 시점에서 1분 구간을 보더라도 허용량을 비교적 정확하게 유지할 수 있다.</p>
<h2 id="단점-2">단점</h2>
<p>요청마다 Timestamp를 저장해야 한다.</p>
<p>따라서 다른 알고리즘에 비해 메모리 사용량이 커질 수 있다.</p>
<hr>
<h1 id="12-sliding-window-counter">12. Sliding Window Counter</h1>
<p>이동 윈도 카운터는</p>
<pre><code class="language-text">Fixed Window Counter
+
Sliding Window</code></pre>
<p>의 특성을 결합한 방식이다.</p>
<p>요청 하나하나의 Timestamp를 저장하는 대신 이전 윈도와 현재 윈도의 카운터를 이용해 현재 요청량을 근사한다.</p>
<hr>
<h2 id="예제">예제</h2>
<p>분당 최대 7개의 요청을 허용한다고 가정한다.</p>
<pre><code class="language-text">이전 1분 요청 = 5
현재 1분 요청 = 3
현재 위치 = 새 Window의 30%</code></pre>
<p>이전 윈도 중 현재 Sliding Window에 포함되는 비율은 약 70%다.</p>
<p>따라서</p>
<pre><code class="language-text">현재 요청량
=
현재 Window 요청
+
이전 Window 요청 × 겹치는 비율</code></pre>
<p>즉</p>
<pre><code class="language-text">3 + 5 × 0.7
= 6.5</code></pre>
<p>정도로 계산할 수 있다.</p>
<hr>
<h2 id="장점-4">장점</h2>
<p>Sliding Window Log처럼 모든 요청 Timestamp를 저장하지 않아도 된다.</p>
<p>따라서</p>
<ul>
<li>메모리 효율이 좋고</li>
<li>Fixed Window보다 경계 문제를 줄일 수 있다.</li>
</ul>
<hr>
<h2 id="단점-3">단점</h2>
<p>이전 Window 내부의 요청이 균등하게 분포했다고 가정하여 계산하기 때문에 정확한 값이 아니라 <strong>추정치</strong>라는 한계가 있다.</p>
<hr>
<h1 id="13-알고리즘-비교">13. 알고리즘 비교</h1>
<p>지금까지의 차이를 간단히 정리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>알고리즘</th>
<th>핵심 특징</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>Token Bucket</td>
<td>토큰을 소비하여 요청 처리</td>
<td>Burst 허용, 메모리 효율</td>
<td>파라미터 설정 중요</td>
</tr>
<tr>
<td>Leaky Bucket</td>
<td>Queue에서 일정 속도로 처리</td>
<td>출력 속도 안정</td>
<td>Queue 적체 가능</td>
</tr>
<tr>
<td>Fixed Window</td>
<td>일정 시간 단위 카운팅</td>
<td>단순, 저비용</td>
<td>경계 구간 문제</td>
</tr>
<tr>
<td>Sliding Window Log</td>
<td>실제 요청 Timestamp 저장</td>
<td>매우 정밀</td>
<td>메모리 사용 큼</td>
</tr>
<tr>
<td>Sliding Window Counter</td>
<td>이전·현재 Window를 가중 계산</td>
<td>정확성과 효율의 절충</td>
<td>근삿값 사용</td>
</tr>
</tbody></table>
<p>결국 특정 알고리즘이 항상 가장 좋은 것은 아니다.</p>
<pre><code class="language-text">Burst 허용 필요
→ Token Bucket

일정한 처리 속도 중요
→ Leaky Bucket

단순한 제한 정책
→ Fixed Window

정확한 제한
→ Sliding Window Log

메모리와 정확성 절충
→ Sliding Window Counter</code></pre>
<p>처럼 서비스의 트래픽 특성에 따라 결정할 수 있다.</p>
<hr>
<h1 id="14-rate-limiter의-기본-아키텍처">14. Rate Limiter의 기본 아키텍처</h1>
<p>알고리즘을 정했다면 다음으로 생각해야 하는 것은</p>
<blockquote>
<p><strong>요청 횟수를 어디에 저장할 것인가?</strong></p>
</blockquote>
<p>이다.</p>
<p>예를 들어 사용자별로 API 호출 횟수를 제한한다면 다음과 같은 정보가 필요하다.</p>
<pre><code class="language-text">User A → 5회
User B → 2회
User C → 10회</code></pre>
<p>이 정보는 모든 요청마다 조회되기 때문에 빠른 저장소가 필요하다.</p>
<p>그래서 메모리 기반 데이터 저장소인 Redis를 사용할 수 있다.</p>
<hr>
<h1 id="15-redis를-사용하는-이유">15. Redis를 사용하는 이유</h1>
<p>Redis는 메모리 기반으로 동작하기 때문에 빠른 읽기와 쓰기가 가능하다.</p>
<p>또한 일정 시간이 지나면 Key를 자동으로 제거하도록 TTL을 설정할 수 있다.</p>
<p>Rate Limiter에서는 다음과 같은 명령을 활용할 수 있다.</p>
<h3 id="incr">INCR</h3>
<p>카운터 값을 1 증가</p>
<pre><code class="language-text">5 → 6</code></pre>
<h3 id="expire">EXPIRE</h3>
<p>해당 Key의 유효 시간을 설정</p>
<pre><code class="language-text">user:123
TTL = 60초</code></pre>
<p>60초가 지나면 해당 카운터를 자동으로 제거할 수 있다.</p>
<hr>
<h1 id="16-기본-동작-흐름">16. 기본 동작 흐름</h1>
<p>전체적인 구조를 단순화하면 다음과 같다.</p>
<pre><code class="language-text">Client
   ↓
Rate Limiter Middleware
   ↓
Redis
   ↓
API Server</code></pre>
<p>Rate Limiter는 Redis에서 현재 요청 횟수를 확인한다.</p>
<pre><code class="language-text">Request
   ↓
현재 Counter 조회
   ↓
Limit 비교
   ↓
Limit 미만
→ Counter 증가
→ API 전달

Limit 초과
→ Request 거절</code></pre>
<hr>
<h1 id="17-처리율-제한-규칙">17. 처리율 제한 규칙</h1>
<p>실제 시스템에서는 모든 API에 같은 제한을 걸지 않는다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Marketing Message
→ 하루 최대 5회</code></pre>
<p>와</p>
<pre><code class="language-text">Login
→ 분당 최대 5회</code></pre>
<p>는 서로 다른 정책을 사용해야 한다.</p>
<p>따라서 제한 정책을 설정 파일 형태로 관리할 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-yaml">domain: messaging
descriptor:
  - key: message_type
    value: marketing
    rate_limit:
      unit: day
      requests_per_unit: 5</code></pre>
<p>는</p>
<pre><code class="language-text">Marketing Message
→ 하루 최대 5회</code></pre>
<p>라는 의미다.</p>
<p>로그인에 대해서는</p>
<pre><code class="language-yaml">domain: authentication
descriptor:
  - key: auth_type
    value: login
    rate_limit:
      unit: minute
      requests_per_unit: 5</code></pre>
<p>처럼 별도의 정책을 둘 수 있다.</p>
<hr>
<h1 id="18-제한을-초과하면-어떻게-할까">18. 제한을 초과하면 어떻게 할까?</h1>
<p>HTTP API에서는 대표적으로</p>
<pre><code class="language-http">429 Too Many Requests</code></pre>
<p>를 반환한다.</p>
<p>그리고 클라이언트가 현재 제한 상태를 알 수 있도록 관련 응답 헤더를 사용할 수도 있다.</p>
<p>예를 들면</p>
<pre><code class="language-text">Rate Limit
→ 전체 허용 요청 수

Remaining
→ 현재 남은 요청 수

Retry After
→ 언제 다시 요청할 수 있는지</code></pre>
<p>등의 정보를 전달하는 방식이다.</p>
<p>이를 통해 클라이언트 역시 무작정 요청을 반복하지 않고 적절한 시간 뒤 다시 시도하도록 만들 수 있다.</p>
<hr>
<h1 id="19-분산-환경에서-발생하는-문제">19. 분산 환경에서 발생하는 문제</h1>
<p>여기까지는 Rate Limiter 하나만 존재한다고 가정했다.</p>
<p>하지만 실제 대규모 환경에서는 여러 개의 서버가 동시에 요청을 처리한다.</p>
<pre><code class="language-text">Client
 ↓
Load Balancer
 ↓
Server 1
Server 2
Server 3</code></pre>
<p>이때 Rate Limiter를 구현하면 대표적으로 두 가지 문제가 발생할 수 있다.</p>
<pre><code class="language-text">Race Condition
Synchronization</code></pre>
<hr>
<h1 id="20-race-condition">20. Race Condition</h1>
<p>예를 들어 현재 카운터가 3이라고 해보자.</p>
<p>Limit은 4다.</p>
<p>Server A와 Server B가 동시에 카운터 값을 읽는다.</p>
<pre><code class="language-text">Server A → Counter = 3
Server B → Counter = 3</code></pre>
<p>두 서버 모두</p>
<pre><code class="language-text">3 &lt; 4</code></pre>
<p>라고 판단한다.</p>
<p>그리고 각각 카운터를 증가시킨다.</p>
<p>원래라면 하나의 요청만 허용해야 하지만 두 요청 모두 허용될 가능성이 있다.</p>
<p>이것이 <strong>경쟁 조건(Race Condition)</strong>이다.</p>
<hr>
<h1 id="21-lock을-사용하면">21. Lock을 사용하면?</h1>
<p>가장 단순한 방법은 하나의 요청이 값을 수정하는 동안 다른 요청의 접근을 막는 것이다.</p>
<pre><code class="language-text">Lock
 ↓
Read
 ↓
Update
 ↓
Unlock</code></pre>
<p>하지만 요청이 매우 많은 Rate Limiter에서는 Lock 자체가 병목이 될 수 있다.</p>
<p>따라서 Redis에서는 원자적인 연산이나 Lua Script 등을 활용하여 여러 연산을 하나의 단위로 처리하는 방법을 고려할 수 있다.</p>
<hr>
<h1 id="22-rate-limiter-간-동기화-문제">22. Rate Limiter 간 동기화 문제</h1>
<p>Rate Limiter 서버 자체도 여러 대라고 해보자.</p>
<pre><code class="language-text">Rate Limiter 1
Rate Limiter 2</code></pre>
<p>Client A가 첫 번째 요청에서는 Limiter 1을 사용하고 다음 요청에서 Limiter 2를 사용한다면 문제가 발생할 수 있다.</p>
<p>각 서버가 자신의 카운터만 가지고 있다면</p>
<pre><code class="language-text">Limiter 1 → User A = 3회
Limiter 2 → User A = 1회</code></pre>
<p>처럼 상태가 달라질 수 있다.</p>
<hr>
<h1 id="23-sticky-session">23. Sticky Session?</h1>
<p>한 가지 방법은 동일한 사용자의 요청을 항상 같은 Rate Limiter 서버로 보내는 것이다.</p>
<pre><code class="language-text">User A
 ↓
항상 Rate Limiter 1</code></pre>
<p>이를 Sticky Session 방식으로 구성할 수 있다.</p>
<p>하지만</p>
<ul>
<li>서버 추가 및 제거가 어려워질 수 있고</li>
<li>특정 서버로 요청이 편중될 수 있으며</li>
<li>시스템의 유연성이 낮아질 수 있다.</li>
</ul>
<p>따라서 보다 일반적인 방법은 <strong>공유 데이터 저장소를 사용하는 것</strong>이다.</p>
<hr>
<h1 id="24-중앙-redis를-이용한-방식">24. 중앙 Redis를 이용한 방식</h1>
<p>Rate Limiter 서버는 여러 개 두되 제한 상태를 하나의 Redis에서 공유한다.</p>
<pre><code class="language-text">Rate Limiter 1 ─┐
Rate Limiter 2 ─┼→ Redis
Rate Limiter 3 ─┘</code></pre>
<p>어떤 Rate Limiter가 요청을 처리하더라도 동일한 사용자 상태를 확인할 수 있다.</p>
<pre><code class="language-text">User A
→ Counter = 동일</code></pre>
<p>이를 통해 서버 간 상태 불일치 문제를 줄일 수 있다.</p>
<hr>
<h1 id="25-성능-최적화">25. 성능 최적화</h1>
<p>글로벌 서비스를 운영한다면 사용자와 Rate Limiter 사이의 물리적 거리도 고려해야 한다.</p>
<p>예를 들어 한국 사용자가 미국에 위치한 Rate Limiter까지 매번 요청한다면 네트워크 지연이 증가한다.</p>
<p>따라서 여러 지역에 Edge Server를 배치하여 사용자와 가까운 곳에서 요청을 처리하는 방법을 사용할 수 있다.</p>
<pre><code class="language-text">한국 User
→ 한국 Edge

미국 User
→ 미국 Edge</code></pre>
<p>다만 여러 지역에서 Rate Limit 상태를 공유해야 한다면 다시 데이터 동기화 문제가 발생한다.</p>
<p>이 경우 시스템의 요구사항에 따라 일관성 수준과 동기화 방법을 결정해야 한다.</p>
<hr>
<h1 id="26-모니터링">26. 모니터링</h1>
<p>Rate Limiter를 구축했다고 끝나는 것은 아니다.</p>
<p>운영하면서 실제 제한 정책이 적절한지 확인해야 한다.</p>
<p>대표적으로 두 가지를 확인할 수 있다.</p>
<h3 id="알고리즘이-트래픽-특성에-적합한가">알고리즘이 트래픽 특성에 적합한가?</h3>
<p>예를 들어 Burst Traffic이 많은데 지나치게 엄격한 알고리즘을 적용했다면 정상 요청이 많이 거절될 수 있다.</p>
<hr>
<h3 id="제한-정책이-적절한가">제한 정책이 적절한가?</h3>
<p>예를 들어</p>
<pre><code class="language-text">초당 2회</code></pre>
<p>라는 제한이 너무 낮아서 정상 사용자까지 계속 429 응답을 받고 있다면 정책을 조정해야 한다.</p>
<p>따라서</p>
<pre><code class="language-text">허용 Request
거절 Request
429 응답률
Latency
Redis 사용량</code></pre>
<p>등을 모니터링하면서 제한 정책을 개선해야 한다.</p>
<hr>
<h1 id="27-이벤트-트래픽에서는-어떻게-생각할까">27. 이벤트 트래픽에서는 어떻게 생각할까?</h1>
<p>예를 들어 이벤트 시작과 동시에 사용자 요청이 한꺼번에 몰리는 상황이 있을 수 있다.</p>
<pre><code class="language-text">EVENT OPEN
   ↓
↓↓↓↓↓↓↓↓↓↓↓↓
Burst Traffic</code></pre>
<p>이 경우 Token Bucket은 버킷에 저장되어 있던 토큰 범위 안에서 순간적인 요청 증가를 허용할 수 있다는 특징이 있다.</p>
<p>따라서 <strong>서비스가 어느 정도의 Burst Traffic을 받아들이면서 전체 처리량을 제한해야 하는 경우</strong> 고려할 수 있다.</p>
<p>다만 선착순 1명 당첨과 같은 이벤트에서</p>
<pre><code class="language-text">누가 실제 당첨자인가?</code></pre>
<p>를 결정하는 문제는 Rate Limiting과는 별도의 문제다.</p>
<p>Rate Limiter는 서버에 들어오는 트래픽을 제어하는 역할이고, 실제 선착순 당첨자를 정확히 한 명으로 결정하려면 동시성 제어와 원자적인 처리 등의 별도 설계가 필요하다.</p>
<hr>
<h1 id="28-hard-limit과-soft-limit">28. Hard Limit과 Soft Limit</h1>
<p>Rate Limit 정책에는 제한을 얼마나 엄격하게 적용할 것인지에 따라서도 차이가 있다.</p>
<h2 id="hard-limit">Hard Limit</h2>
<p>설정한 값을 절대 초과할 수 없다.</p>
<pre><code class="language-text">Limit = 100
→ 101번째 Request 무조건 거절</code></pre>
<h2 id="soft-limit">Soft Limit</h2>
<p>일시적으로 제한을 조금 초과하는 요청을 허용할 수 있다.</p>
<p>Burst Traffic을 어느 정도 허용해야 하는 시스템에서는 이런 정책을 고려할 수 있다.</p>
<hr>
<h1 id="29-rate-limiter는-여러-계층에서-사용할-수-있다">29. Rate Limiter는 여러 계층에서 사용할 수 있다</h1>
<p>이번 장에서는 주로 API 요청을 기준으로 Rate Limiter를 살펴봤지만 Rate Limiting 자체는 애플리케이션 계층에만 존재하는 기능은 아니다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Network Layer
→ IP / Packet 제한

Transport Layer
→ Connection 제한

Application Layer
→ HTTP / API 제한</code></pre>
<p>처럼 제한하려는 대상에 따라 서로 다른 계층에서 적용할 수 있다.</p>
<hr>
<h1 id="30-클라이언트도-rate-limit을-고려해야-한다">30. 클라이언트도 Rate Limit을 고려해야 한다</h1>
<p>서버가 Rate Limit을 적용한다고 해서 클라이언트는 아무것도 고려하지 않아도 되는 것은 아니다.</p>
<p>클라이언트 역시 불필요한 API 호출을 줄이는 것이 좋다.</p>
<h3 id="client-cache">Client Cache</h3>
<p>같은 데이터를 반복해서 서버에 요청하지 않도록 캐시를 사용할 수 있다.</p>
<hr>
<h3 id="rate-limit-정보-확인">Rate Limit 정보 확인</h3>
<p>서버의 제한 정책을 알고 있다면 짧은 시간 동안 지나치게 많은 요청을 보내지 않도록 조절할 수 있다.</p>
<hr>
<h3 id="retry">Retry</h3>
<p>429 응답을 받았다고 즉시 반복 요청하면 상황이 더 나빠질 수 있다.</p>
<pre><code class="language-text">429
 ↓
즉시 Retry
 ↓
429
 ↓
즉시 Retry
 ↓
429...</code></pre>
<p>따라서 일정 시간을 기다렸다가 다시 요청하는 Backoff 전략을 사용할 수 있다.</p>
<pre><code class="language-text">429
 ↓
Wait
 ↓
Retry</code></pre>
<hr>
<h1 id="전체-흐름-정리">전체 흐름 정리</h1>
<p>이번 장에서 Rate Limiter를 설계하는 과정을 하나로 정리하면 다음과 같다.</p>
<pre><code class="language-text">어떤 요청을 제한할 것인가?
        ↓
누구를 기준으로 제한할 것인가?
User / IP / API / Global
        ↓
어디에서 제한할 것인가?
Gateway / Middleware / Application
        ↓
어떤 알고리즘을 사용할 것인가?
        ↓
Token Bucket
Leaky Bucket
Fixed Window
Sliding Window
        ↓
현재 상태는 어디에 저장할 것인가?
        ↓
Redis
        ↓
분산 환경에서는?
        ↓
Race Condition 해결
상태 공유
        ↓
제한 초과
        ↓
429 응답
        ↓
모니터링하면서 정책 개선</code></pre>
<p>결국 Rate Limiter는 단순히</p>
<pre><code class="language-text">&quot;요청이 많으면 차단한다.&quot;</code></pre>
<p>정도의 기능이 아니었다.</p>
<p>실제로는</p>
<blockquote>
<p><strong>누구의 요청을 어떤 시간 단위로 얼마나 허용할 것인지 정의하고, 이를 여러 서버가 동시에 정확하고 빠르게 판단할 수 있도록 만드는 시스템</strong></p>
</blockquote>
<p>이라고 볼 수 있다.</p>
<hr>
<h1 id="공부하면서-정리한-핵심">공부하면서 정리한 핵심</h1>
<p>처음 Rate Limiter를 봤을 때는 단순히 서버 앞에 제한 값을 하나 설정하면 되는 기능이라고 생각했다.</p>
<p>하지만 실제로 설계를 따라가 보니 생각보다 고려할 것이 많았다.</p>
<pre><code class="language-text">제한 대상
+
시간 기준
+
알고리즘
+
저장소
+
분산 환경
+
동시성
+
장애 대응</code></pre>
<p>특히 흥미로웠던 부분은 같은 Rate Limiting이라도 알고리즘에 따라 트래픽을 바라보는 방식 자체가 다르다는 점이었다.</p>
<pre><code class="language-text">Token Bucket
→ 일정 Burst 허용

Leaky Bucket
→ 일정한 속도로 처리

Fixed Window
→ 구현은 단순하지만 경계 문제

Sliding Window Log
→ 정확하지만 높은 메모리 사용

Sliding Window Counter
→ 정확성과 효율 사이의 절충</code></pre>
<p>따라서 어떤 알고리즘이 가장 좋다고 결정하는 것이 아니라 <strong>서비스의 실제 트래픽 패턴에 맞는 방법을 선택하는 것이 중요하다.</strong></p>
<p>또 하나 중요하게 느낀 부분은 Rate Limiter가 단순한 비용 절감 장치만은 아니라는 점이다.</p>
<p>비정상 요청을 제한하고 서버를 보호하며, 특정 사용자나 API가 전체 시스템 자원을 독점하지 못하도록 만드는 역할도 한다.</p>
<p>특히 대규모 시스템에서는 Rate Limiter 자체도 여러 서버에서 동시에 동작해야 하므로 Redis와 같은 공유 저장소, 원자적인 연산, 동기화 문제까지 고려해야 한다는 점이 인상 깊었다.</p>
<p>이번 장은 단순한 요청 제한 알고리즘을 배우는 것을 넘어, <strong>작아 보이는 기능 하나도 대규모 분산 환경에서는 하나의 시스템 설계 문제가 될 수 있다는 것을 보여준 장</strong>이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[AEReport] Generalist의 역량이 중요해지는 시대]]></title>
            <link>https://velog.io/@jung_ji_in02/Generalist%EC%9D%98-%EC%97%AD%EB%9F%89%EC%9D%B4-%EC%A4%91%EC%9A%94%ED%95%B4%EC%A7%80%EB%8A%94-%EC%8B%9C%EB%8C%80</link>
            <guid>https://velog.io/@jung_ji_in02/Generalist%EC%9D%98-%EC%97%AD%EB%9F%89%EC%9D%B4-%EC%A4%91%EC%9A%94%ED%95%B4%EC%A7%80%EB%8A%94-%EC%8B%9C%EB%8C%80</guid>
            <pubDate>Tue, 21 Jul 2026 01:56:35 GMT</pubDate>
            <description><![CDATA[<p>이글은 2026년 6월 16일 Anthropic에서 발행한 보고서 <a href="https://www.anthropic.com/research/claude-code-expertise">Agentic coding and persistent returns to expertise</a> 를 읽고 작성한 후기입니다.</p>
<hr>
<h2 id="agentic-coding-and-persistent-returns-to-expertise">Agentic coding and persistent returns to expertise</h2>
<h3 id="「에이전트형-코딩과-지속되는-전문성의-가치」">「에이전트형 코딩과 지속되는 전문성의 가치」</h3>
<p>AI의 역량이 중요시되는 요즘, 이 보고서에서 말하고자 하는 가장 중요한 핵심은</p>
<blockquote>
<p>AI 코딩 도구를 잘 활용하는 데 필요한 핵심 능력은 직접 코드를 작성하는 능력보다, 자신이 해결하려는 문제 영역을 정확히 이해하는 능력이다.</p>
</blockquote>
<p>Claude Code를 사용할 때 사람은 주로 무엇을 해야 하는지를 결정하고, Claude는 그것을 어떻게 구현할지를 결정합니다.</p>
<p>따라서 </p>
<p>&quot;AI가 구현 작업을 상당 부분 대신하더라도, 업무의 목표를 정의하고 결과를 검증하며 오류를 바로잡는 도메인 전문성의 가치는 사라지지 않는다&quot;</p>
<p>는 것이 이 보고서의 핵심 주장입니다.</p>
<h3 id="claude-code는-어떤-업무에-사용되는가">Claude Code는 어떤 업무에 사용되는가</h3>
<p>보고서는 각 세션을 가장 대표적인 하나의 업무 유형으로 분류했습니다.</p>
<p>주요 비중은 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>업무 유형</th>
<th align="right">세션 비중</th>
</tr>
</thead>
<tbody><tr>
<td>고장 난 코드 수정</td>
<td align="right">26%</td>
</tr>
<tr>
<td>새로운 기능·프로그램 개발</td>
<td align="right">25%</td>
</tr>
<tr>
<td>소프트웨어 운영·배포·설정</td>
<td align="right">17%</td>
</tr>
<tr>
<td>문서·발표 자료 작성</td>
<td align="right">10%</td>
</tr>
<tr>
<td>기존 시스템 이해</td>
<td align="right">7%</td>
</tr>
<tr>
<td>변경 사항 계획</td>
<td align="right">7%</td>
</tr>
<tr>
<td>에이전트·파이프라인 관리</td>
<td align="right">3%</td>
</tr>
<tr>
<td>데이터 분석</td>
<td align="right">3%</td>
</tr>
<tr>
<td>코드 테스트</td>
<td align="right">2%</td>
</tr>
</tbody></table>
<p>전체적으로 보면 다음과 같이 정리할 수 있습니다.</p>
<ul>
<li>코드 작성·수정·테스트·에이전트 관리: 약 56%</li>
<li>소프트웨어 운영: 17%</li>
<li>시스템 이해 및 계획: 14%</li>
<li>데이터 분석 및 문서 작성: 13%</li>
</ul>
<p>또한 대부분의 세션은 완전히 새로운 코드를 만드는 것보다 기존 코드나 시스템을 다루는 데 사용됐습니다.</p>
<ul>
<li>기존 코드 수정: 48%</li>
<li>기존 코드 탐색·이해: 17%</li>
<li>처음부터 새 코드 작성: 14%</li>
<li>코드베이스를 직접 다루지 않는 작업: 약 20%</li>
</ul>
<p>즉, Claude Code는 단순한 코드 생성기가 아니라 기존 시스템 분석, 수정, 운영, 문서 작성, 데이터 분석까지 포함하는 비교적 넓은 업무 도구로 사용되고 있습니다.</p>
<h3 id="3-사람과-claude의-역할-분담">3. 사람과 Claude의 역할 분담</h3>
<p>보고서에서는 의사결정을 크게 두 가지로 나눴습니다.</p>
<ul>
<li>계획 의사결정: 무엇을 할지, 어떤 접근법을 사용할지, 언제 완료된 것으로 볼지</li>
<li>실행 의사결정: 어떤 파일을 수정할지, 어떤 코드를 작성할지, 어떤 명령어를 실행할지</li>
</ul>
<p>분석 결과는 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>의사결정 종류</th>
<th align="right">사람의 비중</th>
<th align="right">Claude의 비중</th>
</tr>
</thead>
<tbody><tr>
<td>계획 의사결정</td>
<td align="right">약 70%</td>
<td align="right">약 30%</td>
</tr>
<tr>
<td>실행 의사결정</td>
<td align="right">약 20%</td>
<td align="right">약 80%</td>
</tr>
</tbody></table>
<p>즉,</p>
<blockquote>
<p>사람은 무엇을 만들지 결정하고, Claude는 어떻게 만들지 결정하는 구조입니다.</p>
</blockquote>
<p>일반적인 세션에서는 사용자와 Claude가 약 4번 정도 대화를 주고받았습니다. 사용자가 프롬프트를 한 번 입력하면 Claude는 평균적으로 약 10개의 행동을 수행했습니다.</p>
<p>Claude가 수행하는 행동에는 다음이 포함됩니다.</p>
<ul>
<li>파일 읽기</li>
<li>코드 수정</li>
<li>명령어 실행</li>
<li>테스트 수행</li>
<li>결과 설명</li>
</ul>
<p>사용자 프롬프트 한 번당 Claude가 생성한 출력은 평균 약 2,400단어였습니다.</p>
<p>사용자가 Claude에게 계획과 실행 권한을 많이 위임할수록 Claude가 한 번의 지시로 수행하는 작업량도 증가했습니다.</p>
<h3 id="4-보고서에서-말하는-전문성의-의미">4. 보고서에서 말하는 ‘전문성’의 의미</h3>
<p>이 보고서에서 전문성은 직업이나 학력, 일반적인 코딩 실력과 동일한 개념이 아닙니다.</p>
<p>전문성은 해당 세션에서 사용자가 보여준 과업별 전문성을 의미합니다.</p>
<p>평가 기준은 크게 세 가지입니다.</p>
<ol>
<li>사용자가 요청을 얼마나 정확하고 구체적으로 정의하는가</li>
<li>결과를 검증하기 위해 무엇을 확인해 달라고 요청하는가</li>
<li>사용자와 Claude 중 누가 상대방의 오류를 더 자주 발견하고 수정하는가</li>
</ol>
<p>전문성은 다음 5단계로 분류됐습니다.</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>1단계 초보</td>
<td>일반적이고 모호한 요청, 오류를 잘 발견하지 못함</td>
</tr>
<tr>
<td>2단계 입문</td>
<td>일부 전문 용어를 사용하지만 검증 요청이 불명확함</td>
</tr>
<tr>
<td>3단계 중급</td>
<td>어느 정도 구체적인 요청과 비일반적인 검증 수행</td>
</tr>
<tr>
<td>4단계 고급</td>
<td>도메인 지식과 트레이드오프를 이해하고 오류를 일부 발견함</td>
</tr>
<tr>
<td>5단계 전문가</td>
<td>설계상의 제약과 위험을 예상하고 Claude의 오류를 적극적으로 수정함</td>
</tr>
</tbody></table>
<p>예를 들어 프로그래밍 경험이 없는 회계 담당자라도 정산 규칙, 예외 조건, 월말 처리 기준을 정확하게 제시하고 Claude가 놓친 예외를 발견한다면 해당 업무에서는 전문가로 평가됩니다.</p>
<p>반대로 경력이 많은 개발자라도 처음 사용하는 기술에 대해 일반적인 질문만 한다면 해당 세션에서는 초보자로 분류될 수 있습니다.</p>
<h3 id="5-전문성이-높을수록-claude가-더-많은-일을-수행한다">5. 전문성이 높을수록 Claude가 더 많은 일을 수행한다</h3>
<p>사용자의 전문성이 높아질수록 한 번의 프롬프트가 Claude에게 더 많은 작업을 유도했습니다.</p>
<table>
<thead>
<tr>
<th>사용자 전문성</th>
<th align="right">프롬프트당 Claude 행동 수</th>
<th align="right">프롬프트당 출력량</th>
</tr>
</thead>
<tbody><tr>
<td>초보</td>
<td align="right">약 5개</td>
<td align="right">약 600단어</td>
</tr>
<tr>
<td>전문가</td>
<td align="right">약 12개</td>
<td align="right">약 3,200단어</td>
</tr>
</tbody></table>
<p>전문가의 지시는 초보자의 지시보다 Claude의 행동을 두 배 이상 많이 유도했고, 출력량은 약 다섯 배 많았습니다.</p>
<p>이는 전문가가 단순히 더 많은 요청을 한다는 뜻이라기보다 다음 요소를 포함한 지시를 제공하기 때문으로 해석됩니다.</p>
<ul>
<li>작업 범위를 명확히 정의</li>
<li>필요한 조건과 제약 제시</li>
<li>오류 가능성 사전 지적</li>
<li>검증 방법 지정</li>
<li>완료 기준 설정</li>
</ul>
<p>보고서의 회귀분석에서도 전문성 단계가 한 단계 높아질 때마다 다른 조건을 통제한 뒤에도 다음과 같은 증가가 나타났습니다.</p>
<ul>
<li>Claude의 행동 수: 약 9% 증가</li>
<li>Claude의 출력량: 약 13% 증가</li>
</ul>
<p>다만 이는 관찰자료를 기반으로 한 연관성 분석이므로, 전문성이 작업량 증가의 직접적인 원인이라고 확정할 수는 없습니다.</p>
<h3 id="6-7개월-동안-claude-code-업무가-어떻게-변화했는가">6. 7개월 동안 Claude Code 업무가 어떻게 변화했는가</h3>
<p>2025년 10월부터 2026년 4월까지 Claude Code의 활용 방식이 크게 달라졌습니다.</p>
<h4 id="코드-오류-수정-비중-감소">코드 오류 수정 비중 감소</h4>
<p>고장 난 코드를 수정하는 세션 비중은 다음과 같이 감소했습니다.</p>
<ul>
<li>2025년 10월: 33%</li>
<li>2026년 4월: 19%
약 7개월 동안 거의 절반 수준으로 감소한 것입니다.</li>
</ul>
<h4 id="운영·분석·문서-작업-증가">운영·분석·문서 작업 증가</h4>
<p>반면 소프트웨어를 실제로 운영하거나 코드 외 결과물을 만드는 작업은 증가했습니다.</p>
<ul>
<li>소프트웨어 운영: 14% → 21%</li>
<li>문서 작성 및 데이터 분석: 약 10% → 20%</li>
</ul>
<p>보고서는 이를 Claude Code가 단순 디버깅 보조도구에서 다음 단계로 이동하고 있다는 신호로 해석합니다.</p>
<ul>
<li>기능을 처음부터 끝까지 개발</li>
<li>소프트웨어 배포 및 실행</li>
<li>데이터 분석</li>
<li>보고서·문서 작성</li>
<li>업무 자동화</li>
</ul>
<h3 id="7-수행되는-작업의-경제적-가치도-증가했다">7. 수행되는 작업의 경제적 가치도 증가했다</h3>
<p>연구진은 각 Claude Code 세션에서 수행된 작업이 프리랜서 시장에서 어느 정도 가격으로 거래될지를 추정했습니다.</p>
<p>분석 결과, 2025년 10월부터 2026년 4월 사이 평균 작업 가치가 약 27% 증가했습니다.</p>
<p>업무별 증가율은 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>업무 유형</th>
<th align="right">추정 가치 증가율</th>
</tr>
</thead>
<tbody><tr>
<td>새로운 기능 개발</td>
<td align="right">약 43%</td>
</tr>
<tr>
<td>소프트웨어 운영</td>
<td align="right">약 34%</td>
</tr>
<tr>
<td>코드 수정</td>
<td align="right">약 32%</td>
</tr>
<tr>
<td>전체 평균</td>
<td align="right">약 27%</td>
</tr>
<tr>
<td>문서·커뮤니케이션</td>
<td align="right">약 2%</td>
</tr>
</tbody></table>
<p>이는 Claude Code가 점차 더 복잡하고 가치가 높은 업무에 사용되고 있다는 의미입니다.</p>
<p>다만 이 금액은 실제 경제적 성과를 직접 측정한 것이 아닙니다. Claude Code 세션과 프리랜서 구인 게시물을 비교한 추정치이므로, 절대적인 금액보다는 시간에 따른 상대적 변화만 보는 것이 적절합니다.</p>
<h3 id="8-전문성이-높을수록-성공률이-높았다">8. 전문성이 높을수록 성공률이 높았다</h3>
<p>보고서는 성공을 여러 단계로 구분했습니다.</p>
<ul>
<li>부분 성공 이상: 사용자의 목표를 일부 또는 전부 달성</li>
<li>판정 성공: 전체 대화 내용을 기준으로 주요 목표를 달성</li>
<li>검증된 성공: 성공 판정과 함께 테스트 통과, 커밋, 풀 리퀘스트, 사용자 확인과 같은 객관적 증거가 존재</li>
</ul>
<p>전문성별 결과는 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>전문성 수준</th>
<th align="right">부분 성공 이상</th>
<th align="right">검증된 성공</th>
</tr>
</thead>
<tbody><tr>
<td>초보</td>
<td align="right">77%</td>
<td align="right">15%</td>
</tr>
<tr>
<td>중급 이상</td>
<td align="right">91~92%</td>
<td align="right">28~33%</td>
</tr>
</tbody></table>
<p>전문가 세션의 검증된 성공률은 초보 세션보다 두 배 이상 높았습니다.</p>
<p>다만 가장 큰 차이는 초보에서 중급으로 이동할 때 나타났습니다. 중급에서 전문가로 갈수록 추가적인 성공률 증가는 비교적 작았습니다.</p>
<p>따라서 보고서는 다음과 같이 해석합니다.</p>
<blockquote>
<p>AI 도구를 활용하기 위해 반드시 최고 수준의 전문가가 될 필요는 없지만, 해당 문제를 실무적으로 이해하는 수준의 역량은 매우 중요하다.</p>
</blockquote>
<h3 id="9-문제가-발생했을-때-전문성의-차이가-더-크게-나타났다">9. 문제가 발생했을 때 전문성의 차이가 더 크게 나타났다</h3>
<p>보고서는 오류, 테스트 실패, 반복 시도, 사용자 불만 등이 확인된 세션을 ‘문제가 발생한 세션’으로 정의했습니다.</p>
<p>문제가 발생한 이후에도 검증된 성공으로 끝난 비율은 다음과 같습니다.</p>
<ul>
<li>초보: 4%</li>
<li>전문가: 15%</li>
</ul>
<p>부분 성공 이상으로 회복한 비율은 다음과 같습니다.</p>
<ul>
<li>초보: 60%</li>
<li>중급<del>전문가: 80</del>81%</li>
</ul>
<p>문제가 생긴 뒤 아무런 코드도 작성하지 못하고 실패로 끝난 ‘포기 세션’ 비율도 차이가 났습니다.</p>
<ul>
<li>초보: 19%</li>
<li>나머지 사용자: 5~7%</li>
</ul>
<p>즉, 전문성은 처음부터 오류를 피하게 하는 역할뿐 아니라 오류가 발생했을 때 다음 행동을 결정하고 Claude를 다시 올바른 방향으로 유도하는 데 중요한 역할을 합니다.</p>
<h3 id="10-직업보다-도메인-전문성이-더-중요할-가능성">10. 직업보다 도메인 전문성이 더 중요할 가능성</h3>
<p>전체 세션에서 검증된 성공률은 다음과 같았습니다.</p>
<ul>
<li>컴퓨터·수학 관련 직종: 약 30%</li>
<li>그 외 직종: 약 26%</li>
</ul>
<p>실제로 코드를 추가하거나 수정한 세션만 보면 다음과 같습니다.</p>
<ul>
<li>컴퓨터·수학 관련 직종: 34%</li>
<li>그 외 직종: 29%</li>
</ul>
<p>차이는 약 5%포인트였습니다.</p>
<p>코딩 세션이 많은 상위 10개 직업군 모두 소프트웨어 관련 직업군의 성공률과 7%포인트 이내에 있었습니다.</p>
<p>주요 직업별 검증된 성공률은 대략 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>직업군</th>
<th align="right">검증된 성공률</th>
</tr>
</thead>
<tbody><tr>
<td>경영·관리</td>
<td align="right">37%</td>
</tr>
<tr>
<td>소프트웨어·수학</td>
<td align="right">34%</td>
</tr>
<tr>
<td>법률</td>
<td align="right">33%</td>
</tr>
<tr>
<td>비즈니스·금융</td>
<td align="right">29%</td>
</tr>
<tr>
<td>보건의료</td>
<td align="right">28%</td>
</tr>
<tr>
<td>예술·디자인·미디어</td>
<td align="right">28%</td>
</tr>
<tr>
<td>영업</td>
<td align="right">28%</td>
</tr>
<tr>
<td>건축·공학</td>
<td align="right">27%</td>
</tr>
<tr>
<td>교육</td>
<td align="right">27%</td>
</tr>
<tr>
<td>과학</td>
<td align="right">27%</td>
</tr>
</tbody></table>
<p>경영·관리 직군이 소프트웨어 직군보다 약간 높은 성공률을 보였습니다.</p>
<p>보고서는 경영자의 다음과 같은 능력이 AI 에이전트 지시에도 적용될 가능성을 제시합니다.</p>
<ul>
<li>업무 분해</li>
<li>목표 설정</li>
<li>역할 위임</li>
<li>결과 검토</li>
<li>완료 조건 확인</li>
</ul>
<p>다만 관리직 사용자가 결과에 대한 확인 표현을 더 명확하게 남겼기 때문에 성공 판정이 높게 나왔을 가능성도 있습니다. 따라서 직업별 차이에 대해서는 신중한 해석이 필요합니다.</p>
<h3 id="11-노동시장에-대한-시사점">11. 노동시장에 대한 시사점</h3>
<p>보고서는 AI 코딩 에이전트가 노동시장에 미칠 수 있는 영향을 다음과 같이 정리합니다.</p>
<h4 id="코딩-능력의-진입장벽은-낮아질-수-있음">코딩 능력의 진입장벽은 낮아질 수 있음</h4>
<p>비개발 직군도 소프트웨어 직군과 유사한 성공률로 코딩 업무를 수행했습니다.\</p>
<p>이는 앞으로 법률, 금융, 의료, 교육, 경영 등 다양한 직군에서 직접 업무용 프로그램과 자동화 도구를 제작할 가능성이 커진다는 의미입니다.</p>
<h4 id="구현-능력보다-문제-정의-능력이-중요해질-수-있음">구현 능력보다 문제 정의 능력이 중요해질 수 있음</h4>
<p>Claude가 코드를 작성하고 명령어를 실행하는 비중이 높아지면서 사람의 역할은 다음과 같이 변화할 수 있습니다.</p>
<ul>
<li>문제를 정확하게 정의</li>
<li>필요한 조건과 제약을 설명</li>
<li>결과가 맞는지 판단</li>
<li>오류를 발견하고 수정 방향 제시</li>
<li>비즈니스나 업무 맥락과 연결</li>
</ul>
<h4 id="전문성은-대체되기보다-증폭될-가능성이-있음">전문성은 대체되기보다 증폭될 가능성이 있음</h4>
<p>AI가 전문성을 완전히 대체한다기보다, 전문성을 가진 사람이 더 많은 기술적 업무를 수행하도록 돕는 형태가 나타났습니다.</p>
<p>즉, 같은 AI 도구를 사용하더라도 도메인 이해가 높은 사람은 더 구체적인 지시를 내리고, 더 많은 작업을 위임하며, 오류 발생 후에도 더 잘 회복했습니다.</p>
<h4 id="최고-수준의-전문가만-혜택을-받는-것은-아님">최고 수준의 전문가만 혜택을 받는 것은 아님</h4>
<p>보고서에서는 초보에서 중급으로 올라갈 때 성공률이 크게 향상되고, 중급 이후에는 증가 폭이 작아졌습니다.</p>
<p>따라서 깊은 전문성도 도움이 되지만, AI 활용에서는 우선 다음 수준을 갖추는 것이 중요합니다.</p>
<ul>
<li>업무 구조 이해</li>
<li>핵심 용어 사용</li>
<li>기본적인 오류 판단</li>
<li>구체적인 검증 요청</li>
<li>명확한 완료 기준 설정</li>
</ul>
<h3 id="12-보고서의-한계">12. 보고서의 한계</h3>
<p>보고서의 결과를 해석할 때는 다음 한계를 고려해야 합니다.</p>
<h4 id="실제-업무-성과를-측정하지-못함">실제 업무 성과를 측정하지 못함</h4>
<p>세션 안에서 테스트가 통과하거나 사용자가 만족했다고 말했더라도, 해당 코드가 이후 실제 제품에 적용됐는지 또는 경제적 가치를 만들었는지는 확인하지 못했습니다.</p>
<h4 id="일부-claude-code-사용-방식이-제외됨">일부 Claude Code 사용 방식이 제외됨</h4>
<p>다음과 같은 비대화형 사용은 분석에서 빠졌습니다.</p>
<ul>
<li>SDK를 통한 사용</li>
<li>제3자 개발환경 통합</li>
<li>자동화 파이프라인</li>
<li>단일 프롬프트를 실행하는 headless 모드</li>
</ul>
<p>따라서 보고서 결과는 대화형 Claude Code 사용에 한정됩니다.</p>
<h4 id="분류를-ai-모델이-수행함">분류를 AI 모델이 수행함</h4>
<p>업무 유형, 직업, 전문성, 성공 여부는 연구자가 모든 세션을 직접 읽어 판정한 것이 아니라 AI 분류기가 대화 내용을 읽어 추정했습니다.</p>
<p>일부 독립적인 로그 데이터와 비교해 검증했지만, 분류 오류 가능성은 남아 있습니다.</p>
<h4 id="인과관계를-확정할-수-없음">인과관계를 확정할 수 없음</h4>
<p>전문성이 높은 세션에서 성공률이 높았다는 것은 확인됐지만, 전문성 자체가 성공률을 높였다고 완전히 입증한 것은 아닙니다.</p>
<p>전문가가 더 적합한 문제를 선택하거나, 더 좋은 개발환경을 사용하거나, 성공 여부를 더 명확히 표현했을 가능성도 존재합니다.</p>
<hr>
<h3 id="최종-요약">최종 요약</h3>
<p>이 보고서는 AI 코딩 에이전트가 프로그래밍의 구현 장벽을 크게 낮추고 있음을 보여줍니다. 개발자가 아닌 사람도 자신이 잘 아는 분야의 문제를 명확히 설명할 수 있다면 소프트웨어 직군과 비슷한 수준으로 코딩 업무를 수행할 수 있었습니다.</p>
<p>그러나 AI가 모든 전문성을 대체하는 것은 아닙니다. 오히려 다음 능력의 중요성이 커지고 있습니다.</p>
<blockquote>
<p>무엇을 만들어야 하는지 결정하고, 요구사항과 제약을 구체화하며, 결과가 올바른지 검증하는 능력</p>
</blockquote>
<p>따라서 보고서가 제시하는 미래의 업무 구조는 다음과 같습니다.</p>
<blockquote>
<p>사람은 문제와 목표를 책임지고, AI는 구현과 실행을 담당한다.</p>
</blockquote>
<p>그리고 이 구조에서는 코딩 숙련도 자체보다 도메인 이해, 문제 정의, 검증 능력, 오류 수정 능력이 AI 활용 성과를 결정하는 핵심 역량이 될 가능성이 높습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 3장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-3%EC%9E%A5-%EC%A0%95%EB%A6%AC-11xsjbs6</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-3%EC%9E%A5-%EC%A0%95%EB%A6%AC-11xsjbs6</guid>
            <pubDate>Wed, 08 Jul 2026 07:41:55 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-2%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 2장 정리</a>
다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-4%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 4장 정리</a></p>
</blockquote>
<h1 id="3장-시스템-설계-면접-공략법">3장. 시스템 설계 면접 공략법</h1>
<p>시스템 설계 면접에서는 단순히 좋은 아키텍처를 알고 있는 것만으로 충분하지 않다.</p>
<p>같은 문제라도</p>
<ul>
<li>요구사항</li>
<li>사용자 규모</li>
<li>데이터 특성</li>
<li>기존 기술 환경</li>
<li>성능 목표</li>
</ul>
<p>에 따라 설계가 달라질 수 있기 때문이다.</p>
<p>따라서 시스템 설계 면접의 핵심은 정답을 빠르게 말하는 것이 아니라,</p>
<blockquote>
<p><strong>주어진 문제를 어떻게 정의하고, 어떤 근거로 설계를 만들어가는지를 보여주는 것</strong></p>
</blockquote>
<p>이라고 이해할 수 있다.</p>
<p>전체적인 면접 흐름은 크게 다음 네 단계로 정리할 수 있다.</p>
<pre><code class="language-text">1. 문제 이해 및 범위 확정
        ↓
2. 개략적인 설계안 제시
        ↓
3. 핵심 영역 상세 설계
        ↓
4. 최종 정리 및 개선점 논의</code></pre>
<hr>
<h1 id="1-문제-이해-및-설계-범위-확정">1. 문제 이해 및 설계 범위 확정</h1>
<p>시스템 설계 문제를 받았다고 해서 바로 서버와 데이터베이스부터 그리기 시작하면 안 된다.</p>
<p>예를 들어</p>
<pre><code class="language-text">&quot;뉴스 피드 시스템을 설계해보세요.&quot;</code></pre>
<p>라는 질문을 받았다고 하자.</p>
<p>이 문장만으로는 실제로 무엇을 만들어야 하는지 알 수 없다.</p>
<p>예를 들어 다음 내용이 모두 정해져 있지 않다.</p>
<ul>
<li>모바일만 지원하는가?</li>
<li>웹도 지원하는가?</li>
<li>게시글 작성 기능이 필요한가?</li>
<li>이미지와 영상도 지원하는가?</li>
<li>피드는 시간순인가?</li>
<li>추천 알고리즘을 적용하는가?</li>
<li>사용자는 몇 명인가?</li>
</ul>
<p>이 상태에서 바로 설계를 시작하면 내가 임의로 설정한 시스템을 설계하게 될 가능성이 높다.</p>
<p>따라서 가장 먼저 해야 하는 것은 <strong>질문을 통해 문제의 범위를 좁히는 것</strong>이다.</p>
<hr>
<h2 id="왜-질문부터-해야-할까">왜 질문부터 해야 할까?</h2>
<p>시스템 설계 문제에는 하나의 정답이 존재하지 않는 경우가 많다.</p>
<p>예를 들어 사용자 1만 명을 대상으로 하는 서비스와 사용자 1억 명을 대상으로 하는 서비스는 동일한 구조를 사용할 필요가 없다.</p>
<pre><code class="language-text">사용자 1만 명
→ 단순한 구조로도 충분할 수 있음

사용자 1억 명
→ 캐시, 샤딩, 메시지 큐 등 추가 고려</code></pre>
<p>따라서 면접 초반의 질문은 단순히 정보를 얻는 과정이 아니라</p>
<blockquote>
<p><strong>내가 설계해야 하는 시스템의 경계를 결정하는 과정</strong></p>
</blockquote>
<p>이라고 볼 수 있다.</p>
<hr>
<h1 id="2-어떤-질문을-해야-할까">2. 어떤 질문을 해야 할까?</h1>
<p>질문은 크게 세 가지 목적을 가진다고 정리했다.</p>
<h2 id="1-기능-범위-확인">1. 기능 범위 확인</h2>
<p>가장 먼저</p>
<blockquote>
<p><strong>이 시스템이 무엇을 해야 하는가?</strong></p>
</blockquote>
<p>를 확인한다.</p>
<p>예를 들어</p>
<ul>
<li>사용자가 어떤 기능을 사용할 수 있는가?</li>
<li>가장 중요한 기능은 무엇인가?</li>
<li>반드시 구현해야 하는 기능은 무엇인가?</li>
<li>이번 설계에서 제외해도 되는 기능은 무엇인가?</li>
</ul>
<p>등을 확인할 수 있다.</p>
<p>이 과정을 통해 불필요한 기능까지 설계하는 것을 방지할 수 있다.</p>
<hr>
<h2 id="2-시스템-규모-확인">2. 시스템 규모 확인</h2>
<p>다음은</p>
<blockquote>
<p><strong>얼마나 큰 시스템을 설계해야 하는가?</strong></p>
</blockquote>
<p>를 확인하는 과정이다.</p>
<p>예를 들어</p>
<ul>
<li>DAU는 얼마인가?</li>
<li>사용자 한 명이 하루 평균 몇 번 요청하는가?</li>
<li>데이터는 얼마나 생성되는가?</li>
<li>서비스가 앞으로 얼마나 성장할 것으로 예상하는가?</li>
</ul>
<p>등을 질문할 수 있다.</p>
<p>이를 바탕으로 이전 장에서 배운 규모 추정을 적용할 수 있다.</p>
<pre><code class="language-text">사용자 수
  ↓
일일 요청량
  ↓
평균 QPS
  ↓
Peak QPS
  ↓
필요한 서버와 DB 규모</code></pre>
<p>즉 사용자 규모를 확인하는 이유는 단순한 정보 수집이 아니라 <strong>뒤에서 설계할 인프라의 크기를 결정하기 위해서</strong>다.</p>
<hr>
<h2 id="3-기술적-제약-확인">3. 기술적 제약 확인</h2>
<p>마지막으로 현재 사용할 수 있는 기술 환경을 확인한다.</p>
<p>예를 들면</p>
<ul>
<li>회사에서 사용하는 데이터베이스</li>
<li>기존 클라우드 환경</li>
<li>사용 가능한 내부 서비스</li>
<li>기존 메시지 큐나 캐시 시스템</li>
<li>특정 기술을 반드시 사용해야 하는지</li>
</ul>
<p>등이다.</p>
<p>실제 개발에서는 이미 회사가 사용하고 있는 인프라가 존재하기 때문에 모든 시스템을 새롭게 구축할 필요는 없다.</p>
<pre><code class="language-text">이미 Redis 사용 중
→ 캐시를 새롭게 만들 필요 없음

AWS 환경 사용 중
→ 기존 AWS 서비스 활용 가능</code></pre>
<p>결국 면접 초반 질문을 통해 확인해야 하는 것은 다음 세 가지다.</p>
<pre><code class="language-text">무엇을 만들어야 하는가?
       +
얼마나 크게 만들어야 하는가?
       +
어떤 환경에서 만들어야 하는가?</code></pre>
<hr>
<h1 id="3-가정은-반드시-기록한다">3. 가정은 반드시 기록한다</h1>
<p>질문을 했지만 면접관이 모든 조건을 정해주는 것은 아니다.</p>
<p>경우에 따라</p>
<blockquote>
<p>&quot;합리적인 값으로 가정해보세요.&quot;</p>
</blockquote>
<p>라고 할 수도 있다.</p>
<p>이 경우에는 내가 세운 가정을 명확하게 기록해두는 것이 좋다.</p>
<p>예를 들어</p>
<pre><code class="language-text">DAU = 1,000만 명
사용자당 하루 게시글 = 2개
친구 수 최대 = 5,000명
피드 정렬 = 최신순</code></pre>
<p>처럼 정리한다.</p>
<p>이렇게 해두면 이후 설계에서</p>
<pre><code class="language-text">&quot;DAU를 1,000만 명으로 가정했기 때문에
이 정도 규모의 캐시가 필요합니다.&quot;</code></pre>
<p>와 같이 설계 근거를 설명할 수 있다.</p>
<hr>
<h1 id="4-예제-뉴스-피드-시스템-요구사항-정리">4. 예제: 뉴스 피드 시스템 요구사항 정리</h1>
<p>뉴스 피드 시스템 설계를 예로 들어보자.</p>
<p>처음에는 단순히</p>
<pre><code class="language-text">뉴스 피드 시스템 설계</code></pre>
<p>라는 문제만 주어졌다고 가정한다.</p>
<p>질문을 통해 다음과 같이 요구사항을 구체화할 수 있다.</p>
<h3 id="지원-환경">지원 환경</h3>
<ul>
<li>웹 지원</li>
<li>모바일 지원</li>
</ul>
<h3 id="핵심-기능">핵심 기능</h3>
<ul>
<li>사용자가 새로운 게시글 작성</li>
<li>친구가 작성한 게시글을 자신의 피드에서 조회</li>
</ul>
<h3 id="피드-정렬">피드 정렬</h3>
<ul>
<li>최신 게시글부터 시간 역순으로 정렬</li>
</ul>
<h3 id="사용자-관계">사용자 관계</h3>
<ul>
<li>한 사용자의 최대 친구 수 약 5,000명</li>
</ul>
<h3 id="사용자-규모">사용자 규모</h3>
<ul>
<li>DAU 약 1,000만 명</li>
</ul>
<h3 id="콘텐츠">콘텐츠</h3>
<ul>
<li>텍스트</li>
<li>이미지</li>
<li>비디오</li>
</ul>
<p>처음의 모호한 문제였던</p>
<pre><code class="language-text">뉴스 피드를 설계하시오.</code></pre>
<p>가 질문을 통해</p>
<pre><code class="language-text">DAU 1,000만 명 규모에서
웹/모바일 사용자가
텍스트와 미디어 게시글을 작성하고,
최대 5,000명의 친구 게시글을
최신순으로 조회할 수 있는 시스템</code></pre>
<p>으로 구체화된 것이다.</p>
<p>이 정도까지 범위를 좁혀야 본격적인 시스템 설계를 시작할 수 있다.</p>
<hr>
<h1 id="5-개략적인-설계안-제시">5. 개략적인 설계안 제시</h1>
<p>요구사항을 정리했다면 바로 세부 구현으로 들어가기보다는 먼저 시스템 전체 구조를 그린다.</p>
<p>여기서는</p>
<blockquote>
<p><strong>어떤 컴포넌트들이 필요하고 서로 어떻게 연결되는가</strong></p>
</blockquote>
<p>정도만 보여주는 것이 중요하다.</p>
<p>대표적으로 다음과 같은 구성 요소를 사용할 수 있다.</p>
<pre><code class="language-text">Client
  ↓
Load Balancer
  ↓
Web Server
  ↓
Service
  ↓
Cache / Database / Message Queue</code></pre>
<p>필요하다면</p>
<ul>
<li>CDN</li>
<li>Object Storage</li>
<li>Notification Service</li>
</ul>
<p>등도 추가할 수 있다.</p>
<p>중요한 것은 처음부터 모든 세부사항을 완벽하게 그리는 것이 아니다.</p>
<p>먼저 큰 구조를 보여주고 면접관과 방향을 맞춘다.</p>
<hr>
<h1 id="6-면접관과-설계를-맞춰가는-과정">6. 면접관과 설계를 맞춰가는 과정</h1>
<p>시스템 설계 면접은 혼자 화이트보드에 구조를 그리는 시험이라고 생각하기 쉽지만, 실제로는 면접관과 계속 설계 방향을 조율하는 과정에 가깝다.</p>
<p>예를 들어</p>
<pre><code class="language-text">&quot;우선 게시글 저장과 피드 생성을
두 개의 주요 흐름으로 나눠보겠습니다.&quot;</code></pre>
<p>라고 설명한 뒤 전체 구조를 보여줄 수 있다.</p>
<p>그리고</p>
<pre><code class="language-text">&quot;이 구조에서 피드 생성 쪽을
조금 더 자세히 살펴봐도 될까요?&quot;</code></pre>
<p>와 같이 면접관의 의견을 확인하면서 진행할 수 있다.</p>
<p>이렇게 하면 면접관이 중요하게 생각하는 영역에 시간을 집중할 수 있다.</p>
<hr>
<h1 id="7-예제-뉴스-피드-시스템의-핵심-흐름">7. 예제: 뉴스 피드 시스템의 핵심 흐름</h1>
<p>뉴스 피드 시스템은 크게 두 가지 흐름으로 나누어 생각할 수 있다.</p>
<pre><code class="language-text">게시글 작성
     +
뉴스 피드 조회</code></pre>
<hr>
<h2 id="1-게시글-작성">1. 게시글 작성</h2>
<p>사용자가 새로운 게시글을 작성한다.</p>
<p>예를 들어 다음과 같은 API를 생각할 수 있다.</p>
<pre><code class="language-http">POST /v1/me/feed</code></pre>
<p>요청은 대략 다음과 같이 처리될 수 있다.</p>
<pre><code class="language-text">사용자
  ↓
Load Balancer
  ↓
Web Server
  ↓
Posting Service</code></pre>
<p>Posting Service는 게시글을 데이터베이스 등에 저장한다.</p>
<p>이후 친구들의 뉴스 피드에 해당 게시글을 전달하기 위한 작업이 필요하다.</p>
<pre><code class="language-text">Posting Service
       ↓
Feed Service
       ↓
친구들의 Feed Cache</code></pre>
<p>필요하다면 알림 서비스에도 이벤트를 전달할 수 있다.</p>
<pre><code class="language-text">새 게시글
   ↓
Notification Service
   ↓
친구에게 알림</code></pre>
<hr>
<h1 id="8-뉴스-피드-조회">8. 뉴스 피드 조회</h1>
<p>사용자가 자신의 뉴스 피드를 확인한다고 하자.</p>
<pre><code class="language-http">GET /v1/me/feed</code></pre>
<p>대략적인 흐름은 다음과 같다.</p>
<pre><code class="language-text">사용자
  ↓
Load Balancer
  ↓
Web Server
  ↓
Feed Service
  ↓
Feed Cache</code></pre>
<p>Feed Service가 해당 사용자의 피드 데이터를 가져와 최신순으로 반환한다.</p>
<p>피드를 조회할 때마다 모든 친구들의 게시글을 DB에서 직접 검색한다면 요청 비용이 커질 수 있다.</p>
<p>따라서 뉴스 피드처럼 읽기 요청이 많은 시스템에서는 캐시 활용이 중요해질 수 있다.</p>
<hr>
<h1 id="9-개략-설계에서-확인해야-할-것">9. 개략 설계에서 확인해야 할 것</h1>
<p>큰 구조를 그렸다면 앞에서 정의한 요구사항을 실제로 만족하는지 확인해야 한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">DAU = 1,000만 명</code></pre>
<p>이라고 가정했다면</p>
<ul>
<li>현재 구조로 예상 QPS를 처리할 수 있는지</li>
<li>DB가 감당할 수 있는지</li>
<li>캐시가 필요한지</li>
<li>웹 서버를 몇 대 정도 두어야 하는지</li>
</ul>
<p>등을 설명할 수 있다.</p>
<p>즉 시스템 설계는 단순하게 그림을 그리는 것이 아니라</p>
<pre><code class="language-text">요구사항
  ↓
규모 추정
  ↓
아키텍처</code></pre>
<p>가 서로 연결되어 있어야 한다.</p>
<hr>
<h1 id="10-상세-설계">10. 상세 설계</h1>
<p>전체적인 구조에 대한 방향이 정해졌다면 이제 중요한 영역을 골라 깊게 들어간다.</p>
<p>여기서 중요한 점은</p>
<blockquote>
<p><strong>모든 컴포넌트를 동일한 수준으로 설명하려고 하지 않는 것</strong></p>
</blockquote>
<p>이다.</p>
<p>면접 시간은 제한되어 있기 때문에 시스템에서 가장 중요한 문제를 골라 집중해야 한다.</p>
<hr>
<h2 id="문제마다-중요한-영역이-다르다">문제마다 중요한 영역이 다르다</h2>
<p>예를 들어 단축 URL 시스템이라면</p>
<pre><code class="language-text">긴 URL
 ↓
Short URL 생성</code></pre>
<p>과정에서 어떤 방식으로 짧은 값을 생성할 것인지가 핵심이 될 수 있다.</p>
<p>따라서</p>
<ul>
<li>Hash</li>
<li>Base62</li>
<li>충돌 처리</li>
</ul>
<p>등을 자세히 논의할 수 있다.</p>
<p>반면 실시간 채팅 시스템에서는</p>
<ul>
<li>메시지 전달 지연</li>
<li>실시간 연결</li>
<li>온라인 상태</li>
<li>메시지 순서</li>
</ul>
<p>등이 더 중요한 문제가 될 수 있다.</p>
<p>즉 상세 설계에서는</p>
<blockquote>
<p><strong>현재 시스템에서 가장 어려운 문제가 무엇인지 찾는 것</strong></p>
</blockquote>
<p>이 중요하다.</p>
<hr>
<h1 id="11-뉴스-피드의-상세-설계">11. 뉴스 피드의 상세 설계</h1>
<p>뉴스 피드 문제에서는 특히 두 가지 부분을 자세히 살펴볼 수 있다.</p>
<h2 id="1-게시글을-친구들의-피드에-어떻게-전달할-것인가">1. 게시글을 친구들의 피드에 어떻게 전달할 것인가?</h2>
<p>사용자가 게시글을 하나 작성했다고 가정하자.</p>
<p>친구가 10명이라면 크게 문제가 되지 않는다.</p>
<p>하지만 친구가 수천 명이라면 상황이 달라진다.</p>
<pre><code class="language-text">새 게시글
  ↓
친구 1 Feed
친구 2 Feed
친구 3 Feed
...
친구 5,000 Feed</code></pre>
<p>한 번의 게시글 작성으로 많은 사용자 피드에 데이터를 전달해야 할 수 있다.</p>
<p>이러한 처리를 <strong>Fan-out</strong> 관점에서 고민할 수 있다.</p>
<p>게시글 작성 요청에서 모든 작업을 직접 처리하면 응답 시간이 길어질 수 있으므로 메시지 큐를 활용하여 비동기적으로 분리하는 방법도 고려할 수 있다.</p>
<pre><code class="language-text">게시글 작성
   ↓
Message Queue
   ↓
Feed Worker
   ↓
친구 Feed Cache 업데이트</code></pre>
<hr>
<h2 id="2-뉴스-피드를-얼마나-빠르게-가져올-것인가">2. 뉴스 피드를 얼마나 빠르게 가져올 것인가?</h2>
<p>뉴스 피드는 사용자가 자주 조회하는 기능이다.</p>
<p>사용자가 피드를 열 때마다</p>
<pre><code class="language-text">친구 목록 조회
   ↓
친구 게시글 전부 조회
   ↓
정렬
   ↓
응답</code></pre>
<p>을 수행하면 많은 비용이 발생할 수 있다.</p>
<p>따라서 사용자별로 미리 생성한 피드를 캐시에 저장하는 방식 등을 생각할 수 있다.</p>
<pre><code class="language-text">User
 ↓
Feed Cache
 ↓
빠른 조회</code></pre>
<p>상세 설계에서는 이런 방식들의 장단점을 비교하면서 어떤 방법을 선택할지 설명한다.</p>
<hr>
<h1 id="12-상세-설계에서-중요한-것은-trade-off">12. 상세 설계에서 중요한 것은 Trade-off</h1>
<p>시스템 설계에는 대부분 완벽한 해결책이 없다.</p>
<p>어떤 선택을 하면 장점과 함께 단점도 발생한다.</p>
<p>예를 들어 사용자별 피드를 미리 만들어두면 조회는 빨라질 수 있다.</p>
<pre><code class="language-text">Read 성능 ↑</code></pre>
<p>하지만 사용자가 게시글을 작성할 때 많은 친구의 피드를 업데이트해야 한다.</p>
<pre><code class="language-text">Write 비용 ↑</code></pre>
<p>반대로 사용자가 피드를 열 때마다 게시글을 조합한다면 작성 비용은 감소하지만 조회 비용은 증가할 수 있다.</p>
<pre><code class="language-text">Write 비용 ↓
Read 비용 ↑</code></pre>
<p>따라서 시스템 설계 면접에서는</p>
<pre><code class="language-text">A가 무조건 좋습니다.</code></pre>
<p>보다</p>
<pre><code class="language-text">A는 조회 성능이 좋지만 쓰기 비용이 증가하고,
B는 쓰기 비용은 적지만 조회 시 비용이 발생합니다.

현재 서비스는 읽기 요청이 많기 때문에
A를 선택하겠습니다.</code></pre>
<p>처럼 설명하는 것이 더 중요하다.</p>
<hr>
<h1 id="13-세부사항에-너무-빨리-빠지지-않기">13. 세부사항에 너무 빨리 빠지지 않기</h1>
<p>시스템 설계 면접에서 흔히 할 수 있는 실수 중 하나가 초반부터 한 부분에 지나치게 깊게 들어가는 것이다.</p>
<p>예를 들어 뉴스 피드 문제에서 아직 전체 구조도 만들지 않았는데</p>
<pre><code class="language-text">게시글 Ranking 알고리즘</code></pre>
<p>부터 자세히 설명하기 시작하면 시간 배분이 무너질 수 있다.</p>
<p>따라서 다음 흐름을 유지하는 것이 좋다.</p>
<pre><code class="language-text">전체 요구사항
    ↓
전체 아키텍처
    ↓
중요한 컴포넌트 선정
    ↓
상세 설계</code></pre>
<p>즉 <strong>큰 구조에서 작은 구조로 내려가는 방식</strong>으로 접근한다.</p>
<hr>
<h1 id="14-마무리-단계">14. 마무리 단계</h1>
<p>상세 설계까지 끝났다고 바로 면접이 끝나는 것은 아니다.</p>
<p>마지막에는 전체 설계를 다시 검토한다.</p>
<hr>
<h2 id="1-최종-구조-요약">1. 최종 구조 요약</h2>
<p>긴 시간 동안 여러 이야기를 했기 때문에 마지막에 핵심 구조를 다시 한번 정리해주는 것이 좋다.</p>
<p>예를 들어</p>
<pre><code class="language-text">사용자의 게시글 작성 요청을
Posting Service에서 처리하고 DB에 저장합니다.

게시글 전달 작업은 메시지 큐를 이용해
비동기로 처리하고,

사용자별 뉴스 피드는 캐시에 저장하여
조회 성능을 높였습니다.</code></pre>
<p>처럼 전체 흐름을 짧게 설명할 수 있다.</p>
<hr>
<h1 id="15-병목-구간-확인">15. 병목 구간 확인</h1>
<p>설계가 끝났다면</p>
<blockquote>
<p><strong>이 시스템에서 가장 먼저 문제가 발생할 가능성이 있는 곳은 어디인가?</strong></p>
</blockquote>
<p>를 생각해본다.</p>
<p>예를 들어</p>
<ul>
<li>DB 쓰기 증가</li>
<li>캐시 용량 부족</li>
<li>특정 인기 사용자의 Fan-out 폭증</li>
<li>메시지 큐 적체</li>
<li>서버 장애</li>
</ul>
<p>등이 있을 수 있다.</p>
<p>여기서 중요한 것은</p>
<pre><code class="language-text">현재 설계는 완벽합니다.</code></pre>
<p>라고 말하는 것이 아니라</p>
<pre><code class="language-text">현재 구조에서 이 부분이 병목이 될 가능성이 있고,
트래픽이 증가하면 이런 방식으로 개선할 수 있습니다.</code></pre>
<p>라고 설명하는 것이다.</p>
<hr>
<h1 id="16-장애-상황도-고려하기">16. 장애 상황도 고려하기</h1>
<p>실제 서비스에서는 서버가 항상 정상적으로 동작하지 않는다.</p>
<p>따라서 다음과 같은 상황도 생각할 수 있다.</p>
<pre><code class="language-text">Web Server 장애
Database 장애
Cache 장애
Network 장애
Message Queue 장애</code></pre>
<p>예를 들어 웹 서버는 여러 대를 두고 로드밸런서를 통해 장애 서버를 제외할 수 있다.</p>
<p>DB는 Replica 구조를 사용할 수 있고, 캐시 역시 여러 노드로 구성할 수 있다.</p>
<p>모든 장애를 완벽하게 설명할 필요는 없지만 <strong>어떤 컴포넌트가 장애를 일으킬 수 있는지 인식하고 있는 것</strong>이 중요하다.</p>
<hr>
<h1 id="17-모니터링">17. 모니터링</h1>
<p>시스템을 운영하려면 장애가 발생했는지 확인할 수 있어야 한다.</p>
<p>따라서</p>
<ul>
<li>로그</li>
<li>메트릭</li>
<li>알림</li>
</ul>
<p>등도 함께 고려할 수 있다.</p>
<p>예를 들면</p>
<pre><code class="language-text">CPU 사용률
Memory 사용률
API 응답시간
Error Rate
Cache Hit Rate
DB Connection
Queue Length</code></pre>
<p>등을 관찰할 수 있다.</p>
<p>실제 환경에서는 Prometheus나 Grafana 같은 도구를 활용할 수도 있다.</p>
<hr>
<h1 id="18-앞으로-사용자가-더-증가한다면">18. 앞으로 사용자가 더 증가한다면?</h1>
<p>현재 설계가 DAU 100만 명을 처리할 수 있다고 하더라도 사용자가 계속 증가할 수 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">100만 명
  ↓
1,000만 명
  ↓
1억 명</code></pre>
<p>으로 증가할 수 있다.</p>
<p>그렇다면</p>
<ul>
<li>서버 Scale-out</li>
<li>DB Read Replica 확대</li>
<li>DB Sharding</li>
<li>Cache 확장</li>
<li>Message Queue</li>
<li>CDN</li>
<li>다중 데이터 센터</li>
</ul>
<p>등을 추가로 고려할 수 있다.</p>
<p>즉 시스템 설계에서는</p>
<blockquote>
<p><strong>현재 요구사항을 만족하면서 미래에 확장할 수 있는 구조인가</strong></p>
</blockquote>
<p>도 함께 생각해야 한다.</p>
<hr>
<h1 id="19-시스템-설계-면접에서-해야-할-것">19. 시스템 설계 면접에서 해야 할 것</h1>
<h2 id="요구사항부터-확인하기">요구사항부터 확인하기</h2>
<p>문제를 받은 즉시 설계하지 않고 필요한 기능과 제약조건을 먼저 확인한다.</p>
<hr>
<h2 id="정답-하나에-집착하지-않기">정답 하나에 집착하지 않기</h2>
<p>같은 문제라도 상황에 따라 설계가 달라질 수 있다.</p>
<p>따라서 여러 방법의 장단점을 비교한다.</p>
<pre><code class="language-text">방법 A
vs
방법 B</code></pre>
<p>그리고 현재 상황에서 어떤 방법이 더 적합한지 근거를 설명한다.</p>
<hr>
<h2 id="계속-설명하면서-진행하기">계속 설명하면서 진행하기</h2>
<p>혼자 생각한 뒤 완성된 답을 보여주는 것이 아니라 내가 어떤 이유로 이런 결정을 했는지 계속 설명한다.</p>
<pre><code class="language-text">&quot;읽기 요청이 많기 때문에 캐시를 추가하겠습니다.&quot;

&quot;여기서는 비동기 처리가 가능하기 때문에
메시지 큐를 사용할 수 있습니다.&quot;</code></pre>
<p>처럼 설계 근거를 공유하는 것이 중요하다.</p>
<hr>
<h2 id="중요한-영역에-집중하기">중요한 영역에 집중하기</h2>
<p>전체 시스템을 동일한 깊이로 설명하기보다 핵심 영역을 선정한다.</p>
<pre><code class="language-text">전체 구조
   ↓
핵심 문제
   ↓
상세 설계</code></pre>
<hr>
<h1 id="20-시스템-설계-면접에서-피해야-할-것">20. 시스템 설계 면접에서 피해야 할 것</h1>
<h2 id="요구사항을-확인하지-않고-바로-설계하기">요구사항을 확인하지 않고 바로 설계하기</h2>
<pre><code class="language-text">문제
 ↓
바로 DB / Redis / Kafka</code></pre>
<p>같은 방식으로 시작하지 않는다.</p>
<p>먼저 왜 이러한 기술이 필요한지 확인해야 한다.</p>
<hr>
<h2 id="처음부터-세부-구현에-빠지기">처음부터 세부 구현에 빠지기</h2>
<p>전체 구조를 잡기 전에 알고리즘이나 DB 컬럼 하나에 과도하게 집중하지 않는다.</p>
<hr>
<h2 id="아무-설명-없이-혼자-설계하기">아무 설명 없이 혼자 설계하기</h2>
<p>시스템 설계 면접은 결과만 보는 시험이 아니다.</p>
<p>따라서 설계 과정을 면접관과 공유해야 한다.</p>
<hr>
<h2 id="하나의-방법만-정답이라고-주장하기">하나의 방법만 정답이라고 주장하기</h2>
<p>대부분의 설계 선택에는 Trade-off가 존재한다.</p>
<p>따라서 다른 방법도 인정하고 현재 상황에서 자신의 선택이 적절한 이유를 설명한다.</p>
<hr>
<h2 id="설계가-끝났다고-바로-마무리하기">설계가 끝났다고 바로 마무리하기</h2>
<p>큰 구조를 완성한 뒤에도</p>
<ul>
<li>병목</li>
<li>장애</li>
<li>확장</li>
<li>모니터링</li>
</ul>
<p>등을 점검할 수 있다.</p>
<hr>
<h1 id="21-면접-시간-배분">21. 면접 시간 배분</h1>
<p>45분 정도의 시스템 설계 면접이라고 가정하면 대략 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-text">1. 요구사항 및 범위 정의
약 3 ~ 10분

2. 개략적인 설계
약 10 ~ 15분

3. 상세 설계
약 10 ~ 25분

4. 정리
약 3 ~ 5분</code></pre>
<p>정확하게 시간을 맞출 필요는 없지만 중요한 것은</p>
<blockquote>
<p><strong>초반에 너무 많은 시간을 사용하거나 특정 컴포넌트 하나에 모든 시간을 소비하지 않는 것</strong></p>
</blockquote>
<p>이다.</p>
<hr>
<h1 id="전체-흐름-정리">전체 흐름 정리</h1>
<p>이번 장의 시스템 설계 면접 과정을 하나로 정리하면 다음과 같다.</p>
<pre><code class="language-text">문제를 받는다
   ↓
바로 설계하지 않는다
   ↓
질문을 통해 요구사항 확인
   ↓
기능 / 규모 / 제약조건 결정
   ↓
필요하다면 규모 추정
   ↓
전체 아키텍처 작성
   ↓
면접관과 방향 확인
   ↓
핵심 문제 선정
   ↓
상세 설계
   ↓
Trade-off 설명
   ↓
병목 / 장애 / 확장성 확인
   ↓
전체 설계 요약</code></pre>
<p>결국 가장 중요한 것은 좋은 기술을 많이 알고 있다는 것을 보여주는 것보다</p>
<blockquote>
<p><strong>주어진 요구사항에서 왜 이 구조를 선택했는지를 논리적으로 설명하는 것</strong></p>
</blockquote>
<p>이라고 이해했다.</p>
<hr>
<h1 id="공부하면서-정리한-핵심">공부하면서 정리한 핵심</h1>
<p>처음에는 시스템 설계 면접이라고 하면</p>
<pre><code class="language-text">문제 제시
   ↓
아키텍처 그림
   ↓
정답</code></pre>
<p>정도로 생각했다.</p>
<p>하지만 이번 장을 보고 나니 실제로는 조금 다르게 느껴졌다.</p>
<pre><code class="language-text">질문
 ↓
가정
 ↓
규모 추정
 ↓
개략 설계
 ↓
상세 설계
 ↓
Trade-off
 ↓
개선 방향</code></pre>
<p>처럼 <strong>설계 결과보다는 설계를 만들어가는 과정 자체가 중요하다.</strong></p>
<p>특히 가장 인상 깊었던 것은 문제를 받자마자 설계를 시작하지 않는다는 점이었다.</p>
<p>실제 개발에서도 요구사항을 제대로 이해하지 않은 상태에서 개발을 시작하면 필요하지 않은 기능을 만들거나, 실제 규모와 맞지 않는 구조를 만들 가능성이 높다.</p>
<p>따라서 시스템 설계 면접에서 질문을 많이 하는 것은 답을 모르기 때문이 아니라</p>
<blockquote>
<p><strong>정확한 문제를 정의하기 위한 엔지니어링 과정</strong></p>
</blockquote>
<p>이라고 이해하게 되었다.</p>
<p>그리고 아직 실제 시스템 설계 면접을 경험해본 적은 없어서 이번 장의 내용이 완전히 와닿지는 않았다.</p>
<p>다만 앞으로 URL 단축기, 뉴스 피드, 채팅 시스템 등의 문제를 직접 풀어보면서</p>
<pre><code class="language-text">요구사항 정의
→ 개략 설계
→ 상세 설계
→ Trade-off</code></pre>
<p>의 흐름을 반복해보면 이번 장에서 설명한 방식이 더 확실하게 이해될 것 같다.</p>
<p>이번 장은 새로운 시스템 기술을 배우는 장이라기보다는, <strong>앞으로 시스템 설계 문제를 어떤 순서로 풀어야 하는지를 알려주는 가이드에 가까웠다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 2장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-2%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-2%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 08 Jul 2026 07:26:59 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이전 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-1%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 1장 정리</a>
다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-3%EC%9E%A5-%EC%A0%95%EB%A6%AC-11xsjbs6">[ 대규모 시스템 설계 스터디 ] 3장 정리</a></p>
</blockquote>
<h1 id="2장-개략적인-규모-추정">2장. 개략적인 규모 추정</h1>
<p>시스템을 설계할 때는 단순히 구조만 그리는 것이 아니라,</p>
<blockquote>
<p><strong>이 구조가 실제 사용자 수와 트래픽을 감당할 수 있는가?</strong></p>
</blockquote>
<p>를 수치로 확인할 필요가 있다.</p>
<p>이를 위해 사용자 수, 요청량, 데이터 크기 등의 정보를 바탕으로 <strong>QPS, 저장 공간, 네트워크 사용량, 필요한 서버 수 등을 대략적으로 계산하는 과정</strong>이 개략적인 규모 추정이다.</p>
<p>중요한 것은 완벽하게 정확한 값을 계산하는 것이 아니라, 주어진 조건을 바탕으로 시스템 규모를 합리적으로 예측하는 것이다.</p>
<hr>
<h2 id="규모-추정을-위해-알아야-할-기본-수치">규모 추정을 위해 알아야 할 기본 수치</h2>
<p>개략적인 규모 추정을 하기 위해서는 기본적으로 다음과 같은 개념에 익숙해야 한다.</p>
<ul>
<li>데이터 크기와 2의 제곱수</li>
<li>메모리, 디스크, 네트워크 등의 응답 지연 시간</li>
<li>시스템 가용성</li>
<li>QPS와 같은 처리량 지표</li>
</ul>
<p>이러한 기본 수치를 알고 있으면</p>
<pre><code class="language-text">사용자 수
   ↓
하루 요청량
   ↓
평균 / 최대 QPS
   ↓
필요 서버 수</code></pre>
<p>와 같은 방식으로 시스템 규모를 추정할 수 있다.</p>
<hr>
<h1 id="1-데이터-크기와-2의-제곱수">1. 데이터 크기와 2의 제곱수</h1>
<p>컴퓨터에서 데이터 크기를 계산할 때는 바이트 단위를 기본으로 사용한다.</p>
<pre><code class="language-text">1 Byte = 8 bit</code></pre>
<p>예를 들어 일반적인 ASCII 문자 하나는 1Byte 정도의 공간을 사용한다.</p>
<p>데이터 규모가 커지면 다음 단위를 사용하게 된다.</p>
<pre><code class="language-text">KB
MB
GB
TB
PB</code></pre>
<p>분산 시스템에서는 하루에도 TB 이상의 데이터가 생성될 수 있기 때문에 이러한 단위 변환에 익숙해야 한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">하루 데이터 생성량
×
보관 기간
=
필요 저장 공간</code></pre>
<p>과 같이 계산한다.</p>
<p>규모 추정에서는 반드시 완벽한 이진 단위 계산만 고집하기보다는 계산을 빠르게 하기 위해 적절한 근삿값을 사용하는 경우도 많다.</p>
<hr>
<h1 id="2-응답-지연-시간-latency">2. 응답 지연 시간 Latency</h1>
<p>시스템 내부의 모든 연산 속도가 동일한 것은 아니다.</p>
<p>CPU 내부의 캐시 접근과 메모리 접근, 디스크 접근, 네트워크 통신은 각각 속도 차이가 매우 크다.</p>
<p>전체적으로 보면 다음과 같은 느낌으로 이해할 수 있다.</p>
<pre><code class="language-text">CPU Cache
   ↓
Memory
   ↓
SSD
   ↓
Disk
   ↓
Network</code></pre>
<p>아래쪽으로 갈수록 일반적으로 더 많은 시간이 필요하다.</p>
<hr>
<h2 id="응답-지연-수치에서-볼-수-있는-점">응답 지연 수치에서 볼 수 있는 점</h2>
<h3 id="1-메모리는-디스크보다-빠르다">1. 메모리는 디스크보다 빠르다</h3>
<p>자주 사용하는 데이터를 메모리에 저장하는 캐시가 중요한 이유이기도 하다.</p>
<pre><code class="language-text">DB / Disk 조회
     ↓
Cache 활용
     ↓
응답 속도 개선</code></pre>
<hr>
<h3 id="2-디스크-탐색-비용은-크다">2. 디스크 탐색 비용은 크다</h3>
<p>디스크에서 원하는 위치를 찾아 이동하는 Seek 과정에는 비교적 큰 시간이 필요하다.</p>
<p>따라서 가능한 경우 불필요한 디스크 접근을 줄이는 것이 중요하다.</p>
<hr>
<h3 id="3-단순한-데이터-압축은-비교적-빠르다">3. 단순한 데이터 압축은 비교적 빠르다</h3>
<p>데이터 크기가 크다면 네트워크로 그대로 전달하는 것보다 압축한 뒤 전송하는 것이 전체 처리 시간을 줄이는 데 도움이 될 수 있다.</p>
<pre><code class="language-text">원본 데이터
   ↓
압축
   ↓
Network 전송</code></pre>
<hr>
<h3 id="4-데이터-센터-사이의-통신에도-시간이-필요하다">4. 데이터 센터 사이의 통신에도 시간이 필요하다</h3>
<p>여러 데이터 센터를 운영한다고 해서 데이터가 즉시 동기화되는 것은 아니다.</p>
<p>예를 들어</p>
<pre><code class="language-text">서울 Data Center
        ↕
미국 Data Center</code></pre>
<p>처럼 물리적으로 멀리 떨어져 있다면 네트워크 지연이 발생한다.</p>
<p>따라서 다중 리전이나 다중 데이터 센터 구조에서는 데이터 복제와 통신 비용도 함께 고려해야 한다.</p>
<hr>
<h1 id="3-가용성-availability">3. 가용성 Availability</h1>
<p>서비스가 얼마나 안정적으로 운영되는지를 나타내는 대표적인 지표가 <strong>가용성</strong>이다.</p>
<p>가용성은 전체 시간 중 시스템을 정상적으로 사용할 수 있었던 시간의 비율을 의미한다.</p>
<pre><code class="language-text">가용성
=
정상 서비스 시간 / 전체 시간</code></pre>
<p>일반적으로 퍼센트로 표현한다.</p>
<p>예를 들어</p>
<pre><code class="language-text">99%
99.9%
99.99%
99.999%</code></pre>
<p>등과 같이 표현할 수 있다.</p>
<p>숫자상으로는 작은 차이처럼 보이지만, 실제 허용 가능한 장애 시간은 크게 달라진다.</p>
<hr>
<h2 id="고가용성-high-availability">고가용성 High Availability</h2>
<p>장시간 서비스 중단 없이 지속적으로 시스템을 제공할 수 있는 특성을 의미한다.</p>
<p>서비스 규모가 커질수록 단순히 성능뿐만 아니라</p>
<blockquote>
<p><strong>장애가 발생했을 때 서비스를 얼마나 계속 유지할 수 있는가</strong></p>
</blockquote>
<p>도 중요한 설계 요소가 된다.</p>
<p>그래서 로드밸런서, 서버 다중화, DB Replica, 다중 데이터 센터 등의 구조가 필요해진다.</p>
<hr>
<h1 id="4-sla">4. SLA</h1>
<p>SLA는 <strong>Service Level Agreement</strong>의 약자다.</p>
<p>서비스 제공자와 고객 사이에서 서비스 수준을 어느 정도까지 보장할 것인지 정하는 약속이라고 이해할 수 있다.</p>
<p>대표적인 항목으로 가용성이 있다.</p>
<p>예를 들어</p>
<pre><code class="language-text">Availability ≥ 99.9%</code></pre>
<p>와 같은 형태로 일정 수준 이상의 서비스 운영을 보장할 수 있다.</p>
<p>가용성에서는 흔히 <strong>9가 몇 개 있는가</strong>로 안정성을 표현하기도 한다.</p>
<pre><code class="language-text">99.9%
99.99%
99.999%</code></pre>
<p>9가 늘어날수록 서비스 중단을 허용할 수 있는 시간이 크게 줄어든다.</p>
<hr>
<h1 id="5-규모-추정에서-중요한-qps">5. 규모 추정에서 중요한 QPS</h1>
<p>이번 장에서 처음 접한 개념 중 가장 중요했던 것이 QPS였다.</p>
<h2 id="qps-queries-per-second">QPS Queries Per Second</h2>
<p>초당 시스템이 처리하는 쿼리 또는 요청의 수를 의미한다.</p>
<p>예를 들어 서버에 하루 동안 8,640만 건의 요청이 들어온다면</p>
<pre><code class="language-text">86,400,000
÷
86,400초
=
1,000 QPS</code></pre>
<p>가 된다.</p>
<p>즉 평균적으로 초당 1,000개의 요청을 처리하는 시스템이다.</p>
<hr>
<h2 id="평균-qps">평균 QPS</h2>
<p>일반적인 계산은 다음과 같다.</p>
<pre><code class="language-text">하루 전체 요청 수
÷
86,400초
=
평균 QPS</code></pre>
<p>여기서</p>
<pre><code class="language-text">24 × 60 × 60 = 86,400초</code></pre>
<p>이다.</p>
<hr>
<h1 id="6-peak-qps">6. Peak QPS</h1>
<p>실제 서비스에서 요청이 하루 종일 일정하게 들어오는 경우는 거의 없다.</p>
<p>예를 들어</p>
<ul>
<li>출근 시간</li>
<li>점심시간</li>
<li>퇴근 시간</li>
<li>이벤트 시작 시간</li>
<li>수강신청 시간</li>
</ul>
<p>등 특정 시간에 요청이 몰릴 수 있다.</p>
<p>따라서 평균 QPS만 보고 서버를 설계하면 실제 트래픽이 몰렸을 때 시스템이 버티지 못할 수 있다.</p>
<p>이를 고려하기 위해 <strong>Peak QPS</strong>를 계산한다.</p>
<pre><code class="language-text">Peak QPS
=
평균 QPS × 여유 계수</code></pre>
<p>예를 들어 평균 QPS가 3,500이고 최대 트래픽을 평균의 2배로 가정하면</p>
<pre><code class="language-text">Peak QPS
=
3,500 × 2
=
7,000</code></pre>
<p>으로 볼 수 있다.</p>
<p>다만 <strong>Peak QPS를 반드시 평균 QPS의 2배로 잡아야 한다는 절대적인 규칙은 없다.</strong></p>
<p>서비스 특성과 트래픽 패턴에 따라 2배, 3배 또는 그 이상으로 설정할 수 있다.</p>
<hr>
<h1 id="7-qps와-rps-tps-차이">7. QPS와 RPS, TPS 차이</h1>
<p>비슷하게 사용되는 용어가 몇 가지 있다.</p>
<h2 id="qps">QPS</h2>
<p><strong>Queries Per Second</strong></p>
<p>초당 처리되는 질의 수</p>
<p>DB나 검색 시스템 등의 처리량을 표현할 때 많이 사용한다.</p>
<hr>
<h2 id="rps">RPS</h2>
<p><strong>Requests Per Second</strong></p>
<p>초당 처리되는 요청 수</p>
<p>특히 HTTP 서버 트래픽을 표현할 때 자주 사용한다.</p>
<pre><code class="language-text">Client
 ↓ HTTP Request
Server</code></pre>
<p>웹 서비스에서는 QPS와 RPS가 비슷한 의미로 사용되는 경우도 있다.</p>
<hr>
<h2 id="tps">TPS</h2>
<p><strong>Transactions Per Second</strong></p>
<p>초당 완료되는 트랜잭션 수</p>
<p>하나의 트랜잭션에는 여러 개의 쿼리가 포함될 수도 있다.</p>
<p>예를 들어 결제 한 번에서</p>
<pre><code class="language-text">재고 확인
   ↓
주문 생성
   ↓
결제 정보 저장
   ↓
재고 감소</code></pre>
<p>와 같이 여러 DB 작업이 묶여 하나의 트랜잭션이 될 수 있다.</p>
<hr>
<h1 id="8-규모-추정-예제">8. 규모 추정 예제</h1>
<p>간단한 SNS 서비스가 있다고 가정해보자.</p>
<h2 id="조건">조건</h2>
<ul>
<li>월간 활성 사용자: 3억 명</li>
<li>그중 50%가 매일 사용</li>
<li>사용자 한 명이 하루 평균 게시글 2개 작성</li>
<li>게시글 중 10%에 미디어 포함</li>
<li>데이터를 5년간 보관</li>
</ul>
<p>이 조건을 이용하면 처리량과 저장 공간을 추정할 수 있다.</p>
<hr>
<h2 id="1단계-dau-계산">1단계. DAU 계산</h2>
<p>월간 사용자 3억 명 중 절반이 매일 서비스를 사용한다고 가정한다.</p>
<pre><code class="language-text">3억 × 50%
=
1.5억</code></pre>
<p>따라서</p>
<pre><code class="language-text">DAU = 약 1.5억 명</code></pre>
<p>이다.</p>
<hr>
<h2 id="2단계-하루-게시글-수-계산">2단계. 하루 게시글 수 계산</h2>
<p>사용자 한 명이 하루 평균 2개의 게시글을 작성한다.</p>
<pre><code class="language-text">1.5억 명
×
2개
=
3억 개 / 일</code></pre>
<hr>
<h2 id="3단계-qps-계산">3단계. QPS 계산</h2>
<p>하루 동안 약 3억 개의 게시 요청이 발생한다고 가정한다.</p>
<pre><code class="language-text">3억
÷
86,400초
≈
3,500 QPS</code></pre>
<p>따라서 평균 QPS는 약</p>
<pre><code class="language-text">3,500 QPS</code></pre>
<p>정도로 추정할 수 있다.</p>
<hr>
<h2 id="4단계-peak-qps-계산">4단계. Peak QPS 계산</h2>
<p>최대 트래픽을 평균의 2배로 가정하면</p>
<pre><code class="language-text">3,500 × 2
=
7,000 QPS</code></pre>
<p>따라서 시스템은 최소한 약 7,000 QPS 수준의 순간 트래픽을 처리할 수 있도록 고려해볼 수 있다.</p>
<hr>
<h1 id="9-저장-공간-계산">9. 저장 공간 계산</h1>
<p>이번에는 미디어 데이터가 얼마나 저장되는지 계산한다.</p>
<p>미디어가 포함된 게시글의 비율을 10%라고 가정했다.</p>
<pre><code class="language-text">3억 게시글
×
10%
=
3천만 개</code></pre>
<p>하루 약 3천만 개의 미디어 파일이 업로드된다.</p>
<p>미디어 하나의 평균 크기를 1MB라고 가정하면</p>
<pre><code class="language-text">3천만
×
1MB
=
약 30TB / 일</code></pre>
<p>정도의 데이터가 생성된다.</p>
<hr>
<h2 id="5년간-저장한다면">5년간 저장한다면?</h2>
<p>하루 약 30TB가 발생한다고 가정하면</p>
<pre><code class="language-text">30TB
×
365일
×
5년
≈
55PB</code></pre>
<p>정도의 저장 공간이 필요하다.</p>
<p>정확한 값을 계산하는 것보다</p>
<blockquote>
<p><strong>이 서비스에서는 수십 PB 수준의 저장 공간이 필요하겠구나</strong></p>
</blockquote>
<p>라는 전체 규모를 파악하는 것이 중요하다.</p>
<hr>
<h1 id="10-규모-추정에서-중요한-사고방식">10. 규모 추정에서 중요한 사고방식</h1>
<p>시스템 설계 면접에서 규모 추정을 요구하는 이유는 단순한 산수 실력을 확인하기 위한 것이 아니다.</p>
<p>오히려 중요한 것은</p>
<pre><code class="language-text">문제 조건 파악
     ↓
필요한 가정 설정
     ↓
계산식 구성
     ↓
결과 도출
     ↓
설계에 반영</code></pre>
<p>과 같은 사고 과정이다.</p>
<hr>
<h2 id="1-복잡한-숫자는-근사한다">1. 복잡한 숫자는 근사한다</h2>
<p>예를 들어</p>
<pre><code class="language-text">99,487</code></pre>
<p>이라는 숫자가 있다면 필요에 따라</p>
<pre><code class="language-text">약 100,000</code></pre>
<p>으로 계산할 수 있다.</p>
<p>규모 추정에서는 정확한 소수점보다 전체 규모를 빠르게 판단하는 것이 중요하기 때문이다.</p>
<hr>
<h2 id="2-가정을-먼저-작성한다">2. 가정을 먼저 작성한다</h2>
<p>계산을 시작하기 전에 어떤 조건을 사용했는지 명확하게 적는다.</p>
<p>예를 들어</p>
<pre><code class="language-text">MAU = 3억
DAU = MAU의 50%
1인당 게시글 = 2개 / 일
미디어 포함 비율 = 10%</code></pre>
<p>처럼 정리한다.</p>
<p>이렇게 하면 계산이 틀려도 어디에서 문제가 발생했는지 확인하기 쉽다.</p>
<p>또한 시스템 설계 면접에서는 면접관과 가정 자체를 먼저 맞출 수 있다.</p>
<hr>
<h2 id="3-반드시-단위를-작성한다">3. 반드시 단위를 작성한다</h2>
<p>예를 들어 단순히</p>
<pre><code class="language-text">500</code></pre>
<p>이라고 적는 것이 아니라</p>
<pre><code class="language-text">500 QPS
500 MB
500 GB
500 Mbps</code></pre>
<p>처럼 단위를 함께 작성해야 한다.</p>
<p>규모 추정에서는 계산 자체보다 단위 실수로 결과가 크게 잘못되는 경우가 있기 때문이다.</p>
<hr>
<h1 id="11-규모-추정에서-자주-계산하는-것">11. 규모 추정에서 자주 계산하는 것</h1>
<p>시스템 설계 문제에서는 주로 다음과 같은 값을 구하게 된다.</p>
<h3 id="요청-처리량">요청 처리량</h3>
<pre><code class="language-text">평균 QPS
Peak QPS
RPS
TPS</code></pre>
<h3 id="데이터">데이터</h3>
<pre><code class="language-text">하루 데이터 생성량
전체 저장 공간</code></pre>
<h3 id="캐시">캐시</h3>
<pre><code class="language-text">필요 Cache Size</code></pre>
<h3 id="서버">서버</h3>
<pre><code class="language-text">필요 서버 수</code></pre>
<p>예를 들어 단일 서버가 최대</p>
<pre><code class="language-text">1,000 RPS</code></pre>
<p>를 처리할 수 있고 Peak RPS가</p>
<pre><code class="language-text">10,000 RPS</code></pre>
<p>라면 단순 계산상</p>
<pre><code class="language-text">10,000 / 1,000
=
10대</code></pre>
<p>정도의 서버가 필요하다는 식으로 접근할 수 있다.</p>
<p>물론 실제 시스템에서는 장애 대비와 여유 용량까지 추가로 고려해야 한다.</p>
<hr>
<h1 id="전체-흐름-정리">전체 흐름 정리</h1>
<p>이번 장의 내용을 하나의 과정으로 정리하면 다음과 같다.</p>
<pre><code class="language-text">사용자가 얼마나 있는가?
        ↓
하루에 얼마나 서비스를 사용하는가?
        ↓
요청이 하루에 몇 건 발생하는가?
        ↓
초당 요청은 몇 개인가?
        ↓
Peak 상황에서는 얼마나 증가하는가?
        ↓
데이터는 하루에 얼마나 만들어지는가?
        ↓
몇 년 동안 저장해야 하는가?
        ↓
어느 정도 규모의 시스템이 필요한가?</code></pre>
<p>결국 개략적인 규모 추정은</p>
<blockquote>
<p><strong>사용자의 행동을 숫자로 바꾼 뒤, 그 숫자를 시스템의 처리량과 저장 공간 요구사항으로 변환하는 과정</strong></p>
</blockquote>
<p>이라고 이해할 수 있다.</p>
<hr>
<h1 id="공부하면서-정리한-핵심">공부하면서 정리한 핵심</h1>
<p>1장에서 로드밸런서, 캐시, DB 다중화, 샤딩처럼 시스템을 <strong>어떻게 확장할 것인지</strong>를 배웠다면,</p>
<p>이번 장에서는 그보다 먼저</p>
<blockquote>
<p><strong>도대체 얼마나 확장해야 하는가?</strong></p>
</blockquote>
<p>를 판단하는 방법을 배웠다고 생각한다.</p>
<p>예를 들어 막연하게</p>
<pre><code class="language-text">&quot;사용자가 많으니까 서버를 여러 대 둬야 한다.&quot;</code></pre>
<p>라고 판단하는 것이 아니라,</p>
<pre><code class="language-text">DAU
→ 하루 요청량
→ 평균 QPS
→ Peak QPS
→ 단일 서버 처리량
→ 필요한 서버 수</code></pre>
<p>와 같이 숫자를 근거로 설계할 수 있다.</p>
<p>특히 이번 장에서 가장 중요하게 느낀 부분은 <strong>정확한 계산보다 합리적인 가정과 계산 과정</strong>이었다.</p>
<p>처음에는 QPS나 가용성 같은 용어가 익숙하지 않았지만, 실제 계산 자체는 크게 어렵지 않았다.</p>
<p>다만 시스템 설계 면접에서는 주어진 조건을 보고 빠르게 가정을 세우고 계산해야 하므로</p>
<ul>
<li>하루는 약 86,400초</li>
<li>데이터 크기 단위</li>
<li>평균 QPS 계산 방식</li>
<li>Peak 트래픽을 고려하는 방법</li>
<li>가용성의 의미</li>
</ul>
<p>정도는 바로 사용할 수 있도록 익숙해질 필요가 있을 것 같다.</p>
<p>이번 장은 새로운 아키텍처를 배우기보다는, <strong>앞으로 시스템을 설계할 때 필요한 숫자 감각을 만드는 장</strong>에 가까웠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[대규모 시스템 설계 스터디] 1장 정리]]></title>
            <link>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-1%EC%9E%A5-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-1%EC%9E%A5-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Wed, 08 Jul 2026 06:19:20 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>다음 포스트: <a href="https://velog.io/@jung_ji_in02/%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EC%8A%A4%ED%84%B0%EB%94%94-2%EC%9E%A5-%EC%A0%95%EB%A6%AC">[ 대규모 시스템 설계 스터디 ] 2장 정리</a></p>
</blockquote>
<h1 id="들어가기에-앞서서">들어가기에 앞서서</h1>
<p>이번 방학 기간동안 대규모 시스템 설계 스터디를 진행한다.</p>
<p>교재는 <a href="https://product.kyobobook.co.kr/detail/S000001033116">가상 면접 사례로 배우는 대규모 시스템 설계 기초</a> 를 사용했다.</p>
<p>기간은 7월 2일 ~ 9월 12일까지로 예상한다.</p>
<p>각 주마다 2번씩 진행되고, 스터디 과제는 벨로그 작성과 각 단원 문제풀이로 진행하며, 중간마다 시험문제를 출제하여 수준을 검토한다.</p>
<p>최종적으론 대규모 프로젝트를 진행하여 스터디를 마무리한다.</p>
<hr>
<h1 id="1장-사용자-증가에-따른-시스템-규모-확장">1장. 사용자 증가에 따른 시스템 규모 확장</h1>
<p>대규모 시스템이라고 해서 처음부터 복잡한 구조로 시작하는 것은 아니다.</p>
<p>처음에는 <strong>사용자 한 명을 처리할 수 있는 단순한 서버 구조</strong>에서 출발하고, 사용자가 증가하면서 발생하는 문제를 하나씩 해결하는 방식으로 시스템을 확장할 수 있다.</p>
<p>이번 장에서는 다음과 같은 흐름으로 시스템이 발전한다.</p>
<pre><code class="language-text">단일 서버
→ 웹 서버와 DB 분리
→ 로드밸런서
→ DB 다중화
→ 캐시
→ CDN
→ 무상태 웹 계층
→ 다중 데이터 센터
→ 메시지 큐
→ 모니터링 및 자동화
→ 데이터베이스 샤딩</code></pre>
<p>결국 핵심은 <strong>특정 서버 하나에 모든 역할과 부하가 집중되지 않도록 시스템을 계속 분리하고 분산하는 것</strong>이다.</p>
<hr>
<h2 id="1-가장-단순한-구조-단일-서버">1. 가장 단순한 구조: 단일 서버</h2>
<p>서비스 초기에는 하나의 서버에서 대부분의 기능을 처리할 수 있다.</p>
<pre><code class="language-text">사용자
 ↓
DNS
 ↓
웹 서버</code></pre>
<p>사용자가 <code>api.mysite.com</code>과 같은 주소로 서비스에 접근하면 다음 과정이 진행된다.</p>
<ol>
<li>사용자가 도메인 주소로 서비스 접속</li>
<li>DNS를 통해 도메인에 해당하는 IP 주소 조회</li>
<li>조회한 IP 주소로 HTTP 요청 전송</li>
<li>웹 서버가 요청을 처리</li>
<li>HTML 또는 JSON 등의 형태로 응답 반환</li>
</ol>
<p>서비스 규모가 작다면 이 정도 구조만으로도 충분하다.</p>
<p>하지만 사용자가 증가하기 시작하면 <strong>하나의 서버가 요청 처리와 데이터 관리까지 모두 담당하는 구조에는 한계가 발생한다.</strong></p>
<hr>
<h3 id="웹과-모바일의-서버-통신-방식">웹과 모바일의 서버 통신 방식</h3>
<p>웹과 모바일은 서버로부터 필요한 데이터의 형태가 조금 다를 수 있다.</p>
<h4 id="웹-브라우저">웹 브라우저</h4>
<pre><code class="language-text">브라우저
← HTML / CSS / JS / 데이터</code></pre>
<p>브라우저에서 화면을 구성하기 위해 HTML이나 JavaScript 등의 리소스를 전달받을 수 있다.</p>
<h4 id="모바일-앱">모바일 앱</h4>
<pre><code class="language-text">모바일 앱
← JSON 데이터</code></pre>
<p>모바일 앱은 애플리케이션 내부에 이미 화면을 구성하는 코드가 존재하기 때문에 서버에서는 필요한 데이터를 API 형태로 전달하는 경우가 많다.</p>
<p>현대적인 웹 애플리케이션 역시 React, Vue 등의 프론트엔드를 별도로 구성하고 서버에서는 JSON API만 제공하는 구조를 많이 사용한다.</p>
<p>즉 단순하게 이해하면 다음과 같다.</p>
<pre><code class="language-text">웹 서버 → 화면을 구성하는 리소스 제공 가능
API 서버 → 클라이언트가 사용할 데이터 제공</code></pre>
<hr>
<h1 id="2-웹-서버와-데이터베이스-분리">2. 웹 서버와 데이터베이스 분리</h1>
<p>사용자가 증가하면 하나의 서버에서 모든 기능을 처리하는 구조를 유지하기 어렵다.</p>
<p>따라서 역할을 분리한다.</p>
<pre><code class="language-text">사용자
 ↓
웹 서버
 ↓
데이터베이스</code></pre>
<ul>
<li><strong>웹 계층</strong>: 사용자 요청 및 비즈니스 로직 처리</li>
<li><strong>데이터 계층</strong>: 데이터 저장 및 조회</li>
</ul>
<p>이렇게 분리하면 웹 서버와 데이터베이스를 각각 독립적으로 확장할 수 있다.</p>
<p>예를 들어 요청 처리량이 부족하다면 웹 서버를 확장하고, 저장 공간이나 데이터 처리량이 부족하다면 데이터베이스를 확장할 수 있다.</p>
<hr>
<h2 id="관계형-db와-nosql">관계형 DB와 NoSQL</h2>
<p>데이터베이스는 크게 다음과 같이 구분할 수 있다.</p>
<h3 id="rdbms">RDBMS</h3>
<p>관계형 데이터베이스</p>
<p>대표적인 예시</p>
<ul>
<li>MySQL</li>
<li>PostgreSQL</li>
<li>Oracle</li>
</ul>
<p>테이블 사이의 관계를 기반으로 데이터를 관리하며 JOIN 등의 연산을 지원한다.</p>
<h3 id="nosql">NoSQL</h3>
<p>관계형 구조를 반드시 사용하지 않는 데이터베이스</p>
<p>대표적으로 다음과 같은 형태가 있다.</p>
<ul>
<li>Key-Value</li>
<li>Document</li>
<li>Column</li>
<li>Graph</li>
</ul>
<p>예시</p>
<ul>
<li>DynamoDB</li>
<li>CouchDB</li>
<li>HBase</li>
</ul>
<p>일반적인 서비스에서는 관계형 데이터베이스를 많이 사용하지만 다음과 같은 상황에서는 NoSQL이 적합할 수 있다.</p>
<ul>
<li>매우 낮은 응답 시간이 필요한 경우</li>
<li>데이터 구조가 일정하지 않은 경우</li>
<li>대규모 데이터를 저장해야 하는 경우</li>
<li>단순한 Key-Value 형태의 읽기·쓰기가 많은 경우</li>
</ul>
<hr>
<h3 id="직렬화와-역직렬화">직렬화와 역직렬화</h3>
<p>NoSQL을 공부하면서 함께 이해하면 좋은 개념이다.</p>
<h4 id="직렬화-serialization">직렬화 Serialization</h4>
<p>프로그램 내부의 객체를 저장하거나 전송할 수 있는 형태로 변환하는 과정</p>
<pre><code class="language-text">Java Object
   ↓
JSON / Byte</code></pre>
<p>예를 들어 Java 객체를 JSON 문자열로 변환하여 Redis나 네트워크를 통해 전달할 수 있다.</p>
<h4 id="역직렬화-deserialization">역직렬화 Deserialization</h4>
<p>저장하거나 전달받은 데이터를 다시 프로그램에서 사용할 수 있는 객체로 복원하는 과정</p>
<pre><code class="language-text">JSON / Byte
   ↓
Java Object</code></pre>
<p>Key-Value 저장소에서는 복잡한 JOIN을 수행하기보다 객체를 JSON 등의 형태로 저장해두고 필요할 때 다시 객체로 복원하여 사용하는 구조도 가능하다.</p>
<hr>
<h1 id="3-서버-규모를-확장하는-두-가지-방법">3. 서버 규모를 확장하는 두 가지 방법</h1>
<p>사용자가 계속 증가하면 서버 성능을 확장해야 한다.</p>
<p>방법은 크게 두 가지다.</p>
<h2 id="수직적-확장-scale-up">수직적 확장 Scale-Up</h2>
<p>기존 서버의 성능을 높이는 방법</p>
<pre><code class="language-text">CPU ↑
Memory ↑
Disk ↑</code></pre>
<p>장점</p>
<ul>
<li>구조가 단순함</li>
<li>기존 시스템의 변경이 비교적 적음</li>
</ul>
<p>하지만 한계도 명확하다.</p>
<ul>
<li>하드웨어 성능에 물리적인 한계 존재</li>
<li>고성능 장비일수록 비용이 크게 증가</li>
<li>서버 한 대가 장애를 일으키면 전체 서비스에 영향을 줄 가능성</li>
</ul>
<p>특히 서버 하나에 시스템 전체가 의존하는 구조를 <strong>SPOF(Single Point of Failure)</strong>라고 한다.</p>
<hr>
<h2 id="수평적-확장-scale-out">수평적 확장 Scale-Out</h2>
<p>서버 성능 자체를 높이는 대신 <strong>서버의 수를 늘리는 방법</strong></p>
<pre><code class="language-text">Server 1
Server 2
Server 3
...</code></pre>
<p>대규모 시스템에서는 일반적으로 수평 확장을 적극적으로 사용한다.</p>
<p>다만 서버가 여러 대가 되면 새로운 문제가 발생한다.</p>
<blockquote>
<p>사용자의 요청을 어떤 서버로 전달할 것인가?</p>
</blockquote>
<p>이 문제를 해결하기 위해 <strong>로드밸런서</strong>를 사용한다.</p>
<hr>
<h1 id="4-로드밸런서">4. 로드밸런서</h1>
<p>로드밸런서는 여러 웹 서버 앞에서 사용자 요청을 적절한 서버로 나누어 전달한다.</p>
<pre><code class="language-text">        사용자
          ↓
     Load Balancer
       ↙      ↘
 Web Server1  Web Server2</code></pre>
<p>사용자는 웹 서버에 직접 접속하지 않고 로드밸런서를 통해 서비스에 접근한다.</p>
<p>외부에서는 로드밸런서의 공개 주소를 사용하고, 로드밸런서와 웹 서버 사이에서는 내부 네트워크의 사설 IP를 사용할 수 있다.</p>
<p>예를 들어 클라우드 환경에서는 다음과 같은 구조가 가능하다.</p>
<pre><code class="language-text">Internet
   ↓
Load Balancer
   ↓
Private Network
 ↙          ↘
EC2 #1      EC2 #2</code></pre>
<h3 id="로드밸런서를-사용하는-이유">로드밸런서를 사용하는 이유</h3>
<h4 id="1-장애-대응">1. 장애 대응</h4>
<p>Server 1에 문제가 발생한다면</p>
<pre><code class="language-text">Server 1 ✕
Server 2 ○</code></pre>
<p>정상적으로 동작하는 Server 2로 요청을 전달할 수 있다.</p>
<h4 id="2-트래픽-증가-대응">2. 트래픽 증가 대응</h4>
<p>기존 서버가 트래픽을 처리하기 어려워지면 새로운 서버를 추가한다.</p>
<pre><code class="language-text">Server 1
Server 2
Server 3 ← 추가</code></pre>
<p>로드밸런서는 추가된 서버까지 포함하여 요청을 분산할 수 있다.</p>
<p>즉,</p>
<blockquote>
<p><strong>로드밸런서는 서버 수평 확장을 가능하게 만드는 핵심 구성 요소 중 하나다.</strong></p>
</blockquote>
<hr>
<h1 id="5-데이터베이스-다중화">5. 데이터베이스 다중화</h1>
<p>웹 서버를 여러 대 구성하더라도 데이터베이스 하나에 장애가 발생하면 서비스가 정상적으로 동작하기 어렵다.</p>
<p>따라서 데이터베이스도 여러 개로 구성할 수 있다.</p>
<p>대표적인 방식이 <strong>Primary-Replica 구조</strong>다.</p>
<pre><code class="language-text">             Primary DB
             /        \
       Replica 1    Replica 2</code></pre>
<h3 id="primary">Primary</h3>
<p>주로 데이터 변경 담당</p>
<ul>
<li>INSERT</li>
<li>UPDATE</li>
<li>DELETE</li>
</ul>
<h3 id="replica">Replica</h3>
<p>Primary의 데이터를 복제하여 보관하고 주로 조회 요청을 담당</p>
<ul>
<li>SELECT</li>
</ul>
<p>일반적으로 서비스에서는 데이터 변경보다 조회 요청이 훨씬 많을 수 있기 때문에 여러 Replica를 두어 읽기 부하를 분산할 수 있다.</p>
<hr>
<h2 id="db-다중화의-장점">DB 다중화의 장점</h2>
<h3 id="성능">성능</h3>
<p>조회 요청을 여러 Replica가 나누어 처리할 수 있다.</p>
<h3 id="안정성">안정성</h3>
<p>데이터를 여러 서버에 복제하기 때문에 서버 하나가 손상되어도 다른 서버에 데이터가 남아 있을 수 있다.</p>
<h3 id="가용성">가용성</h3>
<p>일부 서버에 장애가 발생하더라도 나머지 서버를 통해 서비스를 계속 제공할 수 있다.</p>
<hr>
<h2 id="db-장애가-발생한다면">DB 장애가 발생한다면</h2>
<h3 id="replica-한-대가-장애">Replica 한 대가 장애</h3>
<p>읽기 요청을 다른 DB가 처리하도록 전환한다.</p>
<h3 id="여러-replica-중-하나가-장애">여러 Replica 중 하나가 장애</h3>
<p>정상 Replica 서버가 읽기 요청을 대신 처리한다.</p>
<h3 id="primary가-장애">Primary가 장애</h3>
<p>Replica 중 하나를 새로운 Primary로 승격시키는 방식으로 복구할 수 있다.</p>
<p>실제 운영에서는 복제 지연, 장애 감지, 승격 과정 등의 추가적인 문제가 존재하지만 기본적인 개념은 다음과 같다.</p>
<blockquote>
<p><strong>데이터를 여러 서버에 복제하여 하나의 DB에 대한 의존도를 줄이는 것</strong></p>
</blockquote>
<hr>
<h1 id="6-캐시">6. 캐시</h1>
<p>데이터베이스 조회가 많아지면 DB 역시 부하가 발생한다.</p>
<p>하지만 모든 요청이 반드시 DB까지 갈 필요는 없다.</p>
<p>자주 사용하는 데이터를 메모리에 저장해두고 바로 사용하는 것이 <strong>캐시(Cache)</strong>다.</p>
<pre><code class="language-text">사용자 요청
    ↓
   Cache
   ↓ MISS
Database</code></pre>
<h3 id="cache-hit">Cache Hit</h3>
<p>필요한 데이터가 캐시에 존재</p>
<pre><code class="language-text">요청 → Cache → 응답</code></pre>
<h3 id="cache-miss">Cache Miss</h3>
<p>캐시에 데이터가 없음</p>
<pre><code class="language-text">요청
 ↓
Cache MISS
 ↓
Database
 ↓
Cache 저장
 ↓
응답</code></pre>
<p>이 구조를 통해 DB 조회 횟수를 줄이고 응답 속도를 높일 수 있다.</p>
<hr>
<h2 id="캐시-사용-시-고려할-점">캐시 사용 시 고려할 점</h2>
<h3 id="어떤-데이터를-캐싱할-것인가">어떤 데이터를 캐싱할 것인가</h3>
<p>변경은 적지만 조회가 자주 발생하는 데이터에 적합하다.</p>
<h3 id="데이터-만료-시간">데이터 만료 시간</h3>
<p>캐시는 일정 시간이 지나면 데이터를 제거하도록 TTL 등을 설정할 수 있다.</p>
<p>만료 시간이 지나치게 짧으면</p>
<pre><code class="language-text">Cache Miss 증가
→ DB 요청 증가</code></pre>
<p>반대로 너무 길면</p>
<pre><code class="language-text">Cache 데이터
≠
DB 데이터</code></pre>
<p>가 될 가능성이 높아진다.</p>
<h3 id="캐시-크기">캐시 크기</h3>
<p>메모리가 너무 작으면 필요한 데이터가 계속 제거되면서 캐시 효율이 떨어진다.</p>
<h3 id="캐시-방출-정책">캐시 방출 정책</h3>
<p>캐시 공간이 부족할 경우 어떤 데이터를 제거할지 결정해야 한다.</p>
<p>대표적으로</p>
<ul>
<li>LRU</li>
<li>LFU</li>
<li>FIFO</li>
</ul>
<p>등의 정책을 사용할 수 있다.</p>
<hr>
<h1 id="7-cdn">7. CDN</h1>
<p>캐시는 주로 애플리케이션의 데이터 조회 성능을 개선한다면, CDN은 <strong>정적 파일 전달 속도를 개선하는 역할</strong>을 담당한다.</p>
<p>CDN(Content Delivery Network)은 여러 지역에 분산된 서버에 정적 콘텐츠를 저장해두고 사용자와 가까운 서버에서 전달하는 방식이다.</p>
<p>대표적인 정적 콘텐츠</p>
<ul>
<li>이미지</li>
<li>영상</li>
<li>CSS</li>
<li>JavaScript</li>
</ul>
<hr>
<h2 id="cdn-동작-방식">CDN 동작 방식</h2>
<pre><code class="language-text">사용자
 ↓
가까운 CDN 서버
 ↓
Origin Server</code></pre>
<p>처음 요청한 파일이 CDN에 없다면</p>
<ol>
<li>사용자가 CDN에 이미지 요청</li>
<li>CDN에 해당 파일이 존재하지 않음</li>
<li>CDN이 Origin Server에서 파일 조회</li>
<li>CDN에 파일 저장</li>
<li>사용자에게 전달</li>
</ol>
<p>이후 같은 파일에 대한 요청이 들어오면</p>
<pre><code class="language-text">사용자
 ↓
CDN Cache
 ↓
바로 응답</code></pre>
<p>할 수 있다.</p>
<hr>
<h2 id="s3와-cdn의-차이">S3와 CDN의 차이</h2>
<p>처음에는 S3가 이미지 전달 속도를 직접 높여주는 역할이라고 생각했는데 둘의 역할은 다르다.</p>
<h3 id="s3">S3</h3>
<p>파일을 저장하는 공간</p>
<h3 id="cdn">CDN</h3>
<p>파일을 사용자 가까운 위치에서 빠르게 전달하는 역할</p>
<p>예를 들어</p>
<pre><code class="language-text">이미지 저장
Web Server → S3</code></pre>
<pre><code class="language-text">이미지 조회
사용자 → CDN → S3</code></pre>
<p>와 같은 구조를 사용할 수 있다.</p>
<p>정리하면</p>
<pre><code class="language-text">S3  = 저장소 분리
CDN = 콘텐츠 전달 최적화</code></pre>
<hr>
<h1 id="8-무상태-웹-계층">8. 무상태 웹 계층</h1>
<p>웹 서버를 여러 대 운영하려면 한 가지 중요한 조건이 있다.</p>
<blockquote>
<p><strong>특정 사용자 정보가 특정 웹 서버에 종속되지 않아야 한다.</strong></p>
</blockquote>
<hr>
<h2 id="상태-정보를-서버가-가지고-있는-경우">상태 정보를 서버가 가지고 있는 경우</h2>
<p>예를 들어 사용자 A의 로그인 세션이 Server 1에만 저장되어 있다고 가정한다.</p>
<pre><code class="language-text">User A
 ↓
Server 1 ← 세션 정보 존재</code></pre>
<p>이 경우 다음 요청 역시 Server 1으로 보내야 한다.</p>
<p>Server 2로 요청이 전달되면 사용자 정보를 알 수 없기 때문이다.</p>
<p>이를 해결하기 위해 Sticky Session 등을 사용할 수 있지만 서버 추가·삭제와 장애 대응이 복잡해질 수 있다.</p>
<hr>
<h2 id="stateless-구조">Stateless 구조</h2>
<p>사용자 상태를 웹 서버 외부의 공용 저장소에 보관한다.</p>
<pre><code class="language-text">            Shared Storage
                 ↑
       ┌─────────┼─────────┐
       ↓         ↓         ↓
    Server1   Server2   Server3</code></pre>
<p>어떤 서버가 요청을 받아도 필요한 정보를 동일한 저장소에서 조회할 수 있다.</p>
<p>따라서</p>
<pre><code class="language-text">User
 ↓
Load Balancer
 ↓
아무 Web Server</code></pre>
<p>형태가 가능하다.</p>
<hr>
<h2 id="장점">장점</h2>
<ul>
<li>서버 추가가 쉬움</li>
<li>서버 제거가 쉬움</li>
<li>특정 서버 장애에 대한 의존도 감소</li>
<li>자동 확장과 결합하기 쉬움</li>
</ul>
<p>예를 들어 트래픽이 증가하면</p>
<pre><code class="language-text">Server +1</code></pre>
<p>트래픽이 감소하면</p>
<pre><code class="language-text">Server -1</code></pre>
<p>하는 방식으로 자동 확장할 수 있다.</p>
<hr>
<h1 id="9-여러-데이터-센터-사용">9. 여러 데이터 센터 사용</h1>
<p>서비스가 더 커지면 하나의 지역이나 데이터 센터만 사용하는 것에도 한계가 생긴다.</p>
<p>따라서 여러 지역에 데이터 센터를 구축할 수 있다.</p>
<pre><code class="language-text">             User
              ↓
            DNS
          ↙     ↘
     Data Center A
     Data Center B</code></pre>
<p>사용자의 위치에 따라 가까운 데이터 센터로 연결하는 방식을 사용할 수 있다.</p>
<p>이러한 방식과 관련된 개념이 <strong>GeoDNS</strong>다.</p>
<p>GeoDNS는 사용자의 지역에 따라 서로 다른 IP 주소를 반환할 수 있도록 DNS를 구성하는 방식이다.</p>
<hr>
<h2 id="데이터-센터-장애">데이터 센터 장애</h2>
<p>예를 들어 Data Center A에 장애가 발생하면</p>
<pre><code class="language-text">Data Center A ✕
       ↓
Data Center B ○</code></pre>
<p>정상 데이터 센터로 트래픽을 우회할 수 있다.</p>
<p>하지만 다중 데이터 센터 구조를 구축하려면 다음 문제들을 고려해야 한다.</p>
<h3 id="트래픽-우회">트래픽 우회</h3>
<p>어떤 데이터 센터로 사용자를 전달할 것인가</p>
<h3 id="데이터-동기화">데이터 동기화</h3>
<p>여러 데이터 센터가 동일한 데이터를 어떻게 유지할 것인가</p>
<h3 id="테스트와-배포">테스트와 배포</h3>
<p>여러 지역에 있는 시스템을 어떻게 안정적으로 배포하고 검증할 것인가</p>
<p>결국 서비스 규모가 커질수록 각 시스템 구성 요소를 분리하고 독립적으로 확장하는 구조가 중요해진다.</p>
<hr>
<h1 id="10-메시지-큐">10. 메시지 큐</h1>
<p>서비스가 여러 개로 분리되면 서비스 간 통신도 증가한다.</p>
<p>모든 작업을 동기적으로 처리하면 한 서비스의 처리 시간이 다른 서비스의 응답 속도에도 영향을 줄 수 있다.</p>
<p>이때 사용할 수 있는 것이 <strong>메시지 큐(Message Queue)</strong>다.</p>
<pre><code class="language-text">Producer
   ↓
Message Queue
   ↓
Consumer</code></pre>
<h3 id="producer">Producer</h3>
<p>처리할 메시지를 생성해 큐에 전달</p>
<h3 id="consumer">Consumer</h3>
<p>큐에 저장된 메시지를 가져가 실제 작업 수행</p>
<hr>
<h2 id="메시지-큐를-사용하는-이유">메시지 큐를 사용하는 이유</h2>
<p>예를 들어 이미지 처리 작업이 있다고 가정한다.</p>
<p>메시지 큐가 없다면</p>
<pre><code class="language-text">사용자 요청
 ↓
이미지 처리 완료까지 대기
 ↓
응답</code></pre>
<p>하지만 메시지 큐를 사용하면</p>
<pre><code class="language-text">사용자 요청
 ↓
Queue에 작업 등록
 ↓
응답

        Queue
          ↓
      Worker 처리</code></pre>
<p>처럼 작업을 분리할 수 있다.</p>
<p>이를 통해 서비스 간 결합도를 낮출 수 있다.</p>
<p>생산자는 소비자의 작업 완료를 직접 기다릴 필요가 없고, 소비자 역시 자신의 처리 속도에 맞춰 메시지를 가져올 수 있다.</p>
<hr>
<h2 id="비동기-처리">비동기 처리</h2>
<p>비동기는 쉽게 말하면</p>
<blockquote>
<p><strong>작업이 끝날 때까지 현재 요청이 계속 기다리지 않는 구조</strong></p>
</blockquote>
<p>다.</p>
<p>처리 시간이 긴 작업을 별도로 분리할 때 유용하다.</p>
<hr>
<h1 id="11-로그-메트릭-자동화">11. 로그, 메트릭, 자동화</h1>
<p>시스템 규모가 커질수록 서버가 정상적으로 동작하고 있는지 직접 확인하기 어렵다.</p>
<p>따라서 시스템의 상태를 관찰할 수 있는 장치가 필요하다.</p>
<hr>
<h2 id="로그-log">로그 Log</h2>
<p>서비스에서 발생한 사건을 기록한 데이터</p>
<p>예시</p>
<pre><code class="language-text">사용자 로그인
API 호출
DB 오류
서버 예외</code></pre>
<p>로그를 중앙에서 수집하면 문제가 발생했을 때 원인을 추적하기 쉬워진다.</p>
<hr>
<h2 id="메트릭-metric">메트릭 Metric</h2>
<p>시스템 상태를 숫자로 측정한 값</p>
<p>예를 들어</p>
<h3 id="서버-메트릭">서버 메트릭</h3>
<ul>
<li>CPU 사용률</li>
<li>Memory 사용률</li>
<li>Disk I/O</li>
</ul>
<h3 id="시스템-메트릭">시스템 메트릭</h3>
<ul>
<li>DB 처리량</li>
<li>Cache Hit Rate</li>
<li>API 응답 시간</li>
</ul>
<h3 id="비즈니스-메트릭">비즈니스 메트릭</h3>
<ul>
<li>일일 활성 사용자</li>
<li>매출</li>
<li>재방문율</li>
</ul>
<p>등을 수집할 수 있다.</p>
<p>로그가</p>
<blockquote>
<p><strong>무슨 일이 발생했는가</strong></p>
</blockquote>
<p>를 확인하는 데 가깝다면,</p>
<p>메트릭은</p>
<blockquote>
<p><strong>현재 시스템 상태가 어떠한가</strong></p>
</blockquote>
<p>를 수치로 확인하는 데 가깝다고 이해했다.</p>
<hr>
<h2 id="자동화">자동화</h2>
<p>시스템 규모가 커지면 모든 작업을 사람이 직접 수행하기 어렵다.</p>
<p>따라서 반복적인 운영 작업을 자동화한다.</p>
<p>예를 들면</p>
<pre><code class="language-text">트래픽 증가
 ↓
서버 자동 추가

트래픽 감소
 ↓
서버 자동 제거</code></pre>
<p>와 같은 Auto Scaling도 자동화의 한 사례로 볼 수 있다.</p>
<hr>
<h1 id="12-데이터베이스-규모-확장">12. 데이터베이스 규모 확장</h1>
<p>서비스의 데이터가 계속 증가하면 결국 데이터베이스 역시 확장해야 한다.</p>
<p>데이터베이스도 서버와 동일하게 두 가지 방식으로 확장할 수 있다.</p>
<hr>
<h2 id="db-수직-확장">DB 수직 확장</h2>
<p>하나의 DB 서버 성능을 높이는 방식</p>
<pre><code class="language-text">CPU ↑
Memory ↑
Storage ↑</code></pre>
<p>구조는 비교적 단순하지만</p>
<ul>
<li>하드웨어 한계</li>
<li>높은 비용</li>
<li>단일 장애점 가능성</li>
</ul>
<p>등의 문제가 존재한다.</p>
<hr>
<h1 id="db-수평-확장-sharding">DB 수평 확장: Sharding</h1>
<p>데이터베이스 자체를 여러 서버로 나누는 방법을 <strong>샤딩(Sharding)</strong>이라고 한다.</p>
<pre><code class="language-text">Database
   ↓
┌───────┬───────┬───────┐
Shard 1 Shard 2 Shard 3</code></pre>
<p>각 샤드는 전체 데이터 중 일부만 저장한다.</p>
<p>예를 들어 <code>user_id</code>를 기준으로 나눈다면</p>
<pre><code class="language-text">user_id 1 → Shard A
user_id 2 → Shard B
user_id 3 → Shard C</code></pre>
<p>와 같이 데이터를 분산할 수 있다.</p>
<p>이때 데이터를 어떤 샤드에 저장할지 결정하는 값을 <strong>Shard Key 또는 Partition Key</strong>라고 한다.</p>
<hr>
<h2 id="샤딩에서-중요한-문제">샤딩에서 중요한 문제</h2>
<h3 id="1-resharding">1. Resharding</h3>
<p>데이터가 증가하면서 특정 샤드의 저장 공간이 부족해질 수 있다.</p>
<p>또는 데이터가 고르게 나누어지지 않아 특정 샤드에 데이터가 집중될 수도 있다.</p>
<p>이 경우 데이터 분배 방식을 변경하고 기존 데이터를 다시 배치해야 한다.</p>
<p>이와 관련하여 <strong>Consistent Hashing</strong>과 같은 기법을 사용할 수 있다.</p>
<hr>
<h3 id="2-hotspot-key">2. Hotspot Key</h3>
<p>특정 데이터에 트래픽이 지나치게 집중되는 문제다.</p>
<p>예를 들어 매우 유명한 사용자의 데이터가 하나의 샤드에 저장되어 있다면</p>
<pre><code class="language-text">Shard A ← 요청 폭증
Shard B ← 정상
Shard C ← 정상</code></pre>
<p>처럼 특정 서버만 과부하가 발생할 수 있다.</p>
<p>이러한 데이터는 더 세분화하거나 별도의 처리 전략을 고려해야 한다.</p>
<hr>
<h3 id="3-join-문제">3. JOIN 문제</h3>
<p>샤딩을 적용하면 서로 다른 데이터가 물리적으로 다른 서버에 존재할 수 있다.</p>
<pre><code class="language-text">User → Shard A
Order → Shard B</code></pre>
<p>이 경우 여러 샤드를 넘나드는 JOIN은 단일 DB 내부의 JOIN보다 복잡해질 수 있다.</p>
<p>따라서 필요한 데이터를 일부 중복 저장하는 <strong>비정규화(Denormalization)</strong> 등의 방식을 통해 조회 구조를 단순화하기도 한다.</p>
<hr>
<h1 id="전체-흐름-정리">전체 흐름 정리</h1>
<p>이번 장에서 가장 중요하게 느낀 부분은 각각의 기술을 따로 외우는 것보다 <strong>어떤 문제가 발생했고, 그 문제를 해결하기 위해 다음 기술이 등장했는지 이해하는 것</strong>이었다.</p>
<h3 id="1단계">1단계</h3>
<pre><code class="language-text">단일 서버</code></pre>
<p>사용자가 적을 때는 충분하다.</p>
<h3 id="2단계">2단계</h3>
<pre><code class="language-text">Web Server
   ↓
Database</code></pre>
<p>웹 서버와 데이터 저장소의 역할 분리</p>
<h3 id="3단계">3단계</h3>
<pre><code class="language-text">      Load Balancer
       ↙       ↘
   Server1   Server2</code></pre>
<p>웹 서버 수평 확장</p>
<h3 id="4단계">4단계</h3>
<pre><code class="language-text">Primary DB
  ↓
Replicas</code></pre>
<p>DB 조회 부하 분산 및 장애 대응</p>
<h3 id="5단계">5단계</h3>
<pre><code class="language-text">Cache
CDN</code></pre>
<p>반복적인 DB 조회와 정적 파일 전달 비용 감소</p>
<h3 id="6단계">6단계</h3>
<pre><code class="language-text">Stateless Web Server</code></pre>
<p>어떤 서버가 요청을 처리해도 동일하게 동작하도록 구성</p>
<h3 id="7단계">7단계</h3>
<pre><code class="language-text">Multiple Data Centers</code></pre>
<p>지역 확장 및 데이터 센터 장애 대응</p>
<h3 id="8단계">8단계</h3>
<pre><code class="language-text">Message Queue</code></pre>
<p>서비스 간 결합을 낮추고 비동기 작업 처리</p>
<h3 id="9단계">9단계</h3>
<pre><code class="language-text">Logging
Metrics
Automation</code></pre>
<p>커진 시스템을 안정적으로 운영</p>
<h3 id="10단계">10단계</h3>
<pre><code class="language-text">Database Sharding</code></pre>
<p>데이터베이스 자체도 여러 서버로 분산</p>
<hr>
<h1 id="공부하면서-정리한-핵심">공부하면서 정리한 핵심</h1>
<p>처음에는 로드밸런서, 캐시, CDN, DB 복제, 샤딩 등이 각각 독립적인 기술처럼 보였다.</p>
<p>하지만 전체적인 흐름을 따라가 보면 모두 같은 목적을 가지고 있었다.</p>
<blockquote>
<p><strong>특정 서버나 컴포넌트 하나에 요청·데이터·책임이 집중되는 문제를 줄이는 것</strong></p>
</blockquote>
<p>사용자가 증가하면 웹 서버를 여러 대로 나누고,</p>
<p>DB 부하가 커지면 읽기 서버를 나누고,</p>
<p>반복 조회가 많아지면 캐시를 사용하고,</p>
<p>정적 파일 트래픽이 커지면 CDN으로 분리하고,</p>
<p>데이터 자체가 너무 많아지면 샤딩을 적용한다.</p>
<p>결국 대규모 시스템의 확장은 단순히 <strong>“서버의 성능을 높이는 것”</strong>이 아니라,</p>
<p><strong>시스템을 여러 구성 요소로 분리하고 각 구성 요소가 독립적으로 확장될 수 있도록 만드는 과정</strong>이라고 이해할 수 있었다.</p>
<hr>
<h2 id="전반적인-후기">전반적인 후기</h2>
<p>1강은 확실히 개념적인 요소들이 강한 것 같다.
뭔가 기존에 알고 있던 개념들도 많았지만, 새롭게 알게된 개념과 내가 오인하고 있던 개념들도 정말 많았던 것 같다.</p>
<p>뭔가 전반적인 시스템의 흐름의 틀을 잡을 수 있었던 내용이라 도움이 많이 된 것 같다.</p>
<p>바로 완성된 모습을 보여주기보단, 먼저 기존 아키텍처부터 보여주고, 어떻게 이 아키텍처를 개선해나가는 지를 순차적으로 잘 보여주는 느낌이라 같이 리팩토링하는 느낌을 받을 수 있었다.</p>
<p>1강은 좀 길었던 것 같다. 확실히 설명 해야할 기본 개념들이 많다보니 정리할 내용도 많아져서 나도 뭔가 두서없이 책의 내용만 정리한 것 같다. 다음엔 좀 더 내가 이해한 방식으로 스스로 설명할 수 있도록 해보겠다.</p>
]]></description>
        </item>
    </channel>
</rss>