<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hyeon.log</title>
        <link>https://velog.io/</link>
        <description>정리 블로그</description>
        <lastBuildDate>Tue, 28 May 2024 14:53:12 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hyeon.log</title>
            <url>https://velog.velcdn.com/images/dev_dc_hyeon/profile/5255f75d-3438-44e8-be96-88e44f5292c6/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hyeon.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dev_dc_hyeon" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[데이터 시각화 (matplot, seaborn)]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EA%B0%81%ED%99%94-matplot-seaborn</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EA%B0%81%ED%99%94-matplot-seaborn</guid>
            <pubDate>Tue, 28 May 2024 14:53:12 GMT</pubDate>
            <description><![CDATA[<h1 id="python-주로-사용되는-모듈">python 주로 사용되는 모듈</h1>
<table>
<thead>
<tr>
<th>항목</th>
<th>Matplotlib</th>
<th>Seaborn (Matplotlib위에 구축됨)</th>
</tr>
</thead>
<tbody><tr>
<td>목적</td>
<td>기본적인 데이터 시각화 라이브러리</td>
<td>통계적 데이터 시각화에 중점을 둔 고수준 인터페이스</td>
</tr>
<tr>
<td>기능성</td>
<td>다양한 플롯 유형과 세부 조정 가능</td>
<td>통계적 플롯 유형과 기본 스타일링 제공</td>
</tr>
<tr>
<td>사용 용이성</td>
<td>다소 복잡하고 세부 조정 필요</td>
<td>간단하고 직관적인 API 제공</td>
</tr>
<tr>
<td>통계적 지원</td>
<td>통계적 기능이 부족</td>
<td>기본적으로 통계적 기능 지원</td>
</tr>
<tr>
<td>기본 스타일</td>
<td>기본 스타일이 단순함</td>
<td>기본 스타일이 더 세련되고 시각적으로 매력적</td>
</tr>
<tr>
<td>테마</td>
<td>기본 테마 외에 별도의 설정 필요</td>
<td>다양한 기본 테마와 스타일 제공</td>
</tr>
<tr>
<td>확장성</td>
<td>고도로 사용자 정의 가능</td>
<td>Matplotlib 기반으로 확장 가능</td>
</tr>
<tr>
<td>복잡한 플롯</td>
<td>복잡한 플롯을 그리는 데 적합</td>
<td>복잡한 플롯 구현은 Matplotlib보다 어려울 수 있음</td>
</tr>
<tr>
<td>데이터 핸들링</td>
<td>기본 데이터 핸들링 기능</td>
<td>Pandas DataFrame과의 통합이 우수</td>
</tr>
<tr>
<td>히스토그램</td>
<td>히스토그램을 위한 다양한 옵션 제공</td>
<td>히스토그램 및 커널 밀도 추정 플롯을 쉽게 생성</td>
</tr>
<tr>
<td>시작하기</td>
<td>더 많은 설정과 코드 필요</td>
<td>기본 플롯을 빠르게 생성 가능</td>
</tr>
<tr>
<td>컬러 팔레트</td>
<td>별도의 컬러 팔레트 설정 필요</td>
<td>다양한 기본 컬러 팔레트 제공</td>
</tr>
</tbody></table>
<p>Matplotlib 공식문서</p>
<p><a href="https://matplotlib.org/stable/users/explain/quick_start.html">Quick start guide — Matplotlib 3.9.0 documentation</a></p>
<p>seaborn 공식문서</p>
<p><a href="https://seaborn.pydata.org/api.html">API reference — seaborn 0.13.2 documentation</a></p>
<h1 id="matplotlib-seaborn-기본-개념">Matplotlib, Seaborn 기본 개념(?)</h1>
<p>Seaborn은 Matplotlib위에 구축되어있기에</p>
<p>결국 그래프를 표시할때는 Matplotlib의 show()함수로 표시하게 된다. </p>
<h2 id="아무것도-없는-백지-그리기">아무것도 없는 백지 그리기</h2>
<pre><code>import matplotlib.pyplot as plt

# 빈 Figure 생성
monitor_size = [6.4, 4.8]

fig = plt.figure(figsize=monitor_size) # https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.figure.html

num_graph = (1, 1, 1) # nrows, ncols, index
fig.add_subplot(*num_graph) # https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.subplot.html

# Figure를 보여줌
plt.show()</code></pre><p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e3674ced-5211-4ca1-a199-6e0bc9d8262a/image.png" alt=""></p>
<p>monitor_size와 num_graph는 각각 figure()와 add_subplot()의 기본값이다.</p>
<p>add_subplot은 총 몇개의 그래프를 nrows, ncols, index 별로 그릴것인가 인데 일단 백지를 그리고 싶었기에 총 1*1개의 그래프의 1번째에 백지를 그리는 것이다.</p>
<h2 id="제목-x축-라벨-y-축라벨-그래프에-표시">제목, x축 라벨, y 축라벨 그래프에 표시</h2>
<pre><code>import matplotlib.pyplot as plt

# 빈 Figure 생성
fig = plt.figure() # https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.figure.html
fig.add_subplot() # https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.subplot.html

plt.title(&#39;This is Title&#39;)
plt.xlabel(&#39;X Label&#39;)
plt.ylabel(&#39;Y Label&#39;)

# Figure를 보여줌
plt.show()</code></pre><p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/0fca7c81-18cd-4350-b2b0-8df2e13065af/image.png" alt=""></p>
<p>plt.title()은 그래프의 제목</p>
<p>plt.xlabel()은 x축의 이름</p>
<p>plt.ylabel()은 y축의 이름 을 할당해줄 수 있다.</p>
<h1 id="막대그래프">막대그래프</h1>
<p><strong>주로 범주형 데이터를 살펴볼때 사용한다.</strong></p>
<h2 id="matplot-사용">Matplot 사용</h2>
<pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt

# 데이터 설정
categories = [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;, &#39;D&#39;, &#39;E&#39;]
values1 = [10, 24, 36, 12, 25]
values2 = [15, 18, 25, 22, 30]

# 막대의 위치 설정
x = np.arange(len(categories))  # 카테고리의 위치
width = 0.35  # 막대의 너비

# 그림 설정
plt.figure(figsize=(10, 6))

# 막대그래프 그리기
plt.bar(x - width/2, values1, width, label=&#39;Group 1&#39;, color=&#39;skyblue&#39;)
plt.bar(x + width/2, values2, width, label=&#39;Group 2&#39;, color=&#39;lightgreen&#39;)

# 제목과 축 레이블 설정
plt.title(&#39;Grouped Bar Chart&#39;)
plt.xlabel(&#39;Categories&#39;)
plt.ylabel(&#39;Values&#39;)
plt.xticks(x, categories)  # x축 눈금을 카테고리 이름으로 설정
plt.legend()  # 범례 표시

# 그래프 표시
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/b1f285a1-062b-44f7-aa55-5c760ca28af9/image.png" alt=""></p>
<p>plt.bar는 막대그래프를 </p>
<p>plt.xticks()는 x 축의 눈금을 표시 해주는것이고</p>
<p>plt.legend()는 범례를 표시해준다.</p>
<h2 id="seaborn-사용">Seaborn 사용</h2>
<p>같은 막대그래프를 Seaborn으로 그리는 코드를 작성해보면 아래와 같다.</p>
<pre><code># https://seaborn.pydata.org/generated/seaborn.barplot.html
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt

# 데이터 설정
categories = [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;, &#39;D&#39;, &#39;E&#39;]
values1 = [10, 24, 36, 12, 25]
values2 = [15, 18, 25, 22, 30]

# DataFrame으로 데이터 변환
data = pd.DataFrame({
    &#39;Category&#39;: categories * 2,
    &#39;Values&#39;: values1 + values2,
    &#39;Group&#39;: [&#39;Group 1&#39;] * len(categories) + [&#39;Group 2&#39;] * len(categories)
})

# 그림 설정
plt.figure(figsize=(10, 6))

# Seaborn을 사용한 막대그래프 그리기
sns.barplot(data=data, x=&#39;Category&#39;, y=&#39;Values&#39;, hue=&#39;Group&#39;, palette=[&#39;skyblue&#39;, &#39;lightgreen&#39;])

# 제목과 축 레이블 설정
plt.title(&#39;Grouped Bar Chart&#39;)
plt.xlabel(&#39;Categories&#39;)
plt.ylabel(&#39;Values&#39;)

# 그래프 표시
plt.show()</code></pre><p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/5d3b94a6-9c26-4a29-a0ff-eb450eaee792/image.png" alt=""></p>
<p>sns.barplot의 매개변수로 data, x, y, hue등 한줄로 Matplot의 막대그래프보단 좀더 간편하게 같은 그래프를 그릴 수 있다. 각 매개변수는</p>
<hr>
<p>공식문서의 매개변수 설명중 일부</p>
<h3 id="parameters-매개변수"><strong>Parameters (매개변수):</strong></h3>
<ul>
<li><strong>data</strong>: DataFrame, Series, dict, array 또는 list of arrays<ul>
<li>플로팅할 데이터셋. <strong><code>x</code></strong>와 <strong><code>y</code></strong>가 없으면 이는 wide-form으로 해석됩니다. 그렇지 않으면 long-form으로 예상됩니다.</li>
</ul>
</li>
<li><strong>x, y, hue</strong>: 데이터 내 변수 이름 또는 벡터 데이터<ul>
<li>long-form 데이터 플로팅을 위한 입력값. 해석 예시는 아래 예시를 참고하세요.</li>
</ul>
</li>
<li><strong>order, hue_order</strong>: 문자열 리스트<ul>
<li>범주형 레벨을 플로팅할 순서; 그렇지 않으면 데이터 객체에서 레벨이 추론됩니다.<h1 id="countplot-분포를-나타내는-막대그래프">Countplot (분포를 나타내는 막대그래프)</h1>
</li>
</ul>
</li>
</ul>
<p><strong>범주형 변수의 분포 확인할때 유용한 그래프.</strong></p>
<h2 id="matplot-사용-1">Matplot 사용</h2>
<pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt

# 범주형 데이터 생성
categories = [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;, &#39;A&#39;, &#39;B&#39;, &#39;A&#39;, &#39;C&#39;, &#39;A&#39;, &#39;A&#39;, &#39;B&#39;, &#39;C&#39;, &#39;B&#39;]

# 범주별 빈도 계산
unique_categories, category_counts = np.unique(categories, return_counts=True)

# 막대 그래프 그리기
plt.bar(unique_categories, category_counts, color=&#39;skyblue&#39;)

# 제목과 축 레이블 설정
plt.title(&#39;Count Plot&#39;)
plt.xlabel(&#39;Categories&#39;)
plt.ylabel(&#39;Counts&#39;)

# 그래프 표시
plt.show()
</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/3536f12f-783b-473d-986c-219ccf86ea85/image.png" alt=""></p>
<h2 id="seaborn-사용-1">Seaborn 사용</h2>
<pre><code class="language-python">import seaborn as sns
import matplotlib.pyplot as plt

# 범주형 데이터 생성
categories = [&#39;A&#39;, &#39;B&#39;, &#39;C&#39;, &#39;A&#39;, &#39;B&#39;, &#39;A&#39;, &#39;C&#39;, &#39;A&#39;, &#39;A&#39;, &#39;B&#39;, &#39;C&#39;, &#39;B&#39;]

# Countplot 그리기
plt.figure(figsize=(8, 6))
sns.countplot(x=categories, palette=[&#39;skyblue&#39;])

# 제목 설정
plt.title(&#39;Count Plot&#39;)

# 그래프 표시
plt.show()
</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/d5329b7f-1a6d-417c-b986-5f7e35e7dfc7/image.png" alt=""></p>
<h1 id="선-그래프-꺾은선-그래프">선 그래프 (<strong>꺾은선 그래프)</strong></h1>
<p><strong>시간의 흐름에 따른 데이터의 변화를 보여줄때 사용</strong></p>
<h2 id="matplot-사용-2">Matplot 사용</h2>
<pre><code class="language-python"># https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html
import matplotlib.pyplot as plt

# 데이터 설정
x = [0, 1, 2, 3, 4, 5]
y1 = [0, 1, 4, 9, 16, 25]
y2 = [0, 1, 8, 27, 64, 125]

# 그림 설정
plt.figure(figsize=(10, 6))

# 첫 번째 꺾은선 그래프 그리기
plt.plot(x, y1, marker=&#39;o&#39;, linestyle=&#39;-&#39;, color=&#39;b&#39;, label=&#39;y = x^2&#39;)

# 두 번째 꺾은선 그래프 그리기
plt.plot(x, y2, marker=&#39;s&#39;, linestyle=&#39;--&#39;, color=&#39;r&#39;, label=&#39;y = x^3&#39;)

# 제목과 축 레이블 설정
plt.title(&#39;Multiple Line Plot&#39;)
plt.xlabel(&#39;X-axis&#39;)
plt.ylabel(&#39;Y-axis&#39;)

# 범례 추가
plt.legend()

# 그래프 표시
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/4083f86f-3c42-432b-8ba7-6391ae5dcc64/image.png" alt=""></p>
<h2 id="seaborn-사용-2">Seaborn 사용</h2>
<pre><code class="language-python"># https://seaborn.pydata.org/generated/seaborn.lineplot.html
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt

# 데이터 설정
x = [0, 1, 2, 3, 4, 5]
y1 = [0, 1, 4, 9, 16, 25]
y2 = [0, 1, 8, 27, 64, 125]

# 데이터를 long-form DataFrame으로 변환
data = pd.DataFrame({
    &#39;x&#39;: x + x,  # x 데이터를 두 번 반복하여 long-form 형식으로 변환
    &#39;y&#39;: y1 + y2,  # y1과 y2 데이터를 결합
    &#39;Function&#39;: [&#39;y = x^2&#39;] * len(x) + [&#39;y = x^3&#39;] * len(x)  # 각 데이터에 라벨 추가
})

# 그림 설정
plt.figure(figsize=(10, 6))

# Seaborn을 사용한 꺾은선 그래프 그리기
sns.lineplot(data=data, x=&#39;x&#39;, y=&#39;y&#39;, hue=&#39;Function&#39;, style=&#39;Function&#39;, markers=[&#39;o&#39;, &#39;s&#39;], dashes=False, palette=[&#39;b&#39;, &#39;r&#39;])

# 제목과 축 레이블 설정
plt.title(&#39;Multiple Line Plot&#39;)
plt.xlabel(&#39;X-axis&#39;)
plt.ylabel(&#39;Y-axis&#39;)

# 그래프 표시
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/2ed97926-74a2-4b20-8911-a50eb6639c75/image.png" alt=""></p>
<h1 id="산점도scatter그래프">산점도(scatter)그래프</h1>
<p><strong>두 변수 간의 상관 관계를 확인 할때나, 회귀 모델의 유용성을 시각적으로 평가하고 예측의 정확도를 평가 할떄 사용되는 그래프다.</strong></p>
<h2 id="matplot-사용-3">Matplot 사용</h2>
<pre><code class="language-python">import matplotlib.pyplot as plt

# 데이터 설정
x = [1, 2, 3, 4, 5]
y = [2, 3, 5, 7, 11]

# 산점도 그래프 그리기
plt.figure(figsize=(8, 6))  # 그림의 크기 설정
plt.scatter(x, y, color=&#39;blue&#39;, marker=&#39;o&#39;, s=100)  # 산점도 그래프 그리기

# 제목과 축 레이블 설정
plt.title(&#39;Sample Scatter Plot&#39;)
plt.xlabel(&#39;X-axis&#39;)
plt.ylabel(&#39;Y-axis&#39;)

# 그래프 표시
plt.grid(True)  # 그리드 표시
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/8f417a53-636e-4849-9cba-cfa0f979f523/image.png" alt=""></p>
<h2 id="seaborn-사용-3">Seaborn 사용</h2>
<pre><code class="language-python">import seaborn as sns
import matplotlib.pyplot as plt

# 데이터 설정
x = [1, 2, 3, 4, 5]
y = [2, 3, 5, 7, 11]

# 산점도 그래프 그리기
plt.figure(figsize=(8, 6))  # 그림의 크기 설정
sns.scatterplot(x=x, y=y, color=&#39;blue&#39;, marker=&#39;o&#39;, s=100)  # 산점도 그래프 그리기

# 제목과 축 레이블 설정
plt.title(&#39;Sample Scatter Plot&#39;)
plt.xlabel(&#39;X-axis&#39;)
plt.ylabel(&#39;Y-axis&#39;)

# 그래프 표시
plt.grid(True)  # 그리드 표시
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/5b1f3b98-b3bc-4874-bdfb-35c42f720a32/image.png" alt=""></p>
<h1 id="히스토그램-그래프">히스토그램 그래프</h1>
<p><strong>데이터 분포를 확인할때, 데이터의 추이나 패턴을 확인하여 특정 동향을 파악할때 주로 사용되는 그래프다.</strong></p>
<h2 id="matplot">Matplot</h2>
<pre><code class="language-python">import matplotlib.pyplot as plt

# 데이터 설정
data = [1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 8]

# 히스토그램 그리기
plt.figure(figsize=(8, 6))  # 그림의 크기 설정
plt.hist(data, bins=8, color=&#39;skyblue&#39;, edgecolor=&#39;black&#39;)  # 히스토그램 그리기

# 제목과 축 레이블 설정
plt.title(&#39;Histogram&#39;)
plt.xlabel(&#39;Value&#39;)
plt.ylabel(&#39;Frequency&#39;)

# 그래프 표시
plt.grid(True)  # 그리드 표시
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/3bd67e2b-9e9c-4104-8bfc-7c5e43344724/image.png" alt=""></p>
<h2 id="seaborn">Seaborn</h2>
<pre><code class="language-python">import seaborn as sns
import matplotlib.pyplot as plt

# 데이터 설정
data = [1, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 8]

# 히스토그램 그리기
plt.figure(figsize=(8, 6))  # 그림의 크기 설정
sns.histplot(data, bins=8, color=&#39;skyblue&#39;, edgecolor=&#39;black&#39;)  # 히스토그램 그리기

# 제목과 축 레이블 설정
plt.title(&#39;Histogram&#39;)
plt.xlabel(&#39;Value&#39;)
plt.ylabel(&#39;Frequency&#39;)

# 그래프 표시
plt.grid(True)  # 그리드 표시
plt.show()
</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e6d9dadc-d6ae-4660-a942-d8442b851559/image.png" alt=""></p>
<h1 id="heatmap">Heatmap</h1>
<p>행렬 형태의 데이터의 상관관계를 파악 및 유사성을 확인하고 데이터를 그룹화하거나 분류하는 데 사용, <strong>행렬 형태의 데이터를 보여줄 때 매우 유용한 그래프</strong>.</p>
<h2 id="matplot-1">Matplot</h2>
<pre><code class="language-python">import numpy as np
import matplotlib.pyplot as plt

# 데이터 생성
data = np.random.rand(10, 10)

# Heatmap 그리기
plt.figure(figsize=(8, 6))
plt.imshow(data, cmap=&#39;hot&#39;, interpolation=&#39;nearest&#39;)

# 컬러 바 추가
plt.colorbar()

# 제목 설정
plt.title(&#39;Heatmap&#39;)

# 그래프 표시
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/f6e7f166-13a8-4cae-9261-af28db222f1c/image.png" alt=""></p>
<h2 id="seaborn-1">Seaborn</h2>
<pre><code class="language-python">import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt

# 데이터 생성
data = np.random.rand(10, 10)

# Heatmap 그리기
plt.figure(figsize=(8, 6))
sns.heatmap(data, cmap=&#39;hot&#39;)

# 제목 설정
plt.title(&#39;Heatmap&#39;)

# 그래프 표시
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/8c7fa388-2e81-4a64-a327-b7ee4127bd7a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커 사용법]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EB%8F%84%EC%BB%A4-%EC%82%AC%EC%9A%A9%EB%B2%95</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EB%8F%84%EC%BB%A4-%EC%82%AC%EC%9A%A9%EB%B2%95</guid>
            <pubDate>Sun, 03 Sep 2023 23:44:39 GMT</pubDate>
            <description><![CDATA[<h1 id="도커-엔진-시작하기종료하기">도커 엔진 시작하기/종료하기</h1>
<h2 id="도커-엔진을-시작종료하는-방법">도커 엔진을 시작/종료하는 방법</h2>
<p>도커 엔진은 설치와 함께 실행되며, 계속 동작 상태로 남아 있지만 <strong>컨테이너를 실행 중이 아니라면 컴퓨터의 리소스를 거의 차지하지 않으므로 문제가 없다.</strong></p>
<p>도커 데스크톱이나 리눅스 버전 모두 도커 엔진이 한번 종료되면 모든 컨테이너는 정지 상태가 되고, 컨테이너에는 자동 실행 설정이 없으므로, 도커 엔진과 함께 컨테이너를 복구하려면 컨테이너를 따로 실행하는 스크립트로 실행해야 한다.</p>
<h1 id="컨테이너의-기본적인-사용-방법">컨테이너의 기본적인 사용 방법</h1>
<h2 id="컨테이너-사용의-기본은-도커-명령어">컨테이너 사용의 기본은 도커 명령어</h2>
<p>컨테이너 사용의 기본은 도커 명령어다. 명령 프롬프트 및 터미널 화면에서 프롬프트뒤에 다음 명령어를 입력한다.</p>
<p>컨테이너를 다루는 모든 명령은 ‘docker’ 명령어로 시작한다.</p>
<pre><code class="language-bash">docker ~</code></pre>
<h3 id="명령어와-대상">명령어와 대상</h3>
<p>docker 명령어 뒤에 오는 ‘무엇을’ ‘어떻게’에 해당하는 부분을 ‘커맨드’라고 한다. 커맨드는 다시 상위 커맨드와 하위 커맨드로 구성되며, 상위 커맨드가 ‘무엇을’, 하위 커맨드가 ‘어떻게’에 해당하는 내용을 지정한다. 또 ‘대상’에는 컨테이너명 또는 이미지명 등 구체적인 이름이 들어간다. docker 명령 뒤로 ‘무엇을’, ‘어떻게’, ‘대상’의 순으로 지정하면 된다.</p>
<pre><code class="language-bash">docker 커맨드 대상</code></pre>
<p>예를 들어 이름이 penguin인 이미지를 컨테이너(container)로 실행(run)하려면 다음과 같은 명령을 입력하면 된다.</p>
<pre><code class="language-bash">docker container run penguin</code></pre>
<p>대상과 상위 커맨드(무엇을)의 차이가 잘 와닿지 않을 수 도 있는데, 상위 커맨드는 container(컨테이너) 또는 image(이미지)와 같이 대상의 종류가 들어간다. <strong>딱 12종류만</strong>있다. 대상에 해당하는 부분에는 구체적인 이름을 지정한다.</p>
<p>따라서 penguin 이라는 이름의 이미지를 pull(내려받기)하기 위해서는</p>
<pre><code class="language-bash">docker image pull penguin</code></pre>
<p>penguin이라는 이름의 이미지로 컨테이너를 start(시작)하려면</p>
<pre><code class="language-bash">docker container start penguin</code></pre>
<p>명령어를 입력하면 된다.</p>
<h3 id="옵션과-인자">옵션과 인자</h3>
<p>도커를 써본 사람은 알고 있을 수도 있지만 위에서 말한 명령어들 보다 훨씬 긴 명령어 들을 본적이 있을 것이다.</p>
<p>명령어의 기본적인 형태는 docker [커맨드] [대상]이지만 커맨드에는 앞서 설명한 대상 외에도 <strong>옵션</strong>과 <strong>인자</strong>라는 <strong>추가 정보</strong>가 붙는다.</p>
<p>예를 들면 container run 커맨드에 -d라는 옵션을, penguin을 대상으로 —mode=1이라는 인자를 붙이면 다음과 같은 명령어가 된다.</p>
<pre><code class="language-bash">docker container run -d penguin --mode=1</code></pre>
<p>-d는 ‘백그라운드로 실행해라’ —mode=1는 모드1로 실행해라 라는 의미지만 모든 명령어에 옵션이나 인자가 붙는 것은 아니며, 커맨드와 대상만으로 구성된 명령어도 많다.</p>
<p>반대로 옵션이나 인자를 여러 개 복잡하게 붙일 수 있는 명령어도 있지만 가짓수도 적고 자주 쓰이는 옵션이나 인자도 한정돼 있으므로 기억해두는 편이 좋다.</p>
<h2 id="기본적인-명령어정리">기본적인 명령어(정리)</h2>
<pre><code class="language-bash">docker 커맨드 (옵션) 대상 (인자)</code></pre>
<p>숙달하기 전까지는 ‘docker 커맨드 대상’ 형태가 기본이고, 여기에 커맨드에 옵션이 붙거나 대상에 인자가 지정되는 경우도 있다고 기억해두면 좋다.</p>
<h3 id="커맨드상위-커맨드하위-커맨드">커맨드(상위 커맨드/하위 커맨드)</h3>
<p>도커 명령어의 커맨드는 ‘무엇을’, ‘어떻게’할 것인지 지정하는 부분이라고 설명했다. ‘컨테이너’를 ‘실행’하고 싶다면 container run 커맨드를 사용한다. 다만 start나 run처럼 <strong>container를 붙이지 않아도 실행 가능한 명령어가 있으며, 관례상 이쪽을 사용하는 경우가 많다.</strong></p>
<pre><code class="language-bash"> docker container run</code></pre>
<pre><code class="language-bash"># docker container run을 생략한 커맨드 예
docker run</code></pre>
<h3 id="옵션">옵션</h3>
<p>옵션은 커맨드에 세세한 설정을 지정하는 용도로 쓰인다.</p>
<p>백그라운드로 실행할 때는 -d, 키보드를 통해 조작하고 싶다면 -i 또는 -t를 붙이는 등, 커맨드의 실행 방법 또는 커맨드에 전달할 값을 지정한다. 옵션은 커맨드에 따라 달라진다.</p>
<p>옵션은 - 또는 —으로 시작하는 것이 일반적이지만 - 기호를 붙이지 않는 경우도 있다. 그리고 -나 —중 무엇을 사용할지는 커맨드 작성자의 취향이므로 명확한 기준은 없다.</p>
<pre><code class="language-bash"># 옵션 예
-d
-all</code></pre>
<p>커맨드에 어떤 값을 전달하고 싶을때</p>
<pre><code class="language-bash"># --name 옵션 값의 예
--name penguin</code></pre>
<p>-d와 같이 -를 하나만 사용하는 옵션은 한꺼번에 모아 쓸 수 있다. 예를 들어 -d와 -i, -t를 합쳐 -dit처럼 쓸 수 있다.</p>
<pre><code class="language-bash">-d, -i, -t를 합친 예
-dit</code></pre>
<h3 id="대상">대상</h3>
<p>커맨드와 달리 구체적인 이름을 지정한다. 이름이 penguin인 이미지의 컨테이너를 실행하려면</p>
<p>‘container start [옵션] penguin’ 명령을 사용한다.</p>
<h3 id="인자">인자</h3>
<p>대상에 전달할 값을 전달한다. 문자 코드 또는 포트 번호 등을 전달할 수 있다. 단, 인자를 지정하는 경우는 그리 많지 않다. 예를 들어, MySQL과 워드프레스를 함께 쓸 때 MySQL쪽에서 워드프레스가 지원하는 구식 인증 방식을 지정할 때 세 가지 인자를 사용하는 정도로, 필요한 상황이 제한적이다. 작성 방법은 옵션과 마찬가지로 - 또는 —로 시작하는 경우가 많다.</p>
<pre><code class="language-bash"># 인자의 예
--mode=1
--style nankyoku</code></pre>
<h3 id="상위-커맨드는-생략-가능하다❓">상위 커맨드는 생략 가능하다❓</h3>
<aside>
💡 도커 1.13부터 커맨드가 재편되면서 상위 커맨드와 하위 커맨드의 조합 형태로 일원화 됐다. 그리고 일부 커맨드는 커맨드 자체가 변경되기도 했다.
예를 들어, 이전에는 container run이 아니라 run이라고만 표기해도 됐으나 이런 커맨드가 container run과 같이 ‘상위 커맨드 + 하위 커맨드’의 형식으로 수정됐다.
하지만 이전 커맨드와의 호환성을 유지하기 위해 이전 표기법(상위 커맨드가 없는)형식으로도 실행 가능하지만 언제 수정될지 모르니 ‘상위 커맨드가 있다는 것’정도만 기억해두자.

</aside>

<pre><code class="language-bash"># 커맨드의 이전 표기법(docker run)
docker run penguin
# 재편된 커맨드의 표기법(docker run)
docker container run penguin</code></pre>
<h1 id="대표적인-명령어들표정리">대표적인 명령어들(표정리)</h1>
<h3 id="컨테이너-조작-관련-커맨드상위-커맨드-container">컨테이너 조작 관련 커맨드(상위 커맨드 container)</h3>
<p>컨테이너를 실행/종료, 컨테이너 목록 확인 등 컨테이너를 다루기 위해 사용하는 커맨드다. 컨테이너를 대상으로 어떤 일을 할지는 하위 커맨드를 통해 지정한다.</p>
<pre><code class="language-bash">docker container 하위_커맨드 옵션</code></pre>
<table>
<thead>
<tr>
<th>하위 커맨드</th>
<th>내용</th>
<th>생략 가능 여부</th>
<th>주요 옵션</th>
</tr>
</thead>
<tbody><tr>
<td>start</td>
<td>컨테이너를 실행</td>
<td>O</td>
<td>-i</td>
</tr>
<tr>
<td>stop</td>
<td>컨테이너를 정지</td>
<td>O</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>create</td>
<td>도커 이미지로부터 컨테이너를 생성</td>
<td>O</td>
<td>—name -e -p -v</td>
</tr>
<tr>
<td>run</td>
<td>도커 이미지를 내려받고 컨테이너를 생성해 실행함(다운로드는 필요한 경우에만). docker image pull, docker container create, docker container start라는 세개의 명령을 하나로 합친 것과 같다.</td>
<td>O</td>
<td>—name -e -p -v -d -i -t</td>
</tr>
<tr>
<td>rm</td>
<td>정지 상태의 컨테이너를 삭제</td>
<td>O</td>
<td>-f -v</td>
</tr>
<tr>
<td>exec</td>
<td>실행 중인 컨테이너 속에서 프로그램을 실행</td>
<td>O</td>
<td>-i -t</td>
</tr>
<tr>
<td>ls</td>
<td>컨테이너 목록을 출력</td>
<td>*1</td>
<td>-a</td>
</tr>
<tr>
<td>cp</td>
<td>도커 컨테이너와 도커 호스트 간에 파일을 복사</td>
<td>O</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>commit</td>
<td>도커 컨테이너를 이미지로 변환</td>
<td>O</td>
<td>거의 사용하지 않음</td>
</tr>
</tbody></table>
<blockquote>
<p>생략 가능 커맨드는 ‘docker container 하위_커맨드’가 아니라 ‘docker 하위_커맨드’와 같이 실행한다. 이 방법은 예전 표기법과의 호환성을 위한 것이다.
*1: 생략형은 docker ps</p>
</blockquote>
<h3 id="이미지-조작-관련-커맨드상위-커맨드-iamge">이미지 조작 관련 커맨드(상위 커맨드 iamge)</h3>
<p>이미지를 내려받거나 검색하는 등 이미지와 관련된 기능을 실행하는 커맨드다. 이미지를 대상으로 어떤 일을 할지는 하위 커맨드를 통해 지정한다.</p>
<pre><code class="language-bash">docker image 하위_커맨드 옵션</code></pre>
<table>
<thead>
<tr>
<th>하위 커맨드</th>
<th>내용</th>
<th>생략 가능 여부</th>
<th>주요 옵션</th>
</tr>
</thead>
<tbody><tr>
<td>pull</td>
<td>도커 허브 등의 리포지토리에서 이미지를 내려받음</td>
<td>O</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>rm</td>
<td>도커 이미지를 삭제</td>
<td>*2</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>ls</td>
<td>내려 받은 이미지의 목록을 출력</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>build</td>
<td>도커 이미지를 생성</td>
<td>O</td>
<td>-t</td>
</tr>
</tbody></table>
<blockquote>
<p>*2: 생략형은 docker rmi</p>
</blockquote>
<h3 id="볼륨-조작-관련-커맨드상위-커맨드-volume">볼륨 조작 관련 커맨드(상위 커맨드 volume)</h3>
<p>볼륨 생성, 목록 확인, 삭제 등 볼륨과 관련된 기능을 실행하는 커맨드다. 볼륨을 대상으로 어떤 일을 할지는 하위 커맨드를 통해 지정한다.</p>
<pre><code class="language-bash">docker volume 하위_커맨드 옵션</code></pre>
<table>
<thead>
<tr>
<th>하위 커맨드</th>
<th>내용</th>
<th>생략 가능 여부</th>
<th>주요 옵션</th>
</tr>
</thead>
<tbody><tr>
<td>create</td>
<td>볼륨을 생성</td>
<td>X</td>
<td>—name</td>
</tr>
<tr>
<td>inspect</td>
<td>볼륨의 상세 정보를 출력</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>ls</td>
<td>볼륨읨 목록을 출력</td>
<td>X</td>
<td>-a</td>
</tr>
<tr>
<td>prunem</td>
<td>현재 마운트되지 않음 볼륨을 모두 삭제</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>rm</td>
<td>지정한 볼륨을 삭제</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
</tbody></table>
<h3 id="네트워크-조작-관련-커맨드상위-커맨드-network">네트워크 조작 관련 커맨드(상위 커맨드 network)</h3>
<p>도커 네트워크의 생성, 삭제, 컨테이너의 네트워크 접속 및 접속 해제 등 도커 네트워크와 관련된 기능을 실행하는 커맨드다. 도커 네트워크란 도커 요소 간의 통신에 사용하는 가상 네트워크를 가리킨다.</p>
<pre><code class="language-bash">docker network 하위_커맨드 옵션</code></pre>
<table>
<thead>
<tr>
<th>하위 커맨드</th>
<th>내용</th>
<th>생략 가능 여부</th>
<th>주요 옵션</th>
</tr>
</thead>
<tbody><tr>
<td>connect</td>
<td>컨테이너를 도커 네트워크에 연결</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>disconnect</td>
<td>컨테이너의 도커 네트워크 연결을 해제</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>create</td>
<td>도커 네트워크를 생성</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>inspect</td>
<td>도커 네트워크의 상세 정보를 출력</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>ls</td>
<td>도커 네트워크의 목록을 출력</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>prune</td>
<td>현재 컨테이너가 접속하지 않은 네트워크를 모두 삭제</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>rm</td>
<td>지정한 네트워크를 삭제</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
</tbody></table>
<h3 id="그-밖의-상위-커맨드">그 밖의 상위 커맨드</h3>
<p>대부분은 도커 스웜과 관련된 커맨드로서 <strong>초보자 수준에서는 사용할 일이 없다</strong></p>
<table>
<thead>
<tr>
<th>상위 커맨드</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>checkpoint</td>
<td>현재 상태를 일시적으로 저장한 후, 나중에 해당 시점의 상태로 되돌릴 수 있다. (아직 실험적 기능)</td>
</tr>
<tr>
<td>node</td>
<td>도커 스웜의 노드를 관리하는 기능</td>
</tr>
<tr>
<td>plugin</td>
<td>플러그인을 관리하는 기능</td>
</tr>
<tr>
<td>secret</td>
<td>도커 스웜의 비밀값 정보를 관리하는 기능</td>
</tr>
<tr>
<td>service</td>
<td>도커 스웜의 서비스를 관리하는 기능</td>
</tr>
<tr>
<td>stack</td>
<td>도커 스웜 또는 쿠버네티스에서 여러 개의 서비스를 합쳐 구성한 스택을 관리하는 기능</td>
</tr>
<tr>
<td>swarm</td>
<td>도커 스웜을 관리하는 기능</td>
</tr>
<tr>
<td>system</td>
<td>도커 엔진의 정보를 확인하는 기능</td>
</tr>
</tbody></table>
<aside>
💡 도커 스웜이란?
컨테이너 오케스트레이션 기능을 제공해주는 툴이다. 쿠버네티스와는 별개의 도구다.

</aside>

<h3 id="단독으로-쓰이는-커맨드">단독으로 쓰이는 커맨드</h3>
<p>상위 커맨드 없이 단독으로 쓰이는 특수한 커맨드도 있다. 주로 도커 허브의 검색이나 로그인에 사용되는 커맨드다.</p>
<table>
<thead>
<tr>
<th>단독 커맨드</th>
<th>내용</th>
<th>주요 옵션</th>
</tr>
</thead>
<tbody><tr>
<td>login</td>
<td>도커 레지스트리에 로그인</td>
<td>-u -p</td>
</tr>
<tr>
<td>logout</td>
<td>도커 레지스트리에 로그아웃</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>search</td>
<td>도커 레지스트리를 검색</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>version</td>
<td>도커 엔진 및 명령행 도구의 버전을 출력</td>
<td>거의 사용하지 않음</td>
</tr>
</tbody></table>
<h1 id="컨테이너의-생성과-삭제-실행-정지">컨테이너의 생성과 삭제, 실행, 정지</h1>
<h3 id="컨테이너를-생성하고-실행하는-커맨드-docker-rundocker-container-run">컨테이너를 생성하고 실행하는 커맨드: docker run(docker container run)</h3>
<p>컨테이너를 실행하는 커맨드다. docker image pull, docker container create, docker container start의 기능을 하나로 합친 것과 같다. 이 커맨드는 현재 해당 이미지를 내려받은 상태가 아니라면 <strong>이미지를 먼저 내려받고 ‘대상’으로 사용할 이미지의 이름을 지정한다.</strong></p>
<p>컨테이너의 이름은 —name 옵션, 포트 번호는 -p, 볼륨을 마운트할 때는 -v, 컨테이너를 연결할 네트워크는 —net 옵션 등을 사용할 수 있다.</p>
<pre><code class="language-bash">docker run (옵션) 이미지 (인자)</code></pre>
<table>
<thead>
<tr>
<th>옵션 형식</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>—name 컨테이너_이름</td>
<td>컨테이너 이름을 지정함</td>
</tr>
<tr>
<td>-p 호스트_포트번호:컨테이너_포트번호</td>
<td>포트 번호를 지정함</td>
</tr>
<tr>
<td>-v 호스트_디스크:컨테이너_디렉터리</td>
<td>볼륨을 마운트함</td>
</tr>
<tr>
<td>—net=네트워크_이름</td>
<td>컨테이너를 네트워크에 연결함</td>
</tr>
<tr>
<td>-e 환경변수_이름=값</td>
<td>환경변수를 설정함</td>
</tr>
<tr>
<td>-d</td>
<td>백그라운드로 실행함</td>
</tr>
<tr>
<td>-i</td>
<td>컨테이너에 터미널(키보드)를 연결함</td>
</tr>
<tr>
<td>-t</td>
<td>특수 키를 사용 가능하도록 함</td>
</tr>
<tr>
<td>-help</td>
<td>사용 방법 안내 메시지를 출력함</td>
</tr>
</tbody></table>
<blockquote>
<p>-p는 —publish, -v는 —volumne, -e는 —env, -d는 —detach, -i는 —interactive, -t는 —tty의 생략형이다</p>
</blockquote>
<h3 id="컨테이너를-정지하는-커맨드-docker-stopdocker-container-stop">컨테이너를 정지하는 커맨드: docker stop(docker container stop)</h3>
<p>컨테이너를 삭제하려면 먼저 <strong>반드시 컨테이너를 정지시켜야 한다.</strong></p>
<pre><code class="language-bash">docker stop 컨테이너_이름</code></pre>
<h3 id="컨테이너를-삭제하는-커맨드-docker-rmdocker-container-rm">컨테이너를 삭제하는 커맨드: docker rm(docker container rm)</h3>
<p>컨테이너를 삭제하는 커맨드다. 정지 상태가 아닌 컨테이너를 대상으로 삭제하려고 하면 오류가 발생하며 컨테이너가 삭제되지 않는다.</p>
<pre><code class="language-bash">docker rm 커맨드_이름</code></pre>
<h3 id="한-번만-실행되는-컨테이너와-데몬-형태로-동작하는-컨테이너">한 번만 실행되는 컨테이너와 데몬 형태로 동작하는 컨테이너</h3>
<aside>
💡 컨테이너는 제각각 내용이 다르다. 그런만큼 컨테이너에 따라 지정 가능한 옵션이나 인자도 달라진다. 여러 가지 옵션이나 인자를 지정할 수 있는 컨테이너가 있는 반면, 인자를 지정할 수 없는 것도 있다.
특히 docker run 커맨드에서 사용ㅏ는 -d, -i, -t 옵션은 자주 쓰이는 반면, 사용하지 않을 때도 많으므로 **차이점을 꼭 숙지**해야한다.

<p>-d는 컨테이너를 백그라운드로 실행
-i와 -t는 컨테이너 내부에 터미널로 접속하기 위한 옵션</p>
<p>-d를 붙이지 않고 컨테이너를 실행하면 실행된 컨테이너가 프로그램의 실행을 마칠 때까지 터미널의 제어를 차지하므로 그다음 명령을 입력할 수 없는 상태가 된다.
또한, -i, -t 옵션으 ㄹ붙이지 않으면 컨테이너 안의 파일 시스템에 접근할 수 없다.</p>
<p>이 옵션들은 당연히 사용하는 옵션 같지만 ‘한 번만 실행되는 컨테이너’와 ‘데몬 형태로 동작하는 컨테이너’를 실행할 때는 사용하지않는다.</p>
<p>왜냐하면 한 번만 실행되는 컨테이너는 실행하자마자 종료되므로 컨테이너가 터미널의 제어를 차지하더라도 일시적인 것이기에 문제가 되지 안흔다.
그러나 데몬처럼 계속적으로 실행되는 프로그램은 저절로 종료되지 않으므로 한번 터미널의 제어를 넘기면 이를 되찾아 오기가 번거롭다.</p>
<p>또한 컨테이너 속 파일 시스템을 다르려면 -i, -t 옵션을 사용 해야 하지만 실행하자마자 곧장 종료되는 컨테이너라면 컨테이너 속 파일 시스템에 손을 댈 일도 없으므로 불필요한 옵션이 된다.</p>
</aside>

<h2 id="docker-ps-커맨드">docker ps 커맨드</h2>
<p>컨테이너의 생애주기를 관장하는 커맨드 외에 자주 쓰이는 커맨드이다.</p>
<p>이 커맨드는 컨테이너의 목록을 출력하는 기능을 하는데, docker ps는 현재 실행 중인 컨테이너의 목록을 출력하며, 옵션으로 -a를 사용하면 현재 존재하는 컨테이너(정지 상태 컨테이너도 포함)하여 목록을 출력해준다.</p>
<pre><code class="language-bash">docker ps (옵션)</code></pre>
<p>컨테이너를 실행하거나 컨테이너의 상태가 기대했던 대로인지 확인할 수 있으며, 컨테이너의 상세 정보를 확인할 때도 사용한다.</p>
<h3 id="컨테이너-목록의-정보">컨테이너 목록의 정보</h3>
<p>docker ps 커맨드를 실행하면 다음과 같은 결과가 출력된다.</p>
<p>첫 번째 행은 이름이 출력 되고, 두 번쨰 행 부터 실제 컨테이너의 정보가 출력된다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>CONTAINER ID</td>
<td>컨테이너 식별자. 무작위 문자열이 할당됨. 원래는 64글자지만 앞의 12글자만 출력한다 이 12글자로도 식별자 역할을 수행할 수 있다.</td>
</tr>
<tr>
<td>IMAGE</td>
<td>컨테이너를 만들 때 사용한 이미지의 이름</td>
</tr>
<tr>
<td>COMMAND</td>
<td>컨테이너 실행 시에 실행하도록 설정된 프로그램의 이름. 크게 신경 쓰지 않아도 된다.</td>
</tr>
<tr>
<td>CREATED</td>
<td>컨테이너 생성 후 경과된 시간</td>
</tr>
<tr>
<td>STATUS</td>
<td>컨테이너의 현재 상태. 실행 중이라면 ‘Up’, 종료된 상태라면 ‘Exited’가 출력된다.</td>
</tr>
<tr>
<td>PORTS</td>
<td>컨테이너에 할당된 포트 번호. ‘호스트 번호 → 컨테이너 포트 번호’ 형식으로 출력된다.</td>
</tr>
<tr>
<td>NAMES</td>
<td>컨테이너 이름</td>
</tr>
</tbody></table>
<h2 id="컨테이너와-통신하려면">컨테이너와 통신하려면</h2>
<p>웹 브라우저를 통해 컨테이너를 접근하려면 외부와 접속하기 위한 설정이 필요하다. 이를 위해 포트를 설정한다.</p>
<p>포트란 통신 내용이 드나드는 통로를 의미한다. ‘웹은 포트 80번’ ‘메일은 포트 25번’이다.</p>
<p>아파치는 서버에서 정해둔 포트(80번 포트)에서 웹 사이트에 대한 접근을 기다리다가 사용자가 이 포트를 통해 접근해 오면 요청에 따라 웹 사이트의 페이지를 제공한다. 하지만 컨테이너 속에서 실행 중인 아파치는 외부와 직접 연결되지 않았기 때문에 외부에서 접근할 수 없다.</p>
<p>따라서 컨테이너를 실행 중인 물리적 컴퓨터가 외부의 접근을 대신 받아 전달해주도록 해야한다.</p>
<p>컨테이너를 실행 중인 물리적인 컴퓨터(호스트)의 8080번 포트(이 포트는 다른 소프트웨어가 사용하는 포트와 겹치지 않는 한 임의의 숫자를 사용해도 된다)와 컨테이너의 80번 포트를 연결한다. 이 옵션이 -p 옵션이며, 그 뒤로 ‘호스트의 포트 번호’와 ‘컨테이너의 포트 번호’를 콜론으로 연결해 함께 기재한다.</p>
<pre><code class="language-bash"># 포트 설정 방법
-p 호스트_포트:컨테이너_포트_번호
# 포트 옵션 예
-p 8080:80</code></pre>
<p>컨테이너에서 실행되는 프로그램(소프트웨어)는 독립적으로 실행되기 때문에 위 같이 웹 서버를 실행하는 경우엔 여러개의 웹 서버를 실행할 수 있다. 이러한 상황엔 <strong>호스트 포트 번호를 모두 같은것으로 사용하면 어떤 컨테이너로 가야 할 요청인지 구분할 수 없기 때문에</strong> 컨테이너 A,B에 각각 8080,8081과 같이 겹치지 않은 포트 번호로 실행해야 한다.</p>
<p>꼭 여러 컨테이너로 연결되는 포트를 같게 설정하고 싶다면 리버스 프락시로 서버 이름을 통해 구별하도록 구성한다.</p>
<h1 id="이미지-삭제">이미지 삭제</h1>
<h2 id="이미지-삭제-방법">이미지 삭제 방법</h2>
<p>컨테이너는 이미지로 부터 만들어지는데, 컨테이너를 삭제해도 이미지는 삭제되지 않는다.</p>
<p>이미지가 늘어나면 스토리지 용량을 의미없게 차지하게 되므로 이미지는 되도록 그때그때 삭제하는게 좋다. 이미지 삭제도 컨테이너 삭제때와 같이 <strong>이미지ID 또는 이미지 이름</strong>으로 지정한다.</p>
<p>또한, 컨테이너가 실행중인 컨테이너를 삭제하지 못하는 것처럼, 이미지도 이미로 만든 컨테이너가 있으면 삭제할 수 없다. docker ps -a 명령어로 컨테이너 목록을 출력해 확인하고 <strong>컨테이너를 먼저 종료 및 삭제 하고 이미지를 삭제해야 한다.</strong></p>
<h2 id="docker-image-rm-커맨드">docker image rm 커맨드</h2>
<p>이 커맨드는 컨테이너를 삭제할 떄 처럼 docker rm처럼 생략할 수 없다. docker rm은 컨테이너 삭제 커맨드의 생략형인 docker container rm이기 때문이다.</p>
<h3 id="이미지를-삭제하는-커맨드-사용방법">이미지를 삭제하는 커맨드 사용방법</h3>
<pre><code class="language-bash"># 자주 쓰이는 커맨드
docker image rm 이미지_이름
# 여러 개의 이미지를 삭제하고 싶을 때
docker image rm 이미지_이름 이미지_이름 이미지_이름</code></pre>
<h1 id="docker-image-ls-커맨드">docker image ls 커맨드</h1>
<p>이미지를 삭제하려면 이미지 이름 또는 이미지 ID를 알아야 한다.</p>
<p>컨테이너 목록을 출력하는 docker ps 커맨드가 있듯 이미지 목록을 확인하는 커맨드도 있다. 이 커맨드가 바로 docker image ls 커맨드다.</p>
<p>이 명령어를 사용하면 REPOSITORY, TAG, IMAGE ID, CREATED, SIZE 등을 확인할 수 있다.</p>
<h1 id="docker-network-생성삭제">docker network 생성/삭제</h1>
<h3 id="도커-network를-생성하는-커맨드">도커 network를 생성하는 커맨드</h3>
<pre><code class="language-bash"># 자주 사용하는 커맨드 예
docker network create 네트워크_이름</code></pre>
<h3 id="도커-network를-삭제하는-커맨드">도커 network를 삭제하는 커맨드</h3>
<pre><code class="language-bash"># 자주 사용하는 커맨드 예
docker network rm 네트워크_이름</code></pre>
<h3 id="그-외-도커-네트워크-관련-커맨드">그 외 도커 네트워크 관련 커맨드</h3>
<table>
<thead>
<tr>
<th>커맨드</th>
<th>내용</th>
<th>생략 가능 여부</th>
<th>주요 옵션</th>
</tr>
</thead>
<tbody><tr>
<td>connect</td>
<td>네트워크에 컨테이너를 새로이 접속</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>disconnect</td>
<td>네트워크에서 컨테이너의 접속을 끊음</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>create</td>
<td>네트워크를 생성</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>inspect</td>
<td>네트워크의 상세 정보를 확인</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>ls</td>
<td>네트워크의 목록을 확인</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>prune</td>
<td>현재 아무 컨테이너도 접속하지 않은 네트워크를 모두 삭제</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
<tr>
<td>rm</td>
<td>지정한 네트워크를 삭제</td>
<td>X</td>
<td>거의 사용하지 않음</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[도커 실행을 위해 필요한것]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EB%8F%84%EC%BB%A4-%EC%8B%A4%ED%96%89%EC%9D%84-%EC%9C%84%ED%95%B4-%ED%95%84%EC%9A%94%ED%95%9C%EA%B2%83</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EB%8F%84%EC%BB%A4-%EC%8B%A4%ED%96%89%EC%9D%84-%EC%9C%84%ED%95%B4-%ED%95%84%EC%9A%94%ED%95%9C%EA%B2%83</guid>
            <pubDate>Sun, 03 Sep 2023 23:33:31 GMT</pubDate>
            <description><![CDATA[<p>도커를 사용하기 위해 필수적인 요소들은 뭐가 있을까?? 알아보자.</p>
<h1 id="도커를-사용하려면❓">도커를 사용하려면❓</h1>
<p>실제로 도커를 사용하는 방법에 대해서 알아보자.</p>
<h2 id="윈도우용macos용-도커-사용하기">윈도우용/macOS용 도커 사용하기</h2>
<h3 id="도커는-기본적으로-리눅스용이지만-윈도우와-macos에서도-사용할-수-있다">도커는 기본적으로 리눅스용이지만 윈도우와 macOS에서도 사용할 수 있다</h3>
<p>윈도우와 macOS에서는 각각 ‘윈도우용/macOS용 도커 데스크톱’이 패키지 형태로 제공된다.</p>
<p>리눅스 컴퓨터에 도커를 설치하려면 도커 엔진만 있으면 되지만 윈도우나 macOS에서는 리눅스 운영체제 등의 실행환경이 추가로 필요하기 때문에 이들을 함께 패키지로 묶어 배포한다.</p>
<p>도우용/macOS용이긴 하지만 완전한 윈도우/macOS용 소프트웨어는 아니며 윈도우나 macOS에 사용자에게는 보이지 않는 <strong>가상의 리눅스 환경</strong>을 만들고 이 환경에서 도커 엔진을 구동하는 구조다.</p>
<p>이런 관점에서 보면 앞서 설명한 ‘어떤 형태로든 리눅스 환경에 설치하고 윈도우 또는 macOS를 통해 사용’과 매우 비슷하지만 가상화 부분에서 약간 차이가 있다.</p>
<p>‘어떤 형태로든 리눅스 환경에 설치하고 윈도우 또는 macOS를 통해 사용하는 방법’은 사용자가 명시적으로 ‘가상화 소프트웨어를 설치하고, 그 위에 리눅스 운영체제를 설치한 다음 여기에 도커 엔진을 설치’하는 방법으로 구축하는 데 비해 도커 데스크톱은 사용자가 가상화 환경이나 리눅스 운영체제를 <strong>신경 쓰지 않아도</strong> 도커를 사용할 수 있다.</p>
<p>또한 내부적으로 사용되는 가상화 소프트웨어에도 차이가 있다. 2번 방법은 VirtualBox나 VMware 같은 소프트웨어를 사용하는데 비해 윈도우용 데스크톱은 Hyper-V, macOS용 도커 데스크톱은 HyperKit이라는 가상화 기술을 사용한다. Hyper-V는 윈도우에 포함돼 있으며 HyperKit은 도커 데스크톱 패키지에 포함돼 있다.</p>
<h3 id="도커-데스크톱은-일반적인-소프트웨어처럼-사용한다">도커 데스크톱은 일반적인 소프트웨어처럼 사용한다</h3>
<p>도커 데스크톱은 설치가 간편할뿐만 아니라 도커를 사용하면서 <strong>가상화 소프트웨어나 리눅스 운영체제의 존재를 신경 쓰지 않아도</strong>된다는 특징이 있다.</p>
<p>다만 도커 데스크톱을 실행한 후 실제 도커 엔진을 다룰 때는 드래그 앤드 드롭 같은 마우슬 리용한 조작은 불가능하며, 명령행 인터페이스를 통해 조작해야 한다. 리눅스 컴퓨터에서 사용해도 마찬가지로 명령행 인터페이스를 사용해야 한다.</p>
<h3 id="운영체제가-두-개라니-괜찮을까-❓">운영체제가 두 개라니 괜찮을까 ❓</h3>
<p>운영체제가 두개여서 어렵게 생각할 핑료도 없고 혼란스러울 일도 없다.</p>
<p><strong>도커가 전용으로 사용하는 숨겨진 운영체제</strong>와 같다. 더 이상 사용하지 않게 되어 도커 데스크톱 패키지를 삭제하면 이 리눅스 운영체제도 함께 삭제된다.</p>
<h3 id="도커-데스크톱을-사용하기-위한-조건과-도커-데스크톱이-불안정한-경우">도커 데스크톱을 사용하기 위한 조건과 도커 데스크톱이 불안정한 경우</h3>
<p>문제는 오히려 도커 데스크톱을 사용하기 위한 조건과 간혹 도커 데스크톱이 불안정해지는 경우다.</p>
<p>도커 데스크톱을 사용하려면 윈도우에서는 <strong>Hyper-V(윈도우영 가상환경)가 활성 상태</strong>여야 한다. macOS에서는 사용 요건외에 따로 필요한 조건은 없다.</p>
<p>도커 데스크톱이 불안정해지는 증상의 예로는 매우 느려지거나 VirtualBox 또는 VMware 같은 가상화 소프트웨어와 충돌을 일으키는 경우다.</p>
<p>조건만 맞는다면 <em>8도커 데스크톱이 가장 간단한 방법*</em>임에는 틀림없다. 리눅스를 따로 설치하지 않아도 되고 원격 접속 때문에 골치를 썩힐 일도 없다.</p>
<h3 id="윈도우용macos용-도커-데스크톱을-사용하기-위해-필요한-것">윈도우용/macOS용 도커 데스크톱을 사용하기 위해 필요한 것</h3>
<hr>
<aside>
💡 사용 조건을 만족하는 윈도우 또는 macOS
**주의할 점**
평소에 가상화 소프트웨어를 사용했다면 윈도우와 가상화 소프트웨어를 모두 최신 버전으로 업데이트 해야 한다.

</aside>

<h3 id="wsl2는-무엇인가-윈도우-홈-에디션에서도-도커를-사용할-수-있을까-❓">WSL2는 무엇인가? 윈도우 홈 에디션에서도 도커를 사용할 수 있을까 ❓</h3>
<aside>
💡 도커 데스크톱은 윈도우 10 홈 에디션에서는 사용할 수 없고 프로 이상의 버전을 사용해야 한다. 프로 버전 이상에만 Hyper-V가 포함되어 있기 때문이다.
그러나 2020년 윈도우 10에 **WSL2**라는 새로운 기능이 추가되었다. Windows Subsystem for Linux 2의 약자로, 윈도우에서 리눅스 소프트웨어를 실행하기 위한 기능이다. 이 기능을 이용하면 윈도우 10 홈 에디션에서도 도커 데스크톱을 사용할 수 있다.

</aside>

<h2 id="도커를-실행하기-위한-조건">도커를 실행하기 위한 조건</h2>
<p>도커는 <strong>64비트 운영체제</strong>에서만 동작한다.</p>
<h3 id="도커-엔진-유료-버전-enterprise-edtion">도커 엔진 유료 버전 ‘Enterprise Edtion’</h3>
<aside>
💡 도커는 무료 버전인 **Community Edition(CE)’** 버전과 유료 버전인 **’Enterprise Edition(EE)’**이 있다.
두 버전의 차이점은 EE는 보증과 함께 인증을 거친 인프라 또는 플러그인, 보언 검사 등을 제공한다. 특히 윈도우 서버는 EE만 지원하고 있다.

</aside>]]></description>
        </item>
        <item>
            <title><![CDATA[도커란 무엇일까?]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EB%8F%84%EC%BB%A4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EB%8F%84%EC%BB%A4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Sun, 03 Sep 2023 08:50:39 GMT</pubDate>
            <description><![CDATA[<h1 id="들어가기전">들어가기전</h1>
<aside>
💡 도커란 무엇일까?
도커가 무엇인지 물어보면 “컨테이너 기술이 어떻다”하는 답변만 돌아올 뿐 명확한 답변은 얻기 어렵다.

</aside>

<h1 id="도커란-무엇인가">도커란 무엇인가?</h1>
<h3 id="도커는-데이터-또는-프로그램을-격리시키는-기능을-제공한다">도커는 ‘데이터 또는 프로그램을 격리시키는’ 기능을 제공한다</h3>
<aside>
💡 도커란 데이터 또는 프로그램을 격리시키는 기능을 제공하는 소프트웨어다.

</aside>

<p>이 기능은 주로 서버에 사용된다. 클라이언트 컴퓨터에서도 사용할 수는 있지만 현 시점에는 서버에서 사용하는것이 <strong>주 용도</strong>다.</p>
<p>개인용 컴퓨터나 서버에는 여러 가지 프로그램이 함께 동작한다.</p>
<p>예를 들면, 개인 컴퓨터에선 워드, 엑셀, 메일 프로그램을 동시에 사용할 수 있다.</p>
<p>서버용 컴퓨터에선 아파치, MySQL 등 여러 프로그램이 함께 동작된다.</p>
<p>도커는 다양한 프로그램과 데이터를 각각 <strong>독립된 환경에 격리</strong>하는 기능을 제공한다.</p>
<p>그것도 운영체제(비슷한 것) 통째로 격리하는 기능을 제공해준다.</p>
<h3 id="컨테이너와-도커-엔진">컨테이너와 도커 엔진</h3>
<aside>
💡 컨테이너란?
개인용 컴퓨터 또는 서버 상의 환경을 마치 코스트코에서 판매하는 조립형 창고 같은 작은 방으로 분할한 단위. 이 작은 방에는 **데이터나 프로그램을 둔다.**

</aside>

<p>도커를 사용하려면 도커 소프트웨어의 본질인 <strong>도커 엔진</strong>을 설치해야 한다.</p>
<p><strong>도커 엔진</strong>은 컨테이너를 생성하고 구동시킬 수 있다.</p>
<h3 id="컨테이너를-만드려면-이미지가-필요하다">컨테이너를 만드려면 이미지가 필요하다</h3>
<p>도커 엔진이 있어야 컨테이너를 만들 수 있다고 했는데,</p>
<p>컨테이너를 만들려면 도커 엔진 외에도 컨테이너의 빵틀과도 같은 역할을 하는 <strong>이미지</strong>가 필요하다.</p>
<p>이미지는 종류가 아주 많다. 만들고자 하는 컨테이너의 본질(?)인 소프트웨어의 종류에 따라 달라진다.</p>
<p>예를 들면 아파치 컨테이너를 만들려면 아파치 이미지를 사용해야 하며, MySQL 컨테이너를 만들려면 MySQL 이미지를 사용해야 한다.</p>
<p>컨테이너는 용량이 허락하는 한 하나의 도커에서 여러 개 만들 수도 있다.</p>
<h2 id="데이터나-프로그램을-독립된-환경에-격리해야-하는-이유-❓">데이터나 프로그램을 독립된 환경에 격리해야 하는 이유 ❓</h2>
<p>왜 데이터나 프로그램을 독립된 환경에 격리해야 할까?</p>
<p>데이터는 둘째 치고 프로그램을 독립된 환경에 격리해야 하는 이유는 대부분의 프로그램은 프로그램 단독으로 동작하는 것이 아니라 <strong>어떤 실행 환경이나 라이브러리, 다른 프로그램과 함께 동작</strong>한다.</p>
<p>예를 들면, PHP로 작성된 프로그램을 실행하려면 PHP 실행 환경이 필요하고, 파이썬으로 작성된 프로그램은 다른 라이브러리를 사용해야 하는 경우가 많다.</p>
<p>소프트웨어 역시 단일 프로그램이 아니라 여러 개의 프로그램으로 구성된 경우가 많다.</p>
<p>예를 들면, 워드프레스(WordPress)는 MySQL 데이터베이스가 없다면 사용할 수 없다.</p>
<p>그리고, 다른 프로그램과 특정한 디렉터리를 공유하거나 같은 경로에 설정 정보를 저장하는 경우도 있다.</p>
<p>예를 들면 워드프레스는 MySQL가 있어야 사용할 수 있는데, 워드프레스 5.0버전은 MySQL 8.0 버전 이상이어야만 정상 동작하는데, 워드프레스 하나만을 위해 MySQL을 8버전 이상으로 업데이트 해야하는 경우가 있을 것이다.</p>
<p>위 같은 상황은 공통으로 함께 연동되는 소프트웨어의 상황이었지만, 실행 환경이나 라이브러리, 디렉터리 설정 파일에서도 같은 일이 일어날 수 있다. 공유하는 대상을 어느 한쪽만을 위해 수정하면 다른 쪽에서 오류가 발생하게 된다.</p>
<p>이런 문제는 업데이트할 때만 생기는 것은 아니다.</p>
<p>서버에서는 여러 프로그램이 함께 동작하므로 서버를 처음 구축할 때부터 신중하게 따져보지 않으면 안된다.</p>
<p>설계할 때는 문제가 없었던 프로그램끼리도 실제로 설치해보면 오류를 일으키는 경우도 있다. 이러한 <strong>문제의 원인은 대부분 프로그램 간 공유</strong>에 있다.</p>
<p>프로그램에 따라서는 한 서버에 한 버전 밖에 설치할 수 없으므로 최소 버전을 같이 맞춰 놓으면 문제가 되지 않는다. 그러나 신규 개발이라면 모를까, 기존 프로그램을 함께 설치하려는 상황이라면 연동 프로그램의 버전을 맞출 수 없을 수 도 있다.</p>
<p>디렉터리 역시 시스템 A,B가 같은 디렉터리를 사용하게 돼 있어서 설정 파일이 섞이거나 설정에 충돌이 생길 수 있다.</p>
<p>프로그램도 한 서버에서 함께 지내려면 사람이 함께 지내는 것 이상으로 신경 쓸 것이 많이 생긴다.</p>
<h2 id="프로그램의-격리란❓">프로그램의 격리란❓</h2>
<p>도커 컨테이너는 다른 컨테이너와 완전히 분리된 환경이라고 위에 설명했는데 즉, 컨테이너 안에 들어있는 프로그램은 다른 프로그램과 격리된 상태가 된다.</p>
<p>도커 컨테이너를 사용해 프로그램을 격리하면 여러 프로그램이 한 서버에서 실행되면서 발생하는 문제를 대부분 해결할 수 있다.</p>
<p>일반적인 환경에선 한 대의 서버 혹은 컴퓨터에 한 버전만 설치할 수 있는 소프트웨어가 대부분이다. 워드나 엑셀처럼 버전별로 여러 개를 설치할 수 있는 경우도 있지만, 원칙적으로는 불가능하다고 보는 것이 옳다.</p>
<p>도커 컨테이너는 <strong>완전히 독립된 환경</strong>이므로 여러 컨테이너에서 같은 프로그램을 실행할 수 있다. 버전이 완전히 동일해도 상관없다.</p>
<h1 id="도커의-동작-원리">도커의 동작 원리</h1>
<h2 id="도커의-구조">도커의 구조</h2>
<p>우선 물리 서버가 있고, 이 물리 서버에서 동작하는 리눅스 운영체제가 있다. 여기까지는 일반적인 서버와 다를 바 없다.</p>
<p>일반적인 서버라면 운영체제 위에 프로그램이나 데이터가 직접 올라가겠지만 도커를 사용하는 경우엔 <strong>운영체제 위에 도커 엔진이 동작하고 그 위에서 컨테이너가 동작</strong>한다.</p>
<h3 id="컨테이너-안에는-운영체제-비슷한-것이-들어있다">컨테이너 안에는 운영체제 ‘비슷한 것’이 들어있다.</h3>
<p>컨테이너 속에 넣는다는 표현을 생각하면 빈 컨테이너에 무언가를 넣는 상상하게 되지만 ‘정말로 텅텅 빈 컨테이너’는 의외로 사용되지 않는다.</p>
<p>컨테이너의 구조는 <strong>모든 컨테이너에는 ‘리눅스 운영체제 비슷한 무언가’</strong>가 들어 있다. 음식점에서 아무것도 주문하지 않아도 기본 반찬이 나오듯, 최소한의 컨테이너에도 리눅스 운영체제 비슷한 무언가가 기본 반찬처럼 나온다.</p>
<h3 id="운영체제가-하는-일은-뭘까❓">운영체제가 하는 일은 뭘까❓</h3>
<aside>
💡 운영체제는 **소프트웨어나 프로그램의 명령을 하드웨어에 전달하는 역할**을 한다.

</aside>

<h2 id="도커는-기본적으로-리눅스용이다">도커는 기본적으로 ‘리눅스’용이다</h2>
<p>도커는 밑바탕에서 <strong>리눅스 운영체제가 동작하는 것을 전제</strong>로 하는 구조로 돼어있기 때문에 <strong>리눅스 운영체제에서만 동작</strong>할 수 있다.</p>
<p>그리고, 컨테이너 안에 들어 있는 주변 부분도 이에 맞춰 리눅스 운영체제의 주변 부분이어야 한다. 그렇기에 컨테이너에서 실행할 소프트웨어(프로그램) 역시 리눅스용 소프트웨어다. 윈도우나 macOS용 소프트웨어는 컨테이너에 넣어도 동작하지 않는다.</p>
<p>즉, 도커는 리눅스 컴퓨터에 독립된 환경을 만드는 것이며, 리눅스에서만 동작하고, 컨테이너에서 동작할 프로그램도 리눅스용 컴퓨터다.</p>
<h1 id="도커-허브와-이미지-그리고-컨테이너">도커 허브와 이미지, 그리고 컨테이너</h1>
<h2 id="이미지와-컨테이너">이미지와 컨테이너</h2>
<p>컨테이너를 생성하려면 먼저 이미지를 만들어야 한다. 이미지는 컨테이너를 찍어내는 <strong>빵틀</strong>과 같은 것인데, 컨테이너의 설계도 역할을 한다.</p>
<h3 id="이미지는-금형과-같다">이미지는 금형과 같다</h3>
<p>로봇 장난감을 원하는 아이에게 금형은 아무 소용이 없다. 이미지 역시 이미지 자체로는 큰 쓸모가 없다. <strong>이미지는 컨테이너를 만드는 데 사용</strong>한다.</p>
<p>즉, <strong>이미지만</strong> 사용할 일은 없고, 컨테이너를 사용하는 것이다.</p>
<h3 id="컨테이너로도-이미지를-만들-수-있다">컨테이너로도 이미지를 만들 수 있다</h3>
<p>이미지로만 컨테이너를 만들 수 있는 것이 아니라 컨테이너로도 이미지를 만들 수 있다.</p>
<p>컨테이너로 이미지를 만드는 것은, 이미 만든 컨테이너에 손을 대서 컨테이너의 금형을 새로이 만드는 과정이다.</p>
<p>컨테이너로부터 이미지를 만들 수 없다면, 여러 개의 컨테이너를 일일이 수정할 수 밖에 없었을 것이기에 매우 번거로운 일이었을 것이다.</p>
<h3 id="도커-엔진-간에-이동이-가능하다">도커 엔진 간에 이동이 가능하다</h3>
<p>동일한 컨테이너를 여러 개 만들지 않더라도 이러한 특성을 이용해 다른 물리 서버에 설치된 도커 엔진으로 컨테이너를 이동시킬 수 있다. 컨테이너는 도커 엔진만 설치돼 있으면 구동이 가능하므로 다른 서버나 컴퓨터에 도커 엔진을 설치하고 도커 엔진에 이미지를 이용해 똑같은 컨테이너를 생성하면 된다.</p>
<h2 id="도커-허브와-도커-이미지">도커 허브와 도커 이미지</h2>
<p>이미지는 어디서 구해야 할까❓이미지는 주로 ‘도커 허브’에서 구한다. 도커 허브는 공식적으로 운영되는 도커 레지스트리(도커 이미지를 배포하는 서비스)의 이름이다</p>
<p>도커 허브는 스마트폰의 스토어와 같은 존재로, <strong>공개된 컨테이너 이미지가 모여 있는 곳</strong>이다.</p>
<h3 id="도커-허브에는-어떤-이미지가-공개돼-있을까❓">도커 허브에는 어떤 이미지가 공개돼 있을까❓</h3>
<p>도커 허브에는 운영체제(비슷한 것)만 들어 있는 이미지부터, 여러 가지 소프트웨어가 함께 포함된 것 까지 다양한 이미지가 제공된다. 직접 수정하고 싶다면 간단한 이미지, 편리하게 사용하고 싶다면 설정이 완료된 이미지 등 용도에 따라 원하는 것을 선택하면 된다.</p>
<p>같은 소프트웨어도 다양한 개조된 이미지가 제공된다.</p>
<p>리눅스에는 다양한 배포판이 있는데, 주요 배포판은 모두 이미지로 제공되며, 배포판의 버전마다 운영체제(비슷한 것)만 들어간 컨테이너도 별도로 제공된다.</p>
<h3 id="안전한-컨테이너-이미지를-고르는-법">안전한 컨테이너 이미지를 고르는 법</h3>
<p>이렇게 이미지의 종류가 많다 보니 이미지를 선택하는 데 어려움을 느낄 수도 있다. 그리고 도커 허브는 누구나 자유롭게 이미지를 등록할 수 있기 때문에 안전하지 못한 이미지가 있을 수도 있다.</p>
<p>안전한 이미지를 선택하는 주요 방법을 살펴보자.</p>
<hr>
<aside>
💡 **공식 이미지를 사용한다**
컨테이너 이미지 중에는 공식 이미지가 여럿 있다. 이 공식 이미지는 도커에서 직접 배포하거나 해당 소프트웨어를 개발 및 관리하는 기업이나 조직에서 제공하는 것 도 있다.

<p>공식 이미지를 사용하면 이미지 선택도 쉽고 보안도 챙길 수 있다.</p>
<p>다만 컨테이너에 포함된 ‘운영체제(비슷한 것)’이 특정 운영체제 및 버전으로 한정된 경우가 있어서 원하는 것을 선택하지 못할 수도 있으므로 꼭 특정 운영체제 및 버전을 사용해야 하는 상황에는 주의가 필요하다</p>
</aside>

<hr>
<aside>
💡 **커스텀 이미지를 직접 만들어 사용한다**
컨테이너 이미지를 원하는 대로 구성해 커스텀 이미지를 만들 수 있다. 필요한 최소한의 요소가 담긴 이미지에 필요한 소프트웨어를 추가로 설치해 커스텀 이미지를 만든다.

<p>’운영체제(비슷한 것)’만 들어있는 컨테이너부터 직접 만드는 것은 추천하지 않으나 운영체제가 포함된 이미지에 소프트웨어를 넣는 정도는 그리 어렵지 않다.</p>
<p>공식 이미지가 아닌 모든 커스텀 이미지가 위험성을 안고 있는 것은 아니다. 공식 이미지가 아니더라도 선량한 사용자들이 만든 안전한 이미지도 많다.</p>
<p>다만 경험이 쌓일 때까지는 섣불리 판단하지 말고 신중하게 이미지를 선택해 사용해서 만들자.</p>
</aside>

<h3 id="다양한-형태로-조합이-가능한-컨테이너">다양한 형태로 조합이 가능한 컨테이너</h3>
<p>도커를 사용할 때의 원칙 중 하나로, ‘한 컨테이너에 한 프로그램’이라는 것이 있다. 말 그대로 하나의 프로그램만 담긴 컨테이너를 사용한다는 의미로, 보안 및 유지 관리 측면에서 유리하기 때문에 많이 쓰이는 정책이다.</p>
<p>예를 들면, 워드프레스를 구축할 때도 다양한 구성이 가능하다.</p>
<p>워드프레스를 사용하려면 아파치, MySQL 같은 DBMS, 워드프레스 세 가지 소프트웨어가 필요하다.</p>
<p>도커를 사용해 워드프레스를 구축하는 방법은 이 3개를 각각의 컨테이너로 구성할 수도 있고<strong>(추천)</strong>, 한 컨테이너에 모두<strong>(비추천)</strong> 집어넣는 방법도 있을 것이다.</p>
<h3 id="모든-프로그램이-담긴-컨테이너를-만드는-방법">모든 프로그램이 담긴 컨테이너를 만드는 방법</h3>
<p>컨테이너를 구축하는 방법은 다양하다. 컨테이너의 구성도 그만큼 다양하지만 그중에서도 모든 프로그램을 한데 모아 담은 컨테이너는 바로 실행할 수 있어서 자주 쓰인다.
고객 납품이나 특수한 환경에서는 사용할 수 없겠지만 IT직종에 몸담은 만큼 ‘맛보기라도 써보고 싶다’는 마음이 들기 마련인데, 이럴 때는 모든 프로그램을 담은 컨테이너도 편리하게 사용할 수 있다.</p>
<h3 id="운영체제비슷한-것을-한-종류로-통일해야-할까❓">운영체제(비슷한 것)을 한 종류로 통일해야 할까❓</h3>
<p>도커를 사용하면 호스트 컴퓨터와 컨테이너에서 모두 리눅스 운영체제가 동작한다.</p>
<p>즉, 하나의 물리적 컴퓨터에 여러 개의 운영체제가 있는 상태가 도니다. 뭉뚱그려 리눅스라고 했지만 그 안에도 여러 배포판이 있다.</p>
<p>그렇다면 호스트 컴퓨터와 컨테이너에서 각각 다른 배포판을 사용해도 괜찮을까❓</p>
<p>결론부터 말하면 호스트 컴퓨터와 컨테이너 또는 컨테이너 간에 배보판이나 버전 차이가 있어도 <strong>아무 문제가 없다</strong>.</p>
<p>애초에 도커를 사용하는 경우엔 컨테이너에 세세한 설정을 하지도 않고, 이미지를 선택할 때도 ‘운영체제(비슷한 것)’이 무엇인지도 크게 신경 쓰지 않고 그냥 ‘Latest’(최신 버전)을 선택하는 경우가 많다.</p>
<p>다만 컨테이너에 로그인할 필요가 있거나 특정 DBMS를 사용할 때는 운영체제 종류에 따라 문제를 일으킬 수 있으므로 <strong>배포판을 정확히 선택</strong>해야 한다.</p>
<h1 id="도커-컨테이너의-생애주기와-데이터-저장">도커 컨테이너의 생애주기와 데이터 저장</h1>
<aside>
💡 도커 컨테이너는 ‘오랫동안 아껴 쓰는’ 물건이 아니라 ‘그때 그때 쓰고 버리는’ 일회용품에 가깝다. 이 점을 이해해야 컨테이너 기술을 제대로 활용할 수 있다.

</aside>

<h2 id="도커-컨테이너는-쓰고-버리는-일회용품">도커 컨테이너는 ‘쓰고 버리는’ 일회용품</h2>
<p>컨테이너에 대한 설명을 듣다 보면 컨테이너의 수명 또는 컨테이너의 생애주기라는 이야기가 반드시 나오게 된다. <strong>컨테이너는 ‘쓰고 버리는’ 일회용품 같은 것</strong>이기 때문이다.</p>
<p>앞서 얘기했듯이 컨테이너는 쉽게 만들 수 있다. 그러므로 컨테이너 하나를 업데이트하면서 계속 사용하기 보단 업데이트된 소프트웨어가 들어있는 새로운 컨테이너를 사용하는 것이 좋다.</p>
<p>즉, 새로운 버전이 나오면 새로운 컨테이너로 갈아타는 것이다.</p>
<p>이것이 가능한 이유는 컨테이너는 일반적으로 <strong>여러 개를 동시 가동하는 상황을 전제</strong>로 하기 때문이다. 여러 개의 컨테이너를 하나하나 업데이트하려면 많은 수고가 든다. 초기 구축은 간단히 마쳤는데 유지보수할 때마다 컨테이너를 일일이 업데이트하려니 컨테이너의 장점이 반감된다. 유지보수 횟수가 훨씬 많은 것도 그렇다.</p>
<p>이러한 이유로 오래된 컨테이너를 버리고 새로운 이미지로부터 새로운 컨테이너를 만들어 갈아타는 방식을 사용한다. 이런 방법을 사용하면 수고를 크게 덜 수 있다.</p>
<p>이렇게 컨테이너를 만들고, 실행하고, 종료하고, 폐기한 다음, 다시 컨테이너를 만드는 일련의 과정을 <strong>컨테이너의 생애주기</strong>라고 부른다.</p>
<h2 id="데이터-저장">데이터 저장</h2>
<p>컨테이너를 폐기하면 컨테이너 안에 들어있던 데이터는 어떻게 될까❓</p>
<p>컨테이너를 폐기하면 해당 컨테이너 안에서 편집했던 파일은 당연히 사라진다. 이 파일은 컨테이너 안에 들어있었기 때문이다. 이러면 상당히 곤란하다.</p>
<p>이런 상황을 방지하기 위해 보통은 <strong>도커가 설치된 물리적 서버(호스트)의 디스크를 마운트해</strong> 이 디스크에 데이터를 저장한다.</p>
<p>마운트는 ‘디스크를 연결해 데이터를 기록할 수 있는 상태’다. 컴퓨터에 USB나 외장HDD를 연결하듯이 도커 컨테이너도 물리적 컴퓨터의 HDD또는 SSD에 연결해 데이터를 기록할 수 있다.</p>
<p>이런 방법으로 컨테이너가 폐기되거나 도커엔진 자체에 문제가 생기더라도 <strong>데이터는 외부에 안전하게 저장되어 사라지지 않는다.</strong></p>
<p>따라서 이 외부에 저장된 데이터를 다른 컨테이너와 데이터를 공유할 수도 있어서 매우 편리하다.</p>
<p>간단히 설명하면 운영체제나 소프트웨어 부분은 컨테이너 형태로 만들었다가 쓰고 버리는 것을 반복하고, 데이터는 다른 곳에 저장해 같은 것을 계속 사용한다고 보면 된다. 설정 파일도 마찬가지다. 파일을 수정했다면 삭제되지 않을 곳에 저장한다.</p>
<p>하지만 프로그램을 개발할 때는 다른 저장소에 저장하지 않는 경우도 있으므로 컨테이너를 폐기하기 전에 <strong>중요한 데이터</strong>가 컨테이너에 포함돼 있지 않은지 확인해야 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[빅엔디안과 리틀엔디안]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EB%B9%85%EC%97%94%EB%94%94%EC%95%88%EA%B3%BC-%EB%A6%AC%ED%8B%80%EC%97%94%EB%94%94%EC%95%88</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EB%B9%85%EC%97%94%EB%94%94%EC%95%88%EA%B3%BC-%EB%A6%AC%ED%8B%80%EC%97%94%EB%94%94%EC%95%88</guid>
            <pubDate>Sat, 19 Aug 2023 09:06:13 GMT</pubDate>
            <description><![CDATA[<h1 id="빅엔디안이란">빅엔디안이란?</h1>
<p>이 방식은 사람이 평소 사용하는 방식대로, 메모리에 저장된 순서를 그대로 볼 수 있어서 이해하기 쉽다.</p>
<h2 id="빅엔디안의-예">빅엔디안의 예</h2>
<p>0x12345678 라는 32bit의 숫자를 빅엔디안으로 표시해보자.</p>
<p>0x12345678는 각 4개의 byte(4bit*8=32bit)로 나뉘어 0x12, 0x34, 0x56, 0x78로 나뉘며</p>
<p>아래와 같이 저장된다.</p>
<table>
<thead>
<tr>
<th>메모리 주소</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>0000</td>
<td>0x12</td>
</tr>
<tr>
<td>0004</td>
<td>0x34</td>
</tr>
<tr>
<td>0008</td>
<td>0x56</td>
</tr>
<tr>
<td>00012</td>
<td>0x78</td>
</tr>
</tbody></table>
<h1 id="리틀엔디안이란">리틀엔디안이란?</h1>
<p>낮은 메모리 주소에 낮은 데이터 바이트 부터 저장하는 방식이다. (LSB: Least Sinificant Btye)</p>
<h2 id="리틀엔디안의-예">리틀엔디안의 예</h2>
<p>위 빅엔디안의 예처럼 0x12345678을 예로 들면 아래와 같이 저장된다.</p>
<table>
<thead>
<tr>
<th>메모리 주소</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>0000</td>
<td>0x78</td>
</tr>
<tr>
<td>0004</td>
<td>0x56</td>
</tr>
<tr>
<td>0008</td>
<td>0x34</td>
</tr>
<tr>
<td>00012</td>
<td>0x12</td>
</tr>
</tbody></table>
<h1 id="빅엔디안과-리틀엔디안의-비교">빅엔디안과 리틀엔디안의 비교</h1>
<h2 id="숫자를-계산">숫자를 계산</h2>
<p>리틀엔디안 방식은 낮은 데이터의 바이트 부터 저장하기에, 사람이 덧셈과 같은 계산을 할때와 같은 방식으로 계산할 수 있기에 리틀 엔디안이 좀더 빠르다고 할 수 있다. 왜냐하면 가장 낮은자리 수부터 계산하여 올림or내림 할 숫자가 있는지를 판단할 수 있기 때문이다.</p>
<h2 id="숫자를-비교">숫자를 비교</h2>
<p>숫자를 비교하는 부분에 있어선 가장 높은 데이터의 바이트 부터 저장하는 방식인 빅엔디안이 유리할 것이다. 높은 숫자 부터 비교해 숫자를 비교할 수 있기 때문이다.</p>
<h2 id="디버깅">디버깅</h2>
<p>또한, 보통 사람이 사용하는 방식 그대로 저장하는 빅 엔디안이 디버깅에 편할 것이다.</p>
<h2 id="엔디안은-무엇에-의해-지정될까">엔디안은 무엇에 의해 지정될까?</h2>
<p>엔디안 방식은 CPU가 어떤 엔디안을 사용하는지에 따라 달라진다.</p>
<table>
<thead>
<tr>
<th>리틀 엔디안</th>
<th>빅 엔디안</th>
</tr>
</thead>
<tbody><tr>
<td>Intel</td>
<td>IBM</td>
</tr>
<tr>
<td>AMD</td>
<td>SPARC</td>
</tr>
</tbody></table>
<p>CPU 제조사마다 엔디안을 사용하는 것이 다르다.</p>
<h3 id="일반적으로-자주-사용되는-데스크톱-cpu는-리틀엔디안이다">일반적으로 자주 사용되는 데스크톱 CPU는 리틀엔디안이다.</h3>
<p>위에 숫자를 비교하는 부분과 디버깅 부분을 보면 빅엔디안이 좀더 효율적인것 같은데,</p>
<p>왜 자주 볼 수 있는 CPU(Intel, AMD)는 리틀엔디안 일까?</p>
<p>이유는 x86(32bit)아키텍처가 리틀 엔디안을 사용하기 때문에 주로 사용되는 데스크톱용의 CPU는 Intel이나AMD인 것이다.</p>
<h3 id="네트워크-전송시의-엔디안">네트워크 전송시의 엔디안</h3>
<p>네트워크 전송시의 엔디안 방식은 빅엔디안으로 역사적으로 통일 되어있다.</p>
<p>따라서, 주로 사용되는 데스크톱은 리틀엔디안 방식이기에</p>
<p>네트워크 전송전 리틀엔디안 순서로 되어있는 메모리 순서를 → 빅엔디안 순서로 변경 후 전송하게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[구글 드라이브 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EA%B5%AC%EA%B8%80-%EB%93%9C%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EA%B5%AC%EA%B8%80-%EB%93%9C%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 14 Aug 2023 04:29:11 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>구글 드라이브, 드롭박스, 마이크로소프트의 원드라이브, 애플의 아이클라우드 등 클라우드 저장소 서비스는 높은 인기를 누리게 된 클라우드 서비스인데, 그 가운데 구글 드라이브 서비스를 설계해 보자.</p>
<blockquote>
<p>구글 드라이브는 파ㄹ 저장 및 동기화 서비스로, 문서, 사진, 비디오, 기타 파일을 클라우드에 보관할 수 있고 이 파일들은 컴퓨터, 스마트폰, 태블릿 등 어떤 단말에서도 이용 할 수 있다. 그리고 보관된 파일은 다른사람과 손쉽게 공유할 수 있다.</p>
</blockquote>
<h1 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h1>
<p>요구사항을 도출해보자.</p>
<ul>
<li>가장 중요한 것은 파일 업로드/다운로드, 파일 동기화, 알림이다.</li>
<li>모바일과 웹 앱 둘다 지원해야한다</li>
<li>파일을 암호화해야 한다</li>
<li>파일 크기 제한은 10GB다</li>
<li>일간 능동 사용자수는 천만명이다</li>
</ul>
<h2 id="개략적-추정치">개략적 추정치</h2>
<ul>
<li>가입 사용자는 오천만명이고 천만 명의 DAU 사용자가 있다</li>
<li>모든 사용자에게 10GB의 무료 저장공간을 할당</li>
<li>매일 사용자가 평균 2개의 파일을 업로드한다고 가정하고 파일의 평균 크기는 500KB다</li>
<li>읽기:쓰기 비율은 1:1</li>
<li>필요한 저장공간 총량 = 5천만 사용자 X 10GB = 500페타바이트</li>
<li>업로드 API QPS = 1천만 사용자 X 2회 업로드/24시간/3600초 = 약 240</li>
<li>최대 QPS = QPS X 2 = 480</li>
</ul>
<h1 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h1>
<p>모든 것을 담은 단일 서버로 부터 출발해 점진적으로 천만명 사용자 지원이 가능한 시스템으로 발전시켜보자.</p>
<p>우선 아래와 같은 단일 서버로 시작해보자</p>
<ul>
<li>파일을 올리고 다운로드 하는 과정을 처리할 웹 서버</li>
<li>사용자 데이터, 로그인, 파일 정보 등의 메타데이터를 보관할 데이터베이스</li>
<li>파일을 저장할 저장소 시스템: 파일 저장을 위해 1TB의 공간을 사용할 것이다.</li>
</ul>
<p>웹 서버는 아파치, 데이터베이스는 MYSQL로 설치하고, 업로드되는 파일을 저장할 drive/라는 디렉터리를 준비하자. drive/ 디렉터리 안에는 네임스페이스라 불리는 하위 디렉터리들을 둔다. 각 네임스페이스 안에는 특정 사용자가 올린 파일이 보관된다. 이 파링들은 원래 파일과 같은 이름을 갖는다. 각 파일과 폴더는 그 상대 경로를 네임스페이스 이름과 결합하면 유일하게 식별해 낼 수 있다.</p>
<p>아래 그림은 drive/ 디렉터리에 실제 파일이 보관된 그림이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/233cb267-5d0c-4345-971e-a81ecdfa8813/image.png" alt=""></p>
<h3 id="api">API</h3>
<p>이 시스템은 어떤 API들을 제공해야 할까? 기본적으로 세 가지 API가 필요한데, 파일 업로드 API, 다운로드 API, 파일 갱신 히스토리 제공 API다</p>
<h3 id="1-파일업로드-api">1. 파일업로드 API</h3>
<p>이 시스템은 두 가지 종류의 업로드를 지원한다.</p>
<ul>
<li>단순 업로드: 파일 크기가 작을 때 사용한다</li>
<li>이어 올리기: 파일 사이즈가 크고 네트워크 문제로 업로드가 중단될 가능성이 높다고 생각되면 사용한다</li>
</ul>
<p>이어 올리기 API의 예</p>
<p><a href="https://api.example.com/files/upload?uploadType=resumable">https://api.example.com/files/upload?uploadType=resumable</a></p>
<p>인자</p>
<ul>
<li>uploadType=resumable</li>
<li>data: 업로드할 로컬 파일</li>
</ul>
<p>이어 올리기는 다음 세 단계 절차로 이루어진다.</p>
<ul>
<li>이어 올리기 URL을 받기 위한 최초 요청 전송</li>
<li>데이터를 업로드하고 업로드 상태 모니터링</li>
<li>업로드에 장애가 발생하면 장애 발생시점부터 업로드를 재시작</li>
</ul>
<h3 id="2-파일-다운로드-api">2. 파일 다운로드 API</h3>
<p>API의 예</p>
<p><a href="https://api.example.com/files/download">https://api.example.com/files/download</a></p>
<p>인자</p>
<ul>
<li>path: 다운로드할 파일의 경로</li>
</ul>
<p>예</p>
<p>{</p>
<p>“path”: “/recipes/soup/best_soup.txt”</p>
<p>}</p>
<h3 id="3-파일-갱신-히스토리-api">3. 파일 갱신 히스토리 API</h3>
<p>API의 예</p>
<p><a href="https://api.exmaple.com/files/list_revisions">https://api.exmaple.com/files/list_revisions</a></p>
<p>인자</p>
<ul>
<li>path: 갱신 히스토리를 가져올 파일의 경로</li>
<li>limit: 히스토리 길이의 최대치</li>
</ul>
<p>예</p>
<p>{</p>
<p>“path”: “/recipes/soup/best_soup.txt”,</p>
<p>“limit”: 20</p>
<p>}</p>
<p>3개 나열한 API는 모든 사용자 인증을 필요로 하고 HTTPS 프로토콜을 사용해야 한다. SSL를 지원하는 프로토콜을 이용하는 이유는 클라이언트와 백엔드 서버가 주고받는 데이터를 보호하기 위한 것이다.</p>
<h2 id="한-대-서버의-제약-극복">한 대 서버의 제약 극복</h2>
<p>업로드되는 파일이 많아지다 보면 결국에는 파일 시스템은 가득 차게 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/48794b0e-96d9-4dc9-855d-3ecbc0e84ba4/image.png" alt=""></p>
<p>위 그림의 파일 시스템은 딱 10MB의 여유공간밖에는 남지 않은 상태다. 이렇게 되면 사용자는 더 이상 파일을 올릴 수 없게 되므로, 긴급히 문제를 해결해야 한다. 가장 먼저 떠오르는 해결책은 데이터를 샤딩하여 여러 서버에 나누어 저장하는 것이다. 아래그림은 user_id를 기준으로 샤딩한 예다</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/572a18c8-74b4-419a-9e1c-f92afbac29dd/image.png" alt=""></p>
<p>위 그림처럼 데이터베이스를 샤딩 해두어도 <strong>서버에 장애가 생기면</strong> 데이터를 잃게 되지 않을까 여전히 걱정할 것이다. 이의 해결책은 아마존S3를 사용하는것이 가장 적합할 것이다. 아마존 S3는 업계 최고 수준의 <strong>규모 확장성, 가용성, 보안, 성능</strong>을 제공하는 객체 저장소 서비스다.</p>
<p>S3는 다중화를 지원하는데, 같은 지역 안에서 다중화를 할 수도 있고 여러 지역에 걸쳐 다중화를 할 수도 있다. AWS 서비스 지역은 아마존 AWS가 데이터 센터를 운영하는 지리적 영역이다. 아래 그림에 나오듯이, 데이터를 다중화 할 때는 같은 지역 안에서만 할 수도 있고 여러 지역에 걸쳐 할 수도 있다. 여러 지역에 걸쳐 다중화하면 데이터 손실을 막고 가용성을 최대한 보장할 수 있으므로 그렇게 하기로 하고, S3 버킷은 마치 파일 시스템의 폴더와도 같은 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/98904424-6fac-44c9-bec9-0a71b3706a81/image.png" alt=""></p>
<p>파일을 S3에 넣고 나니 이제 데이터 손실 걱정은 사라졌다. 미래에 비슷한 문제가 벌어지는 것을 막기 위해, 개선할 부분들을 살펴보자.</p>
<ul>
<li>로드밸런서: 네트워크 트래픽을 분산하기 위해 로드밸런서를 사용한다. 트래픽을 고르게 분산할 수 있게 해주는 장치이고, 특정 웹 서버에 장애가 발생하면 자동으로 해당 서버를 <strong>우회</strong>해준다</li>
<li>웹 서버: 로드밸런서를 추가하고 나면 더 많은 웹 서버를 손쉽게 추가할 수 있다. 따라서 트래픽이 폭증해도 쉽게 대응이 가능하다.</li>
<li>메타데이터 데이터베이스: 데이터베이스를 파일 저장 서버에서 분리하여 SPOF를 회피한다. 아울러 다중화 및 샤딩 정책을 적용하여 가용성과 규모 확장성 요구사항에 대응한다.</li>
<li>파일 저장소: S3를 파일 저장소로 사용하고 가용성과 데이터 무손실을 보장하기 위해 두 개 이상의 지역에 데이터를 다중화한다.</li>
</ul>
<p>이 모든 부분을 개선하고 나면 웹 서버, 메타데이터 데이터베이스, 파일 저장소가 한 대 서버에서 여러 서버로 잘 분리 되었을 것이다. 아래 그림은 수정한 설계안이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/83dc2223-e9ad-4dd1-aaf9-eabb0e838c9e/image.png" alt=""></p>
<h2 id="동기화-충돌">동기화 충돌</h2>
<p>구글 드라이브 같은 대형 시스템의 경우 때때로 동기화 충돌이 발생할 수 있다. 두 명 이상의 사용자가 같은 파일이나 폴더를 동시에 업데이트하려고 하는 경우다. 이런 충돌은 어떻게 해소할 수 있을까? 먼저 처리되는 변경은 성공한 것으로 보고, 나중에 처리되는 변경은 충돌이 발생한 것으로 표시하면 해소할 수 있을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/af1c513e-f29b-498f-8e19-344060432d76/image.png" alt=""></p>
<p>위 그림에서 사용자 1과 2는 같은 파일을 동시에 갱신하려 한다. 하지만 이 시스템은 사용자 1의 파일을 먼저 처리했다. 따라서 사용자 1의 파일 갱신 시도는 정상적으로 처리되었지만 사용자 2에 대해서는 동기화 충돌 오류가 발생할 것이다. 이 오류는 어떻게 해결할까? 오류가 발생한 시점에 이 시스템에는 같은 파일의 두 가지 버전이 존재하게 된다. 즉, 사용자 2가 가지고 있는 로컬 사본과 서버에 있는 최신 버전이다. 이 상태에서 사용자는 두 파일을 하나로 합칠지 아니면 둘 중 하나를 다른 파일로 대체할지를 결정하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c1dae558-12b6-4316-b1d5-fc420cf72fff/image.png" alt=""></p>
<p>여러 사용자가 같은 문서를 편집할 때 발생할 수 있는 동기화 문제를 해결하는 것은 흥미로운 과제다.</p>
<h2 id="개략적-설계안">개략적 설계안</h2>
<p>아래그림은 이번 면접 문제에 대한 개략적 설계안이다. 지금부터 각각의 컴포넌트에 대해 조금 더 상세히 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/cbc616d5-7385-4c79-8509-78744beab8fc/image.png" alt=""></p>
<p>사용자 단말: 사용자가 이용하는 웹브라우저나 모바일 앱 등의 클라이언트.</p>
<p>블록 저장소 서버: 파일 블록을 클라우드 저장소에 업로드하는 서버다. 블록 저장소는 블록 수준 저장소라고도 하며, 클라우드 환경에서 데이터 파일을 저장하는 기술이다. 이 저장소는 파일을 여러개의 블록으로 나눠 저장하며, 각 블록에는 고유한 해시값이 할당된다. 이 해시값은 메타데이터 데이터베이스에 저장된다. 각 블록은 독립적인 객체로 취급되며 클라우드 저장소 시스템에 보관된다. 파일을 재구성하려면 블록들을 원래 순서대로 합쳐야 한다. 예시한 설계안의 경우 한 블록은 드롭박스의 사례를 참고하여 4MB로 정했다.</p>
<ul>
<li>클라우드 저장소: 파일은 블록 단위로 나눠져 클라우드 저장소에 보관된다</li>
<li>아카이빙 저장소: 오랫동안 사용되지 않은 비활성 데이터를 저장하기 위한 시스템ㅁ</li>
<li>로드밸런서: 요청을 모든 API서버에 고르게 분산하는 시스템</li>
<li>API 서버: 파일 업로드 외에 거의 모든 것을 담당하는 서버. 사용자 인증, 프로필 관리, 파일 메타데이터 갱신 등에 사용</li>
<li>메타데이터 데이터베이스: 성능을 높이기 위해 자주 쓰이는 메타데이터는 캐시한다.</li>
<li>알림 서비스: 특정 이벤트가 발생했음을 클라이언트에 알리는데 쓰이는 발생/구독 프로토콜 기반 시스템. 예시 설계안의 경우 클라이언트에게 파일이 추가되었거나, 편집되었거나, 삭제되었음을 알려, 파일의 최신 상태를 확인하도록 하는 데 쓰인다.</li>
<li>오프라인 사용자 백업 큐: 클라이언트가 접속 중이 아니라서 파일의 최신 상태를 확인할 수 없을 때는 해당 정보를 큐에 두어 나중에 클라이언트가 접속했을 때 동기화될 수 있도록 한다.</li>
</ul>
<h1 id="3단계-상세-설계">3단계 상세 설계</h1>
<p>블록 저장소 서버, 메타데이터 데이터베이스, 업로드 절차, 다운로드 절차, 알림 서비스, 파일 저장소 공간 및 장애 처리 흐름에 대해 좀 더 자세히 알아보자.</p>
<h2 id="블록-저장소-서버">블록 저장소 서버</h2>
<p>정기적으로 갱신되는 큰 파일들은 업데이트가 일어날 때마다 전체 파일을 서버로 보내면 네트워크 대역폭을 많이 잡아먹는다. 이를 최적화 할때 사용할 수 있는 두 가지 방법을 알아보자</p>
<ul>
<li>델타 동기화: 파일이 수정되면 전체 파일 대신 수정이 일어난 블록만 동기화 하는것</li>
<li>압축: 블록 단위로 압축해 두면 데이터 크기를 많이 줄일 수 있다. 이때 압축 알고리즘은 파일 유형에 따라 정한다. 예를 들어 텍스트 파일은 gzip이나 bzip2를 쓰고, 이미지나 비디오를 압축할 때는 다른 압축 알고리즘을 쓰는 것이다.</li>
</ul>
<p>이 시스템에서 블록 저장소 서버는 파일 업로드에 관계된 힘든 일을 처리하는 컴포넌트다. 클라이언트가 보낸 파일을 블록 단위로 나누고, 각 블록에 압축 알고리즘을 적용해야 하고, 암호화까지 해야한다. 아울러 전체 파일을 저장소 시스템으로 보내는 대신 수정된 블록만 전송해야 한다.</p>
<p>새 파일이 추가되었을 때 블록 저장소 서버가 어떻게 동작하는지는 아래 그림에 나와 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/772eef5d-2b61-4563-bc17-fe1f63b6c2f1/image.png" alt=""></p>
<ul>
<li>주어진 파일을 작은 블록들로 나눈다</li>
<li>각 블록을 압축한다</li>
<li>클라우드 저장소에 보내기전에 암호화한다</li>
<li>클라우드 저장소로 보낸다</li>
</ul>
<p>아래 그림은 델타 동기화 전략이 어떻게 동작하는지를 보여준다. 검정색 블록은 수정된 블록이다. 갱신된 부분만 동기화해야 하므로 이 두 블록만 클라우드 저장소에 업로드하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/0dd3efd6-f939-4cc4-bf1f-db76fa6db6f6/image.png" alt=""></p>
<p>블록 저장소 서버에 델타 동기화 전략과 압축 알고리즘을 도입하였으므로, 네트워크 대역폭 사용량을 감소할 수 있다.</p>
<h2 id="높은-일관성-요구사항">높은 일관성 요구사항</h2>
<p>이 시스템은 강한 일관성 모델을 기본으로 지원해야 한다. 같은 파일이 단말에 따라 다르게 보이는 것은 허용할 수 없다는 뜻이다. 메타데이터 캐시와 데이터베이스 계층에도 같은 원칙이 적용되어야 한다.</p>
<p>메모리 캐시는 보통 최종 일관성 모델을 지원한다. 따라서 강한 일관성을 달성하려면 다음 사항을 보장해야 한다.</p>
<ul>
<li>캐시에 보관된 사본과 데이터베이스에 있는 원본이 일치한다.</li>
<li>데이터베이스에 보관된 원본에 변경이 발생하면 캐시에 있는 사본을 무효화한다</li>
</ul>
<p>관계형 데이터베이스는 ACID를 보장하므로 강한 일관성을 보장하기 쉽다. 하지만 NoSQL 데이터베이스는 이를 기본으로 지원하지 않으므로, 동기화 로직 안에 프로그램해 넣어야 한다.</p>
<h2 id="메타데이터-데이터베이스">메타데이터 데이터베이스</h2>
<p>아래그림은 이 데이터베이스의 스키마 설계안이다. 중요한 것만 간추린, 아주 단순화된 형태의 스키마임에 유의하자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/f193cb4f-8c46-4a0d-a4a8-58c2a89212d2/image.png" alt=""></p>
<p>user: user 테이블에는 이름, 이메일, 프로파일 사진 등 사용자에 관계된 기본적 정보들이 보관된다</p>
<p>device: device 테이블에는 단말 정보가 보관된다. push_id는 모바일 푸시 알림을 보내고 받기 위한 것이다. 한 사용자가 여러 대의 단말을 가질 수 있음에 유의해야 한다</p>
<p>namespace: namespace 테이블에는 사용자의 루트 디렉터리 정보가 보관된다.</p>
<p>file: file 테이블에는 파일의 최신 정보가 보관된다.</p>
<p>file_version: 파일의 갱신 이력이 보관되는 테이블. 이 테이블에 보관되는 레코드는 전부 읽기 전용이다. 갱신 이력이 훼손되는 것을 막기 위한 조치</p>
<p>block: 파일 블록에 대한 정보를 보관하는 테이블. 특정 버전의 파일은 파일 블록을 올바른 순서로 조합하기만 하면 복원 가능</p>
<h2 id="업로드-절차">업로드 절차</h2>
<p>사용자가 파일을 업로드하면 무슨 일이 벌어지는지 상세하게 알아보자.</p>
<p>첫 번째 요청은 파일 메타데이터를 추가하는 것이고, 두 번째 요청은 파일을 클라우드 저장소로 업로드하기 위한 요청이다. 이 두 요청은 전부 클라이언트 1이 보낸 것이다.</p>
<ul>
<li>파일 메타데이터 추가</li>
</ul>
<ol>
<li>클라이언트 1이 새 파일의 메타데이터를 추가하기 위한 요청 전송</li>
<li>새 파일의 메타데이터를 데이터베이스에 저장하고 업로드 상태를 대기중으로 변경</li>
<li>새 파일이 추가되었음을 알림 서비스에 통지</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/6d683aee-4911-4d73-abe2-e1956406f63a/image.png" alt=""></p>
<ol start="4">
<li>알림 서비스는 관련된 클라이언트에게 파일이 업로드 되고 있음을 알림</li>
</ol>
<ul>
<li>파일을 클라우드 저장소에 업로드</li>
</ul>
<ol>
<li>클라이언트1이 파일을 블록 저장소 서버에 업로드</li>
<li>블록 저장소 서버는 파일을 블록 단위로 쪼갠 다음 압축하고 암호화 한 후 클라우드 저장소에 전송</li>
<li>업로드가 끝나면 클라우드 스토리지는 완료 콜백을 호출. 이 콜백 호출은 API 서버로 전송됨</li>
<li>메타데이터 DB에 기록된 해당 파일의 상태를 완료로 변경</li>
<li>알림 서브에 파일 업로드가 끝났음을 통지</li>
<li>알림 서비스는 관련된 클라이언트2에게 파일 업로드가 끝났음을 알림</li>
</ol>
<p>파일을 수정하는 경우에도 흐름은 비슷하다.</p>
<h2 id="다운로드-절차">다운로드 절차</h2>
<p>파일 다운로드는 팡리이 새로 추가되거나 편집되면 자동으로 시작된다. 그렇다면 클라이언트는 다른 클라이언트가 파일을 편집하거나 추가했다는 사실을 어떻게 감지할까? 두 가지 방법이 있다.</p>
<ul>
<li>클라이언트 A가 접속 중이고 다른 클라이언트가 파일을 변경하면 알림 서비스가 클라이언트 A에게 변경이 발생했으니 새 버전을 끌어가야 한다고 알린다.</li>
<li>클라이언트 A가 네트워크에 연결된 상태가 아닐 경우에는 데이터는 캐시에 보관될 것이다. 해당 클라이언트의 상태가 접속 중으로 바뀌면 그때 해당 클라이언트는 새 버전을 가져갈 것이다.</li>
</ul>
<p>어떤 파일이 변경되었음을 감지한 클라이언트는 우선 API 서버를 통해 메타데이터를 새로 가져가야 하고, 그 다음에 블록들을 다운받아 파일을 재구성해야 한다. 아래 그림은 자세한 흐름을 보여준다. 지면 한계상 가장 중요한 컴포넌트들만 그렸음을 유의하자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/5fef5334-cb4e-41de-8c7e-936981bf636a/image.png" alt=""></p>
<ol>
<li>알림 서비스가 클라이언트 2에게 누군가 파일을 변경했음을 알림</li>
<li>알림을 확인한 클라이언트 2는 새로운 메타데이터를 요청</li>
<li>API 서버는 메타데이터 데이터베이스에게 새 메타데이터 요청</li>
<li>API 서버에게 새 메타데이터가 반환됨</li>
<li>클라이언트 2에게 새 메타데이터가 반환됨</li>
<li>클라이언트 2는 새 메타데이터를 받는 즉시 블록 다운로드 요청 전송</li>
<li>블록 저장소 서버는 클라우드 저장소에서 블록 다운로드</li>
<li>클라우드 저장소는 블록 서버에게 요청된 블록 반환</li>
<li>블록 저장소 서버는 클라이언트에게 요청된 블록 반환. 클라이언트 2는 전송된 블록을 사용하여 파일 재구성</li>
</ol>
<h2 id="알림-서비스">알림 서비스</h2>
<p>파일의 일관성을 유지하기 위해, 클라이언트는 로컬에서 파일이 수정되었음을 감지하는 순간 다른 클라이언트에게 그 사실을 알려서 충돌 가능성을 줄여야 한다. 알림 서비스는 그 목적으로 이용된다. 단순하게 보자면 알림 서비스는 이벤트 데이터를 클라이언트로 내는 서비스다. 따라서 다음 두가지 정도의 선택지가 있다.</p>
<ul>
<li>롱 폴링: 드롭박스가 이 방식을 채택하고 있다.</li>
<li>웹 소켓: 클라이언트와 서버 사이에 지속적인 통신 채널을 제공한다. 따라서 양방향 통신이 가능하다.</li>
</ul>
<p>둘 다 좋은 방안이지만 본 설계안의 경우에는 롱 폴링을 사용할 것인데 이유는 다음과 같다.</p>
<ul>
<li>채팅 서비스와 달리, 본 시스템의 경우 알림 서비스와 양방향 통신이 필요하지 않다. 서버는 파일이 변경된 사실을 클라이언트에게 알려주어야 하지만 반대 방향의 통신은 요구되지 않는다.</li>
<li>웹소켓은 실시간 양방향 통신이 요구되는 <strong>채팅 서비스</strong>에 적합하다. 구글 드라이브의 경우 알림을 보낼 일은 그렇게 자주 발생하지 않으며, 알림을 보내야 하는 경우에도 단시간에 많은 양의 데이터를 보낼일은 없다.</li>
</ul>
<p>롱 폴링 방안을 쓰게 되면 각 클라이언트는 알림 서버와 롱 폴링용 연결을 유지하다가 특정 파일에 대한 변경을 감지하면 해당 연결을 끊는다. 이때 클라이언트는 반드시 메타데이터 서버와 연결해 파일의 최신 내역을 다운로드 해야한다. 해당 다운로드 작업이 끝났거나 연결 타임아웃 시간에 도달한 경우에는 즉시 새 요청을 보내어 롱 폴링 연결을 복원하고 유지해야 한다.</p>
<h2 id="저장소-공간-절약">저장소 공간 절약</h2>
<p>파일 갱신 이력을 보존하고 안정성을 보장하기 위해서는 파일 여러 버전을 여러 데이터센터에 보관할 필요가 있다. 그런 상황에서 모든 버전을 자주 백업하게 되면 저장용량이 너무 빨리 소진될 가능성이 있다. 이런 문제를 피하고 비용을 절감하기 위해서는 보통 아래 세 가지 방법을 사용한다.</p>
<ul>
<li>중복 제거: 중복된 파일 블록을 계정 차원에서 제거하는 방법이다. 두 블록이 같은 블록인지는 해시 값을 비교하여 판단한다</li>
<li>지능적 백업 전략을 도입한다. 다음과 같은 전략을 생각해 볼 수 있다<ul>
<li>한도 설정: 보관해야 하는 파일 버전 개수에 상한을 두는 것. 상한에 도달하면 제일 오래된 버전은 버린다</li>
<li>중요한 버전만 보관: 어떤 파일은 아주 자주 바뀐다. 예를 들어 편집 중인 문서가 업데이트될 때마다 새로운 버전으로 관리한다면 짧은 시간 동안 1000개가 넘는 버전이 만들어질 수 있다. 불필요한 버전과 사본이 만들어지는 것을 피하려면 그 가운데 중요한 것만 골라내면 된다.</li>
</ul>
</li>
<li>자주 쓰이지 않는 데이터는 아카이빙 저장소로 옮긴다. 몇달 혹은 수년간 이용되지 않은 데이터가 이에 해당한다. 아마존 S3 글래시어 같은 아키이빙 저장소 이용료는 S3보다 훨씬 저렴하다.</li>
</ul>
<h2 id="장애-처리">장애 처리</h2>
<p>장애는 대규모 시스템이라면 피할 수 없는 것이다. 설계 시 이 점을 반드시 고려해야 한다.</p>
<ul>
<li>로드밸런서 장애: 로드밸런서에 장애가 발생할 경우 부 로드밸런서가 활성화되어 트래픽을 이어받아야 한다. 로드 밸런서끼리는 보통 <strong>박동 신호</strong>를 주기적으로 보내어 상태를 모니터링한다. 일정 시간 동안 박동 신호에 응답하지 않은 로드밸런서는 장애가 발생한 것으로 간주한다.</li>
<li>블록 저장소 서버 장애: 블록 저장소 서버에 장애가 발생하였다면 다른 서버가 미완료 상태 또는 대기 상태인 작어블 이어받아야 한다.</li>
<li>클라우드 저장소 장애: S3 버킷은 여러 지역에 다중화할 수 있으므로, 한 지역에서 장애가 발생하였다면 다른 지역에서 파일을 가져오면 된다.</li>
<li>API 서버 장애: API 서벋르은 <strong>무상태 서버</strong>다. 따라서 로드밸런서는 API서버에 장애가 발생하면 트래픽을 해당 서버로 보내지 않음으로써 장애 서버를 격리 한다.</li>
<li>메타데이터 캐시 장애: 메타데이터 캐시 서버도 다중화한다. 따라서 한 노드에 장애가 생겨도 다른 노드에서 데이터를 가져올 수 있다. 장애가 발생한 서버는 새 서버로 교체하면 된다.</li>
<li>메타데이터 데이터베이스 장애<ul>
<li>주 데이터베이스 서버 장애: 부 데이터베이스 서버 가운데 하나를 데이터베이스 서버로 바꾸고, 부 데이터베이스 서버를 새로 하나 추가한다.</li>
<li>부 데이터베이스 서버 장애: 다른 부 데이터베이스 서버가 읽기 연산을 처리하도록 하고 그동안 장애 서버는 새 것으로 교체한다.</li>
</ul>
</li>
<li>알림 서비스 장애: 접속 중인 모든 사용자는 알림 서버와 롱 폴링 연결을 하나씩 유지한다. 따라서 알림 서비스는 많은 사용자와의 연결을 유지하고 관리해야 한다. 2012년도에 있었던 드롭박스 행사의 발표자료에 따르면, 한 대의 드롭박스 알림 서비스 서버가 관리하는 연결의 수는 1백만 개가 넘는다. 따라서 한 대 서버에 장애가 발생하면 백만 명 이상의 사용자가 롱폴링 연결을 다시 만들어야 한다. 주의할 것은 한 대 서버로 백만 개 이상의 접속을 유지하는 것은 가능하지만, 동시에 백만 개 접속을 ‘시작’하는 것은 불가능하다는 점이다. 따라서 롱 폴링 연결을 복구하는 것은 상대적으로 느릴 수 있다.</li>
<li>오프라인 사용자 백업 큐 장애: 이 큐 또한 다중화해 두어야 한다. 큐에 장애가 발생하면 구독 중인 클라이언트들은 백업 큐로 구독 관계를 재설정해야 한다.</li>
</ul>
<h1 id="4단계-마무리">4단계 마무리</h1>
<p>높은 수준의 일관성, 낮은 네트워크 지연, 그리고 빠른 동기화가 요구되는 구글 드라이브 시스템을 설계해 보았다</p>
<h2 id="지금까지-제시한-설계안-말고-다른-선택지가-있는지-고민해보자">지금까지 제시한 설계안 말고, 다른 선택지가 있는지 고민해보자.</h2>
<h3 id="블록-저장소-서버를-거치지-않고-파일을-클라우드-저장소-서버에-직접-업로드한다면">블록 저장소 서버를 거치지 않고 파일을 클라우드 저장소 서버에 직접 업로드한다면?</h3>
<ul>
<li>분할, 압축, 암호화 로직을 클라이언트에 두어야 하므로 플랫폼별로 따로 구현해야 한다(iOS, 안드로이드, 웹 등). 당초 설계안에서는 이 모두를 블록 저장소 서버라는 곳에 모아 뒀으므로 그럴 필요가 없다.</li>
<li>클라이언트가 해킹 당할 가능성이 있으므로 암호화 로직을 클라이언트에 두는 것은 적절치 않은 선택일 수 있다.</li>
</ul>
<h3 id="접속상태-관리-로직을-별도-서비스로-옮기기">접속상태 관리 로직을 별도 서비스로 옮기기</h3>
<ul>
<li>접속상태 관리 로직을 <strong>알림 서비스에서 분리</strong>하면 다른 서비스에서도 쉽게 활용이 가능해 질것이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[유튜브 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EC%9C%A0%ED%8A%9C%EB%B8%8C-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EC%9C%A0%ED%8A%9C%EB%B8%8C-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Fri, 11 Aug 2023 04:29:49 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>유튜브(넷플릭스)와 같은 플랫폼을 설계하는 방법을 알아보자.</p>
<p>유튜브 시스템은 겉 보기에는 간단하다. 콘텐츠 창작자가 비디오를 올리고, 사용자는 재생 버튼을 누르면 영상이 재생된다. 하지만 이게 그렇게 간단할까? 이 겉보기에 단순한 시스템은 엄청나게 복잡한 수많은 기술들이 숨어있다. 유튜브에 대한 통계자료 몇 가지를 살펴보자.</p>
<ul>
<li>월간 능동 사용자 수: 2십 억</li>
<li>매일 재생되는 비디오 수: 5십 억</li>
<li>미국 성인 가운데 73%가 유튜브 이용</li>
<li>5천만명의 창작자</li>
<li>유튜브의 광고 수입은 2019년 기준으로 150억 달러이며 이는 2018년도 대비 36%가 증가한 수치</li>
<li>모바일 인터넷 트래픽 가운데 37%를 유튜브가 점유</li>
<li>80개 언어로 이용 가능</li>
</ul>
<h1 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h1>
<p>유튜브에서는 단순히 비디오를 보는 것 말고도 많은 일을 할 수 있다. 댓글을 남길 수도 있고, 비디오를 공유하거나 좋아요 버튼을 누를 수도 있고, 자기 재생목록에 저장을 할 수도 있고, 채널을 골라 구독할 수 도 있다. 이 모두를 설계하는 것 보다는, 적절한 요구사항을 도출해 설계해보자.</p>
<h3 id="요구사항-도출">요구사항 도출</h3>
<ul>
<li>영상을 올리는 것이 가장 중요한 기능이다</li>
<li>모바일 앱, 웹 브라우저, 스마트 TV를 지원해야 한다</li>
<li>일간 능동 사용자 수는 5백만명 이다</li>
<li>5백만명이 평균적으로 30분씩 사용한다</li>
<li>다국어 지원이 필요하다</li>
<li>현존하는 비디오의 종류와 해상도를 대부분 지원해야한다</li>
<li>암호화가 필요하다</li>
<li>영상의 크기는 최대 1GB이다</li>
<li>다른 제 3자의 서비스(클라우드)와 같은 것을 활용해 만들어도 된다.</li>
</ul>
<h3 id="개략적-규모-측정">개략적 규모 측정</h3>
<ul>
<li>일간 능동 사용자 DAU(Daily Active User)수는 5백만명</li>
<li>한 사용자는 하루 평균 5개의 영상 시청</li>
<li>10%의 사용자가 하루에 1영상 업로드</li>
<li>영상 평균 크기는 300MB</li>
<li>영상 저장을 위해 매일 새로 요구되는 저장 용량 = 5백만X10%X300MB=150TB</li>
<li>CDN<ul>
<li>클라우드 CDN을 통해 영상을 서비스할 경우 CDN에서 나가는 데이터의 양에 따라 과금</li>
<li>아마존의 클라우드프론트를 CDN 솔루션으로 사용할 경우 100% 트래픽이 미국에서 발생한다고 가정하면 1GB당 $0.02의 요금이 발생. 문제를 단순화하기 위해 영상스트리밍 비용만 따진다.</li>
<li>따라서 매일 발생하는 요금은 5백만 * 5영상 * 0.3GB * $0.02=$150,000</li>
</ul>
</li>
</ul>
<p>위 추정 결과에 따르면 CDN을 통해 영상을 서비스하면 비용이 엄청나다. 클라우드 서비스가 서비스 사업자가 큰 고객에게 비용 할인을 해주는 점을 감안하더라도 만만찮은 비용이기에 이 비용을 줄이는 방법에 대해서는 상세 설계때 이야기 해보겠다.</p>
<h1 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h1>
<p>개략적 설계안에서는 제3자 서비스 (CDN과 BLOB 스토리지)를 사용하여 개략적 설계안을 작성할 것이다. 왜 직접 만들지 않고 제3자가 제공해주는 서비스를 이용하는지에 대한 이유는 아래와 같다.</p>
<aside>
💡 규모 확장이 쉬운 BLOB이나 CDN을 만드는 것은 지극히 복잡할 뿐 아니라 많은 비용이 든다. 넷플릭스나 페이스북 같은 큰 회사도 모든 서비스를 스스로 구축하지는 않는다. 넷플릭스의 아마존 클라우드 서비스를 사용하고, 페이스북은 아카마이의 CDN을 이용한다

</aside>

<p>개략적으로 보면 이 시스템은 세 개의 컴포넌트로 구성된다</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/47805bb8-d359-4768-9301-158df8c9debe/image.png" alt=""></p>
<ul>
<li>단말: 컴퓨터, 모바일 폰, 스마트 TV를 통해 영상을 시청할 수 있다</li>
<li>CDN: 비디오는 CDN에 저장하고 재생 버튼을 누르면 CDN으로부터 스트리밍이 이루어진다</li>
<li>API서버: 영상 스트리밍을 제외한 모든 요청은 API 서버가 처리한다. 피드 추천, 비디오 업로드 URL 생성, 메다데이터 데이터베이스와 캐시 갱신, 사용자 가입 등등이 API서버가 처리하는 작업이다</li>
</ul>
<p>자 이제 핵심적인 기능 두가지인 <strong>비디오 업로드 절차, 비디오 스트리밍 절차</strong>를 개략적으로 설계해보자</p>
<h2 id="비디오-업로드-절차">비디오 업로드 절차</h2>
<p>아래 그림은 비디오 업로드 절차의 개략적 설계안이다.</p>
<p>이 설계안은 다음의 컴포넌트들로 구성되어 있다.</p>
<ul>
<li>사용자: 컴퓨터나 모바일 폰, 혹은 스마트 TV를 통해 유튜브를 시청하는 이용자다.</li>
<li>로드밸런서: API 서버 각각으로 고르게 요청을 분산하는 역할을 담당</li>
<li>API 서버: 비디오 스트리밍을 제외한 다른 모든 요청을 처리</li>
<li>메타데이터 데이터베이스: 비디오의 메타데이터를 보관하고 샤딩과 다중화를 적용하여 성능 및 가용성을 충족한다</li>
<li>메타데이터 캐시: 성능을 높이기 위해 비디오 메타데이터와 사용자 객체는 캐시한다.</li>
<li>원본 저장소: 원본 비디오를 보관할 대형 이진 파일 저장소(BLOB)시스템이다.</li>
</ul>
<aside>
💡 BLOB저장소는 “이진 데이터를 하나의 개체로 보관하는 데이터베이스 관리 시스템”이다.

</aside>

<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/17286a2c-8c28-4f29-9136-14d71784e3d6/image.png" alt=""></p>
<ul>
<li>트랜스코딩 서버: 영상 트랜스코딩은 비디오 인코딩이라 부릑도 하는 절차로, 비디오의 포맷을 변환하는 절차다. 단말이나 대역폭 요구사항에 맞는 최적의 영상 스트림을 제공하기 위해 필요하다</li>
<li>트랜스코딩 비디오 저장소: 트랜스코딩이 완료된 영상을 저장하는 BLOB 저장소다.</li>
<li>CDN: 영상을 캐시하는 역할을 답당한다. 사용자가 재생 버튼을 누르면 영상 스트리밍은 CDN을 통해 이루어진다</li>
<li>트랜스코딩 완료 큐: 영상 트랜스코딩 완료 이벤트들을 보관할 메시지 큐다</li>
<li>트랜스코딩 완료 핸들러: 트랜스코딩 완료 큐에서 이벤트 데이터를 꺼내어 메타데이터 캐시와 데이터베이스를 갱신할 작업 서버들이다.</li>
</ul>
<p>각 컴포넌트가 무슨 일을 하는지느 살펴보았으니, 영상 업로드가 어떻게 처리되는지를 들여다보자. 다음의 두 프로세스가 병렬적으로 수행된다고 보면 된다.</p>
<ol>
<li>영상 업로드</li>
<li>영상 메타데이터 갱신. 메타데이터에는 영상 URL, 크기, 해상도, 포맷, 사용자 정보가 포함된다.</li>
</ol>
<h3 id="프로세스-a-영상-업로드">프로세스 a: 영상 업로드</h3>
<p>아래 그림은 영상 업로드가 어떻게 이루어지는지를 보여준다. 요약하면 아래와 같다.</p>
<ol>
<li>영상을 원본 저장소에 업로드한다.</li>
<li>트랜스코딩 서버는 원본 저장소에서 해당 영상을 가져와 트랜스코딩을 시작한다.</li>
<li>트랜스코딩이 완료되면 아래 두 절차가 <strong>병렬적</strong>으로 수행된다.<ol>
<li>완료된 영상을 트랜스코딩 영상 저장소로 업로드</li>
<li>트랜스코딩 완료 이벤트를 트랜스코딩 완료 큐에 넣는다<ol>
<li>트랜스코딩이 끝난 비디오를 CDN에 올린다</li>
<li>완료 핸들러가 이벤트 데이터를 큐에서 꺼낸다</li>
<li>완료 핸들러가 메타데이터 데이터베이스와 캐시를 갱신한다.</li>
</ol>
</li>
</ol>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/fa54cfa5-7965-4c8c-817a-6de1eef63413/image.png" alt=""></p>
<ol start="4">
<li>API 서버가 단말에게 영상 업로드가 끝나서 스트리밍 준비가 되었음을 알린다.</li>
</ol>
<h3 id="프로세스-b-메타데이터-갱신">프로세스 b: 메타데이터 갱신</h3>
<p>원본 저장소에 파일이 업로드되는 동안, 단말은 <strong>병렬적</strong>으로 영상 메타데이터 갱신 요청을 API서버에 보낸다. 이 요청에 포함된 메타데이터에는 파일 이름, 크기, 포맷 등의 정보가 들어 있다. API서버는 이 정보로 메타데이터 캐시와 데이터베이스를 업데이트한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/5c2b8a01-f7d2-48eb-93ff-33d52e2589ef/image.png" alt=""></p>
<h3 id="비디오-스트리밍-절차">비디오 스트리밍 절차</h3>
<p>유튜브에서 영상 재생버튼을 누르면 스트리밍은 바로 시작되며 영상 다운로드가 완료디어야 영상을 볼 수 있다거나 하는 불편함은 없다. 여기서 다운로드라 함은 영상을 단말로 내려 받는 것을 말하며, 스트리밍은 여러분의 장치가 원격지의 영상으로부터 지속적으로 영상 스트림을 전송 받아 영상을 재생하는 것을 말한다.</p>
<p>글너데 영상 스트리밍이 이루어지는 절차를 논하기에 앞서, 우리는 먼저 스트리밍 프로토콜이라는 중요한 개념을 알아두어야 한다.</p>
<aside>
💡 스트리밍 프로토콜이란?
영상 스트리밍을 위해 데이터를 전송할 때 쓰이는 표준화된 통신방법이다. 널리 사용되는 스트리밍 프로토콜로는 다음과 같은 것이 있다.

</aside>

<ul>
<li>MPEG-DASH. MPEG는 “Moving Picture Experts Group”의 약자이며, DASH는 “Dynamic Adaptive Streaming over HTTP”의 약어이다.</li>
<li>애플 HLS. HLS는 “HTTP Live Streaming”의 약어이다.</li>
<li>마이크로소프트 스무스 스트리밍(Microsoft Smooth STreaming).</li>
<li>어도비 HTTP 동적 스트리밍(Adobe HTTP Dynamic Streaming, HDS).</li>
</ul>
<p>이 프로토콜의 동작 원리를 정확하게 이해하거나 이름들을 외울 필요는 없다. 중요한 점은, <strong>프로토콜마다 지원하는 영상 인코딩이 다르고 플레이어도 다르다는 것</strong>이다. 따라서 영상 스트리밍 서비스를 설계할 때는 <strong>서비스의 용례에 맞는 프로토콜을 잘 골라</strong>야한다.</p>
<p>영상은 CDN에서 바로 스트리밍된다. 사용자의 단말에 가장 가까운 CDN 에지 서버가 영상 전송을 담당할 것이다. 따라서 전송시간은 아주 낮다. 아래그림은 이 부분의 개략적 설계안이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/2051948c-6edd-4146-b0ae-e244b5285021/image.png" alt=""></p>
<h1 id="3단계-상세설계">3단계 상세설계</h1>
<p>지금까지 살펴본 개략적 설계안에서는 전체 시스템을 두 부분, 즉 영상 업로드를 담당하는 부분과 영상 스트리밍을 담당하는 부분으로 나눠 살펴봤다. 상세 설계에서는 최적화 방안과 함께 좀 더 상세히 다듬고 오류 처리 매커니즘에 대해서도 알아보자.</p>
<h2 id="비디오-트랜스코딩">비디오 트랜스코딩</h2>
<p>영상을 녹화하면 해당 영상을 특정 포맷으로 저장한다. 이 영상은 다른 단말에서도 순조롭게 재생되려면 다른 단말과 호환되는 <strong>비트레이트</strong>와 포맷으로 저장되어야 한다.</p>
<aside>
💡 비트레이트란?
영상을 구성하는 비트가 얼마나 빨리 처리되어야 하는지를 나타내는 단위다. 비트레이트가 높은 비디오는 일반적으로 고화질 비디오이다.

</aside>

<p>비트레이트가 높은 비디오 스트림을 정상 재생하려하면 보다 높은 성능의 컴퓨팅 파워가 필요하고, 인터넷 회선 속도도 빨라야 한다.</p>
<p>비디오 트랜스코딩은 다음과 같은 이유로 중요하다.</p>
<ul>
<li>가공되지 않은 원본 영상은 저장 공간을 많이 차지한다. 가령 초당 60프레임으로 녹화된 HD 영상은 수백 GB의 저장공간을 차지하게 될 수 있다.</li>
<li>상당수의 단말과 브라우저는 특정 종류의 비디오 포맷만 지원한다. 따라서 호환성 문제를 해결하려면 하나의 영상을 여러 포맷으로 인코딩해 두는 것이 바람직하다.</li>
<li>사용자에게 끊김 없는 고화질 영상 재생을 보장하려면, 네트워크 대역폭이 충분하지 않은 사용자에게는 저화질 영상을, 대역폭이 충분한 사용자에게는 영상을 보내는 것이 바람직하다.</li>
<li>모바일 단말의 경우 네트워크 상황이 수시로 달라질 수 있다. 영상이 끊김 없이 재생되도록 하기 위해서는 영상 화질을 자동으로 변경하거나 수동으로 변경할 수 있도록 하는 것이 바람직하다.</li>
</ul>
<p>인코딩 포맷은 아주 다양하다. 하지만 그 대부분은 다음 두 부분으로 구성되어 있다.</p>
<ul>
<li>컨테이너: 영상 파일, 오디오, 메타데이터를 담는 바구니 같은 것이다. 컨테이너 포맷은 .avi, .mov, .mp4같은 파일 확장자를 보면 알 수 있다.</li>
<li>코덱: 영상 화질은 보존하면서 파일 크기를 줄일 목적으로 고안된 압축 및 압축 해제 알고리즘이다. 가장 많이 사용되는 영상 코덱으로는 H.264, VP9, HEVC가 있다.</li>
</ul>
<h3 id="유향-비순환-그래프dag-모델">유향 비순환 그래프(DAG) 모델</h3>
<p>영상을 트랜스코딩하는 것은 컴퓨팅 자원을 많이 소모할 뿐 아니라 시간도 많이 드는 작업이다. 게다가 콘텐츠 창작자는 각자 자기만의 영상 프로세싱 요구사항을 갖고 있다. 가령 어떤 사람은 영상 위에 워터마크를 표시하고 싶어 할 것이고, 어떤 사람은 섬네일 이미지를 스스로 만들어 쓰고 싶어 할 것이고, 어떤 사람은 고화질 영상을 선호하는 반면 또 다른 어떤 이는 저화질 영상도 충분하다고 생각할 것이다.</p>
<p>이처럼 각기 다른 유형의 영상 프로세싱 파이프라인을 지원하는 한편 처리 과정의 <strong>병렬성</strong>을 높이기 위해서는 적절한 수준의 추상화를 도입하여 클라이언트 프로그래머로 하여금 실행할 작업을 손수 정의할 수 있도록 해야 한다. 예를 들어 페이스북의 스트리밍 영상 엔진은 유향 비순환 그래프(DAG)를 도입하여 작업을 단계별로 배열할 수 있도록 하여 해당 작업들이 <strong>순차적 또는 병렬적</strong>으로 실행될 수 있도록 하고 있다. 아래 설계안에서도 이와 유사한 DAG모델을 도입하여 유연성과 병렬성을 달성할 수 있도록 하겠다. 아래 그림이 설계안이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/39e739b3-aa3a-4f0e-b68c-29e1b51832ac/image.png" alt=""></p>
<p>위 그림에서 원본 영상은 일단 영상, 오디오, 메타데이터의 세 부분으로 나뉘어 처리된다. 영상 부분에 적용되는 작업은 다음과 같다.</p>
<ul>
<li>검사: 좋은 품질의 영상인지, 손상은 없는지 확인하는 작업이다.</li>
<li>영상 인코딩: 비디오를 다양한 해상도, 코덱, 비트레이트 조합으로 인코딩 하는 작업이다. 아래 그림을 봐보자.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/33b0f702-fb54-4b08-b853-a1eadb7e05d0/image.png" alt=""></p>
<ul>
<li>섬네일: 사용자가 업로드한 이미지나 비디오에서 자동 추출된 이미지로 섬네일을 만드는 작업</li>
<li>워터마크: 영상에 대한 식별정보를 이미지 위에 오버레이 형태로 띄워 표시하는 작업</li>
</ul>
<h2 id="비디오-트랜스코딩-아키텍처">비디오 트랜스코딩 아키텍처</h2>
<p>아래 설계안에서는 클라우드 서비스를 활용한 영상 트랜스코딩 아키텍처를 다음과 같이 정의했다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/57dde922-6537-4cda-81dc-7bb6f4fa8438/image.png" alt=""></p>
<p>이 아키텍처는 다섯 개의 주요 컴포넌트로 구성되어 있다.</p>
<ul>
<li>전처리기</li>
<li>DAG 스케줄러</li>
<li>자원 관리자</li>
<li>작업 실행 서버</li>
<li>임시 저장소</li>
</ul>
<p>이 아키텍처가 동작한 결과로 인코딩된 영상이 만들어진다.</p>
<h3 id="전처리기">전처리기</h3>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e1eed167-8b3e-42e9-a043-8c0de85b90dc/image.png" alt=""></p>
<p>전처리기가 하는 일은 아래 처럼 3가지 이다.</p>
<ol>
<li>비디오 분할: 비디오 스트림을 GOP라고 불리는 단위로 쪼갠다. GOP는 특정 순서로 배열된 프레임(frame) 그룹이다. 하나의 GOP는 독립적으로 재생 가능하며, 길이는 보통 몇 초 정도이다. 어떤 종류의 오래된 단말이나 브라우저는 GOP 단위의 영상 분할을 지원하지 않는다. 그런 단말의 경우에는 전처리기가 영상 분할을 대신한다.</li>
<li>DAG 생성: 클라이언트 프로그래머가 작성한 설정 파일에 따라 DAG를 만들어낸다. 아래그림은 2개 노드와 1개 연결선으로 구성된 DAG의 사례와 설정 파일이다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/faaefbc8-d2bd-4e73-954d-4a3e3517ed5d/image.png" alt=""></p>
<ol start="3">
<li>데이터 캐시: 전처리기는 분할된 영상의 캐시이기도 하다. 안정성을 높이기 위해 전처리기는 GOP와 메타데이터를 임시 저장소에 보관한다. 영상 인코딩이 실패하면 시스템은 이렇게 보관된 데이터를 활용해 인코딩을 재개한다.</li>
</ol>
<h3 id="dag-스케줄러">DAG 스케줄러</h3>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/172ed2df-d069-415a-9b7f-6cf1a3065263/image.png" alt=""></p>
<p>DAG 스케줄러는 DAG 그래프를 몇 개 단계로 분할한 다음에 그 각각을 자원 관리자의 작업 큐에 집어넣는다. 아래그림은 DAG 스케줄러가 어떻게 동작하는지를 보여주는 하나의 사례다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c0ad0d20-a1ad-408d-a534-35e7f68317bf/image.png" alt=""></p>
<ol>
<li>첫번째 단계에서는 영상, 오디오, 메타데이터를 분리한다</li>
<li>영상 파일을 인코딩하고, 섬네일을 추출하고, 오디오 파일 또한 인코딩한다.</li>
</ol>
<h3 id="자원-관리자">자원 관리자</h3>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/0993d21a-fa3f-4b22-97d5-14fddb31ae97/image.png" alt=""></p>
<p>자원 관리자는 자원 배분을 효과적으로 수행하는 역할을 담당한다. 아래그림과 같이 세 개의 큐와 작업스케줄러로 구성된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/bb052513-04c2-4a73-b812-5b859591dcf4/image.png" alt=""></p>
<ul>
<li>작업 큐: 실행할 작업이 보관되어 있는 우선순위 큐다.</li>
<li>작업 서버 큐: 작업 서버의 가용 상태 정보가 보관되어 있는 우선순위 큐다.</li>
<li>실행 큐: 현재 실행 중인 작업 및 작업 서버 정보가 보관되어 있는 큐다.</li>
<li>작업 스케줄러: 최적의 작업/서버 조합을 골라, 해당 작업 서버가 작업을 수행하도록 지시하는 역할을 담당한다.</li>
</ul>
<p>작업 관리자는 다음과 같이 동작한다.</p>
<ul>
<li>작업 관리자는 작업 큐에서 가장 높은 우선순위의 작업을 꺼낸다</li>
<li>작업 관리자는 해당 작업을 실행하기 적합한 작업 서버를 고른다</li>
<li>작업 스케줄러는 해당 작업 서버에게 작업 실행을 지시한다</li>
<li>작업 스케줄러는 해당 작업이 어떤 서버에게 할당되어있는지에 관한 정보를 실행 큐에 넣는다</li>
<li>작업 스케줄러는 작업이 완료되면 해당 작업을 실행 큐에서 제거한다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/2ee70c10-25f7-4e07-93d2-080ce5041a45/image.png" alt=""></p>
<p>작업 서버는 DAG에 정의된 작업을 수행한다. 아래 그림처럼, 작업 종류에 따라 작업 서버도 구분하여 관리한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/9a7a3cd6-7677-4ef4-952e-4c94236a011e/image.png" alt=""></p>
<h3 id="임시-저장소">임시 저장소</h3>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/74ef252f-e937-4958-a18e-58d6e1aaf5c7/image.png" alt=""></p>
<p>임시 저장소 구현에는 여러 저장소 시스템을 활용할 수 있다. 어떤 시스템을 선택할 것이냐는 저장할 데이터의 유형, 크기, 이용 빈도, 데이터 유효기간 등에 따라 달라진다. 예를 들어 메타데이터는 작업 서버가 빈번히 참조하는 정보이고 그 크기도 작은 것이 보통이다. 따라서 메모리에 캐시해 두면 좋을 것이다. 그러나 영상/오디오 데이터는 BLOB 저장소에 두는 것이 바람직하다. 임시 저장소에 보관한 데이터는 영상 프로세싱이 완룐되면 삭제한다.</p>
<h3 id="인코딩된-비디오">인코딩된 비디오</h3>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/469fc079-5dd1-419a-9a10-ea9399eef3eb/image.png" alt=""></p>
<p>인코딩된 영상은 닌코딩 파이프라인의 최종 결과물이다.</p>
<h3 id="시스템-최적화">시스템 최적화</h3>
<p>영상을 업로드하는 과정, 스트리밍하고 트랜스코딩하는 절차에 대해서는 충분히 알것이다. 이제 <strong>속도, 안전성, 그리고 비용 측면</strong>에서 이 시스템을 최적화 해보자.</p>
<h3 id="속도-최적화-비디오-병렬-업로드">속도 최적화: 비디오 병렬 업로드</h3>
<p>영상 전부를 한 번의 업로드로 올리는 것은 비효율적이다. 하나의 영상은 아래 그림과 같은 작은 GOP들로 분할할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/f9e31563-f6bb-4f68-a686-a29fb9a89b81/image.png" alt=""></p>
<p>이렇게 분할한 GOP를 <strong>병렬적</strong>으로 업로드하면 설사 일부가 실패해도 빠르게 업로드를 재개할 수 있다. 따라서 비디오를 GOP 경계에 맞춰 분할하는 작업을 단말이 수행하면 아래그림과 같이 업로드 속도를 높일 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/db1cd6a2-2faf-4b66-8767-10a682f15977/image.png" alt=""></p>
<h3 id="속도-최적화-업로드-센터를-사용자-근거리에-지정">속도 최적화: 업로드 센터를 사용자 근거리에 지정</h3>
<p>업로드 속도를 개선하는 또 다른 방법은 업로드 센터를 아래 그림 처럼 여러 곳에 두는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/678ba7fe-144e-4fe0-8ecc-e9a7e3e2840b/image.png" alt=""></p>
<h3 id="속도-최적화-모든-절차를-병렬화">속도 최적화: 모든 절차를 병렬화</h3>
<p>낮은 응답지연을 달성하는 것은 어려운 일이다. 이를 위해 시도해 볼 수 있는 또 하나의 방법은, 느슨하게 결합된 시스템을 만들어서 <strong>병렬성</strong>을 높이는 것이다.</p>
<p>이를 위해서는 지금까지의 설계안을 조금 변경해야 한다. 영상을 원본 저장소에서 CDN으로 옮기는 절차를 좀더 자세히 보자. 이 절차는 아래 그림과 같다. 어떤 단계의 결과물은 이전 단계의 결과물을 입력으로 사용하여 만들어진다는 것을 알 수 있다. 이런 <strong>의존성</strong>이 있으면 <strong>병렬성</strong>을 높이기 어렵다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/a7faba26-a5f0-473b-8034-2070e5ccaced/image.png" alt=""></p>
<p>이 시템의 결합도를 낮추기 위해, 아래 그림과 같은 메시지 큐를 도입한다.</p>
<p>이 메시지 큐가 어떻게 시스템 결합도를 낮추는지, 예를 들어 살펴보자.</p>
<ul>
<li>메시지 큐를 도입하기 전에 인코딩 모듈은 다운로드 모듈의 작업이 끝나기를 기다려야 했다</li>
<li>메시지 큐를 도입한 뒤에 인코딩 모듈은 다운로드 모듈의 작업이 끝나기를 더 이상 기다릴 필요가 없다. 메시지 큐에 보관된 이벤트 각각을 인코딩 모듈은 <strong>병렬적</strong>으로 처리할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/81649b13-f307-414e-bf2a-727775857169/image.png" alt=""></p>
<h3 id="안전성-최적화-미리-사인된-업로드-url">안전성 최적화: 미리 사인된 업로드 URL</h3>
<p>안전성은 모든 제품의 가장 중요한 측면 가운데 하나일 것이다. 허가받은 사용자만이 올바른 장소에 영상을 업도르할 수 있기 있도록 하기 위해, 아래와 같이 미리 사인된 업로드 URL을 이용한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/16578076-8268-4450-85e8-5482cfc1f231/image.png" alt=""></p>
<p>이를 위해 업로드 절차는 다음과 같이 변경한다.</p>
<ol>
<li>클라이언트는 HTTP 서버에 POST 요청을 하여 미리 사인된 URL을 받는다. 해당 URl이 가리키는 객체에 대한 접근 권한이 이미 주어져 있는 상태다. ‘미리 사인된 URL’이라는 용어는 사실 아마존 S3에서 쓰이는 용어다. 다른 클라우드 업체에서는 다른 이름으로 부를 수도 있다. 마이크로소프트 애저가 제공하는 BLOB 저장소는 같은 기능을 “접근 공유 시그니처”라 부른다.</li>
<li>API 서버는 미리 사인된 URL을 돌려준다.</li>
<li>클라이언트는 해당 URL이 가리키는 위치에 영상을 업로드한다.</li>
</ol>
<h3 id="안전성-최적화-영상-보호">안전성 최적화: 영상 보호</h3>
<p>많은 콘텐츠 제작자가 영상을 인터네셍 업로드하기를 주저하는데, 영상 원본을 도난 당할까 우려해서다. 영상의 저작권을 보호하기 위해, 3가지 선택지 가운데 하나를 채택할 수 있다.</p>
<ul>
<li>디지털 저작권 관리(DRM) 시스템 도입: 이 부문에서 가장 널리 사용되는 시스템으로는 애플의 페어플레이, 구글의 와이드바인, 마이크로소프트의 플레이레디가 있다.</li>
<li>AES 암호화: 영상을 암호화하고 접근 권한을 설정하는 방식이다. 암호화된 영상은 재생 시에만 복호화한다. 허락된 사용자만 암호화된 영상을 시청할 수 있다.</li>
<li>워터마크: 영상 위에 소유자 정보를 포함하는 이미지 오버레이를 올리는 것이다. 회사 로고나 이름 등을 이 용도에 사용할 수 있다.</li>
</ul>
<h3 id="비용-최적화">비용 최적화</h3>
<p>CDN은 이 시스템의 <strong>핵심적인</strong> 부분이다. 세계 어디서도 끊김 없이 빠르게 영상을 시청할 수 있도록 해준다. 하지만, 개략적인 추정치에서도 알 수 있듯이, CDN은 비싸다. 데이터 크기가 크면 클수록 더 비싸진다. 어떻게 비용을 낮출 수 있을까?</p>
<ol>
<li>인기 영상은 CDN을 통해 재생하되 다른 영상은 영상 서버를 통해 재생하는 것이다(아래그림)</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/33b364a7-1e9a-4549-8206-96f3d8d83048/image.png" alt=""></p>
<ol start="2">
<li>인기가 별로 없는 영상은 인코딩 할 필요가 없을 수도 있따. 짧은 영상이라면 필요할 때 인코딩하여 재생할 수 있다.</li>
<li>어떤 영상은 특정 지역에서만 인기가 높다. 이런 영상은 다른 지역에 옮길 필요가 없다.</li>
<li>CDN을 직접 구축하고 인터넷 서비스 제공자와 제휴한다. CDN을 직접 구축하는 것은 초대형 프로젝트다. ISP로는 컴캐스트 등이 있다. ISP는 전세계 어디나 있으며 사용자와 가깝다. 이들과 제휴하면 사용자 경험을 향상시킬 수 있고 인터넷 사용 비용을 낮출 수 있을 것이다.</li>
<li>이 모든 최적화는 콘텐츠 인기도, 이용 패턴, 영상 크기 등의 데이터에 근거한 것이다. 최적화를 시도하기 전에 시청 패턴을 분석하는 것은 중요하다.</li>
</ol>
<h3 id="오류-처리">오류 처리</h3>
<p>시스템 오류는 대형 시스템에서는 불가피하다. 장애를 아주 잘 감내하는 시스템을 만드려면 이런 오류를 우아하게 처리하고 빠르게 회복해야 한다.</p>
<ul>
<li>회복 가능 오류: 특정 영상 세그먼트를 트랜스코딩하다 실패했다든가 하는 오류는 회복 가능한 오류에 속한다. 일반적으로 보자면 이런 오류는 몇 번 재시도하면 해결된다. 하지만 계속해서 실패하고 복구가 어렵다 판단되면 클라이언트에게 적절한 오류 코드를 반환해야 한다.</li>
<li>회북 불가능 오류: 영상 포맷이 잘못되었다거나 하는 회복 불가능한 오류가 발견되면 시스템은 해당 영상에 대한 작업을 중단하고 클라이언트에게 적절한 오류 코드를 반환해야한다.</li>
</ul>
<p>시스템 컴포넌트 각각에 발생할 수 있는 오류에 대한 전형적 해결 방법을 아래에 요약 했다.</p>
<ul>
<li>업로드 오류: 몇 회 재시도한다</li>
<li>비디오 분할 오류: 낡은 버전의 클라이언트가 GOP 경계에 따라 영상을 분할하지 못하는 경우라면 전체 영상을 서버로 전송하고 서버가 해당 영상을 분할 처리하도록 한다</li>
<li>트랜스코딩 오류: 재시도한다</li>
<li>전처리 오류: DAG 그래프를 재생성한다</li>
<li>DAG 스케줄러 오류: 작업을 다시 스케줄링한다</li>
<li>자원 관리자 큐에 장애 발생: 사본을 이용한다.</li>
<li>작업 서버 장애: 다른 서버에서 해당 작업을 재시도</li>
<li>API 서버 장애: API 서버는 무상태이므로 신규 요청은 다른 API서버로 우회될 것 이다</li>
<li>메타데이터 캐시 서버 장애: 데이터는 다중화되어 있으므로 다른 노드에서 데이터를 여전히 가져올 수 있을 것이다. 장애가 난 캐시 서버는 새로운 것으로 교체한다.</li>
<li>메타데이터 데이터베이스 서버 장애:<ul>
<li>주 서버가 죽었다면 부 서버 가운데 하나를 주 서버로 교체한다</li>
<li>부 서버가 죽었다면 다른 부 서버를 토앻 읽기 연산을 처리하고 죽은 서버는 새것으로 교체한다.</li>
</ul>
</li>
</ul>
<h1 id="4단계-마무리">4단계 마무리</h1>
<p>유튜브와 같은 영상 스트리밍 서비스를 만들기 위해 어떤 설계가 필요한지 살펴보았다. 위에서 다루지 않은 내용 중 다음과 같은 내용을 생각해보면 좋다.</p>
<ul>
<li>API 계층의 규모 확장성 확보 방안: API 서버는 무상태 서버이므로 수평적 규모 확장이 가능하다는 사실을 언급하면 좋다</li>
<li>데이터베이스 계층의 규모 확장성 확보 방안: 데이터베이스의 다중화와 샤딩 방법에 대해 이야기하자</li>
<li>라이브 스트리밍: 라이브 스트리밍은 영상을 실시간으로 녹화하고 방송하는 절차를 말한다. 라이브 스트리밍 시스템과 비-라이브 스트리밍 시스템 간에는 비슷한 점 도 많다. 둘 다 영상 업로드, 인코딩, 스트리밍이 필요하다는 점이 같다. 가장 <strong>중요한 차이</strong>는 다음과 같다<ul>
<li>라이브 스트리밍의 경우에는 응답지연이 좀 더 낮아야 한다. 따라서 스트리밍 프로토콜 선정에 유의해야 한다.</li>
<li>라이브 스트리밍의 경우 병렬화 필요성은 떨어질 텐데, 작은 단위의 데이터를 실시간으로 빨리 처리해야 하기 때문이다.</li>
<li>라이브 스트리밍의 경우 오류 처리 방법을 달리해야 한다. 너무 많은 시간이 걸리는 방안은 사용하기 어렵다.</li>
</ul>
</li>
<li>비디오 삭제: 저작권을 위바한 영상, 선정적 영상, 불법적 행위에 관계된 영상은 내려야한다. 내릴 영상은 업로드 과정에서 식별해 낼 수도 있지만, 사용자의 신고 절차를 통해 판별할 수도 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[검색어 자동완성 시스템]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EA%B2%80%EC%83%89%EC%96%B4-%EC%9E%90%EB%8F%99%EC%99%84%EC%84%B1-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EA%B2%80%EC%83%89%EC%96%B4-%EC%9E%90%EB%8F%99%EC%99%84%EC%84%B1-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Thu, 10 Aug 2023 06:58:10 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>검색어 자동완성 시스템을 설계해보자.</p>
<p>구글에 dinner를 검색하면 아래와 같은 검색어 자동 기능이 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/310a0581-429a-4a18-82bc-58636a3825a6/image.png" alt=""></p>
<h1 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h1>
<h2 id="요구사항-도출">요구사항 도출</h2>
<p>요구사항을 가정해 보자.</p>
<ul>
<li>빠른 응답 속도: 사용자가 검색어를 입력함에 따라 자동완성 검색어도 충분히 빨리 표시되어야 한다. 페이스북 검색어 자동완성 시스템에 관한 문서를 보면 시스템 <strong>응답속도는 100밀리초</strong> 이내여야 한다. 그렇지 않다면 사용자 입장에선 이용이 불편해진다.</li>
<li>연관성: 자동완성되어 출력되는 검색어는 사용자가 <strong>입력한 단어와 연관된것</strong>이어야 한다.</li>
<li>정렬: 시스템의 계산 결과는 <strong>인기도</strong>등의 <strong>순위 모델</strong>에 의해 정렬되어 있어야 한다.</li>
<li>규모 확장성: 시스템은 많은 트래픽을 감당할 수 있도록 확장 가능해야 한다.</li>
<li>고가용성: 시스템의 일부에 장애가 발생하거나, 느려지거나, 예상치 못한 네트워크 문제가 생겨도 시스템은 <strong>계속 사용 가능해야한다.</strong></li>
</ul>
<h2 id="개략적-규모-추정">개략적 규모 추정</h2>
<ul>
<li>일간 능동 사용자(DAU)는 천만 명으로 가정한다.</li>
<li>평균적으로 한 사용자는 매일 10건의 검색을 수행한다고 가정한다.</li>
<li>질의할 때마다 평균적으로 20바이트의 데이터를 입력한다고 가정한다.<ul>
<li>문자 인코딩 방법으로는 ASCII를 사용한다고 가정할 것이므로, 1문자 = 1바이트이다.</li>
<li>질의문은 평균적으로 4개 단어로 이루어진다고 가정할 것이며, 각 단어는 평균적으로 다섯 글자로 구성된다고 가정할 것이다.</li>
<li>따라서 질의당 평균 4X5=20 바이트이다.</li>
</ul>
</li>
<li>검색창에 글자를 입력할 때마다 클라이언트는 검색어 자동완성 백엔드에 요청을 보낸다. 따라서 평균적으로 1회 검색당 20건의 요청이 백엔드로 전달된다. 예를 들어 사용자가 dinner라고 입력하면 다음의 6개 요청이 순차적으로 백엔드에 전송된다<ol>
<li>search?q=d</li>
<li>search?q=di</li>
<li>search?q=din</li>
<li>search?q=dinn</li>
<li>search?q=dinne</li>
<li>search?q=dinner</li>
</ol>
</li>
<li>대략 초당 24,000건의 질의(QPS)가 발생할 것이다(=10,000,000사용자 X 10질의/일X20자/24시간/3600초).</li>
<li>최대 QPS=QPSX2=대략 48,000</li>
<li>질의 가운데 20% 정도는 신규 검색어라고 가정할 것이다. 따라서 대략 0.4GB 정도다. 매일 0.4GB의 신규 데이터가 시스템에 추가된다는 뜻이다.</li>
</ul>
<h1 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h1>
<p>개략적으로 보면 시스템은 두 부분으로 나뉜다.</p>
<ul>
<li>데이터 수집 서비스: 사용자가 입력한 질의를 실시간으로 수집하는 시스템이다. 데이터가 많은 애플리케이션에 실시간 시스템은 그다지 바람직하지 않지만 설계안을 만드는 출발점으로는 괜찮을 것이다. 상세 설계안을 준비할 때 보다 현실적인 안으로 교체하자.</li>
<li>질의 서비스: 주어진 질의에 다섯 개의 인기 검색어를 정렬해 내놓는 서비스</li>
</ul>
<h2 id="데이터-수집-서비스">데이터 수집 서비스</h2>
<p>데이터 수집 서비스가 어떻게 동작하는지 간단한 예제를 통해 살펴보자. 질의문과 사용빈도를 저장하는 빈도 테이블이 있다고 가정하겠다. 처음에 이 테이블은 비어 있는데, 사용자가 ‘twitch’,’twitter’,’twitter’,’twillo’를 순서대로 검색하면 그 상태가 아래 그림처럼 바뀌어 간다</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/ac2a090d-0999-4692-8523-fa4a6395dcf1/image.png" alt=""></p>
<h2 id="질의-서비스">질의 서비스</h2>
<p>아래표 같은 빈도 테이블이 있는 상태라고 가정하면 두 개의 필드가 있음을 볼 수 있다.</p>
<ul>
<li>query: 질의문을 저장하는 필드다</li>
<li>frequency: 질의문이 사용된 빈도를 저장하는 필드다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/f1ca2250-deea-420a-aa8f-00f3c7fd33e5/image.png" alt=""></p>
<p>이 상태에서 사용자가 “tw”를 검색창에 입력하면 아래의 “top 5” 자동완성 검색어가 표시 되어야 하는데, “top 5”는 빈도 필드에 기록된 수치를 사용한다고 가정해보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/3fee062d-eed8-4897-ad2b-a73dd034081c/image.png" alt=""></p>
<p>가장 많이 사용된 5개 검색어 “top 5”는 아래의 SQL 질의문을 사용해 계산할 수 있다.</p>
<pre><code class="language-sql">SELECT * FROM frequency_table
WHERE query Like `prefix%`
ORDER BY frequency DESC
LIMIT 5</code></pre>
<p>데이터 양이 적을 때는 나쁘지 않은 설계안이다. 하지만 데이터가 아주 많아지면 데이터베이스가 병목이 될 수 있다. 이 병목 현상을 해결할 방법은 상세 설계에서 알아보자</p>
<h1 id="3단계-상세-설계">3단계 상세 설계</h1>
<p>데이터 수집 서비스와 질의 서비스의 두 부분으로 구성된 개략적 설계안은 최적화된 결과물이라고 말하기엔 어렵지만 출발점으론 썩 괜찮은 방법 이었다. 상세 설계에선 컴포넌트를 몇 개 골라 보다 상세히 설계하고 최적화 방안을 논의해볼 것이다</p>
<ul>
<li>트라이 자료구조</li>
<li>데이터 수집 서비스</li>
<li>질의 서비스</li>
<li>규모 확장이 가능한 저장소</li>
<li>트라이 연산</li>
</ul>
<h2 id="트라이-자료구조">트라이 자료구조</h2>
<p>개략적 설계안에서는 관계형 데이터베이스를 저장소로 사용했었다. 하지만 관계형 데이터베이스를 이용해 가장 인기 있었던 다섯 개 질의문을 골라내는 방법은 효율적이지 않다. 이 문제는 트라이를 사용해 해결할 것이다. </p>
<aside>
💡 트라이 자료구조란?
트라이의 이름은 “retrieval”이라는 단어에서 온것이며,
접두어 트리 (prefix tree)라고도 불린다

</aside>

<p>트라이가 시스템의 핵심적 부분이 될 것이므로, 충분한 시간을 할애하여 주어진 요구사항에 딱 맞는 트라이를 만들도록 할 것이다.</p>
<p>일단, 트라이 자료구조에 대해 간단하게 살펴본 후 어떻게 최적화하면 응답 시간을 줄일 수 있을지에 집중해보겠다.</p>
<p>트라이는 문자열들을 간략하게 저장할 수 있는 자료구조다. 문자열을 꺼내는 연산에 초점을 맞추어 설계된 자료구조이며, 핵심 아이디어는 아래와 같다.</p>
<ul>
<li>트라이는 트리 형태의 자료구조다</li>
<li>이 트리의 루트 노드는 빈 문자열을 나타낸다</li>
<li>각 노드는 글자 하나를 저장하며, 26개(해당 글자 다음에 등장할 수 있는 모든 글자의 개수)의 자식 노드를 가질 수 있다</li>
<li>각 트리 노드는 하나의 단어, 또는 접두어 문자열을 나타낸다</li>
</ul>
<p>아래 그림은 질의어 ‘tree’, ‘try’, ‘true’, ‘toy’, ‘wish’, ‘win’이 보관된 트라이다 해당 질의어를 나타내는 노드는 굵은 외곽선으로 표시 되어 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/7cad1ff2-5099-47a3-b91e-8fc2262eb703/image.png" alt=""></p>
<p>기본 트라이 자료구조는 노드에 문자열만 저장한다, 이용 빈도에 따라 정렬된 결과를 반환하기 위해서는 노드에 <strong>빈도 정보까지</strong> 저장해야할 필요가 있다</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/7480b2c0-a852-411e-9a6e-b206e41d7b69/image.png" alt=""></p>
<p>이 빈도 정보를 트라이 노드에 저장하게 되면 아래 그림과 같은 상태가 될 것이다</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/19c13f03-f3de-466e-b034-a145e2f65808/image.png" alt=""></p>
<p>그러면 이 트라이 자료구조로 검색어 자동완성은 어떻게 구현할 수 있을까? 알고리즘을 살펴보기전, 용어 몇가지를 숙지하자</p>
<ul>
<li>p: 접두어(prefix)의 길이</li>
<li>n: 트라이 안에 있는 노드 개수</li>
<li>c: 주어진 노드의 자식 노드 개수</li>
</ul>
<p>가장 많이 사용된 질의어 k개는 다음과 같이 찾을 수 있다</p>
<ul>
<li>해당 접두어를 표현하는 노드를 찾는다. 시간 복잡도는 O(p)다</li>
<li>해당 노드부터 시작하는 하위 트리를 탐색하여 모든 유효 노드를 찾는다 유효한 검색 문자열을 구성하는 노드가 유효 노드다 시간 복잡도는 O(c)다</li>
<li>유효 노드들을 정렬하여 가장 인기 있는 검색어 k개를 찾는다 시간 복잡도는 O(clogc)다</li>
</ul>
<p>위 그림의 예제를 보자 k=2이고, 입력으로 ‘be’를 입력했다고 가정하면 아래와 같이 동작할 것이다</p>
<ol>
<li>접두어 노드 ‘be’를 찾는다</li>
<li>해당 노드부터 시작하는 하위 트리를 탐색하여 모든 유효 노드를 찾는다. 아래그림의 경우에는 [beer: 10], [best: 35], [bet: 29]가 유효 노드다</li>
<li>유효 노드를 정렬하여 2개만 골라낸다. [best: 35]와 [bet: 29]가 접두어(즉, 검색어) “tr”에 대해 검색된 2개의 인기 검색어다</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/7e47b0b7-07c7-464b-a7e3-6012351294c0/image.png" alt=""></p>
<p>동작에 필요한 시간 복잡도는 O(p) + O(c) + O(clogc)다.</p>
<p>이 알고리즘은 직관적이지만 최악의 경우에는 k개 결과를 얻으려고 전체 트라이를 다 검색해야 하는 일이 생길 수도 있다. 이 문제를 해결하는 방법은 두가지가 있다.</p>
<ol>
<li>접두어의 최대 길이를 제한</li>
<li>각 노드에 인기 검색어를 캐시</li>
</ol>
<p>이 두가지 최적화 방안을 순서대로 살펴보자</p>
<h3 id="접두어-최대-길이-제한">접두어 최대 길이 제한</h3>
<p>사용자가 검색창에 긴 검색어를 입력하는 일은 거의 없다. 따라서 p값은 정숫값(가령 50같은)이라고 가정해도 안전하다. 검색어의 최대 길이를 제한할 수 있다면 “접두어 노드를 찾는” 단계의 시간 복잡도는 O(p)에서 O(작은 상수값)=O(1)로 바뀔 것이다</p>
<h3 id="노드에-인기-검색어-캐시">노드에 인기 검색어 캐시</h3>
<p>각 노드에 k개의 인기 검색어를 저장해두면 전체 트라이를 검색하는 일을 방지할 수 있다.</p>
<p>5~10개 정도의 자동완성 제안을 표시하면 충분하므로, k는 작은 값이다. 미리 캐시해 두면 각 노드에 질의어를 저장할 공간이 많이 필요하게 된다는 단점도 있지만, 빠른 응답속도가 아주 중요할 때는 이 정도의 단점은 희생할 만한 가치가 있다.</p>
<p>아래 그림은 개선된 트라이 구조다. 각 노드에 가장 인기 있는 검색어 다섯가지를 저장하도록 했다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/9a8bfa3a-14d9-4895-9cae-e28cc7a1a1b7/image.png" alt=""></p>
<p>앞의 두 가지 최적화 기법을 적용하면 시간 복잡도가 어떻게 달라지는지 알아보면 다음과 같다</p>
<ol>
<li>접두어 노드를 찾는 시간 복잡도는 O(1)로 바뀐다</li>
<li>최고 인기 검색어 5개를 찾는 질의의 시간 복잡도도 O(1)로 바뀐다. 이미 인기 검색어가 캐시되어 있기 때문이다</li>
</ol>
<p>각 단계의 시간 복잡도가 O(1)로 바뀐 덕분에, 최고 인기 검색어 k개를 찾는 전체 시간 복잡도도 O(1)로 바뀐다</p>
<h2 id="데이터-수집-서비스-1">데이터 수집 서비스</h2>
<p>지금까지 살펴본 설계안은 사용자가 입력을 할 때마다 실시간으로 데이터를 수정했다. 이 방법은 다음 두 가지 문제로 그다지 <strong>실용적이지 않다</strong></p>
<ol>
<li>매일 수천만 건의 질의가 입력될 텐데 그때마다 트라이를 갱신하면 질의 서비스는 심각하게 느려질 것이다</li>
<li>일단 트라이가 만들어지고 나면 인기 검색어는 그다지 자주 바뀌지 않을 것이다. 그러니 트라이는 그렇게 자주 갱신할 필요가 없다</li>
</ol>
<p>규모 확장이 쉬운 데이터 수집 서비스를 만들려면 데이터가 어디서 오고 어떻게 이용되는지를 살펴야 한다. 트위터 같은 실시간 애플리케이션이면 제안되는 검색어를 항상 신선하게 유지할 필요가 잇지만 구글 검색 같은 애플리케이션이라면 그렇게 자주 바꿔줄 이유는 없을 것이다</p>
<p>용례가 달라지더라도 데이터 수집 서비스의 토대는 바뀌지 않을 것이다. 트라이를 만드는 데 쓰는 데이터는 보통 데이터 분석 서비스나 로깅 서비스로부터 올 것이기 때문이다</p>
<p>아래 그림은 데이터 분석 서비스의 수정된 설계안이다 각 컴포넌트를 차례로 살펴보자</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e9d06f07-405d-45cc-8522-269c0c8f46c1/image.png" alt=""></p>
<h3 id="데이터-분석-서비스-로그">데이터 분석 서비스 로그</h3>
<p>데이터 분석 서비스 로그에는 검색창에 입력된 질의에 관한 원본 데이터가 보관된다. 새로운 데이터가 추가될 뿐 수정은 이루어지지 않으며 로그 데이터에는 인덱스를 걸지 않는다</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e2dc5fe3-771e-4423-b306-96957f7959eb/image.png" alt=""></p>
<h3 id="로그-취합-서버">로그 취합 서버</h3>
<p>데이터 분석 서비스로부터 나온 로그는 보통 그 양이 엄청나고 데이터 형식도 제각각인 경우가 많다. 따라서 이 데이터를 잘 취합하여 우리 시스템이 쉽게 소비할 수 있도록 해야한다.</p>
<p>애플리케이션의 종류에 따라 취합 방식은 달라질 수 있다. 트위터 같은 실시간 애플리케이션은 취합 주기를 보다 짧게 가져가야 하며, 대부분의 애플리케이션은 일주일에 한 번 정도로 로그를 취합해도 충분할 것이다.</p>
<h3 id="취합된-데이터">취합된 데이터</h3>
<p>아래표는 매주 취합한 데이터의 사례다. time 필드는 해당 주가 시작한 날짜를 나타낸다. frequency 필드는 해당 질의가 해당 주에 사용된 횟수의 합이다</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/72160844-c120-4ed7-b1a9-e07d6cceacce/image.png" alt=""></p>
<h3 id="작업-서버">작업 서버</h3>
<p>작업 서버는 주기적으로 비동기적 작업을 실행하는 서버 집합이다. 트라이 자료구조를 만들고 트라이 데이터베이스에 저장하는 역할을 담당한다</p>
<h3 id="트라이-캐시">트라이 캐시</h3>
<p>트라이 캐시는 분산 캐시 시스템으로 트라이 데이터를 메모리에 유지하여 읽기 연산 성능을 높이는 구실을 한다. 매주 트라이 데이터베이스의 스냅샷을 떠서 갱신한다.</p>
<h3 id="트라이-데이터베이스">트라이 데이터베이스</h3>
<p>트라이 데이터베이스는 지속성 저장소다. 트라이 데이터베이스로 사용할 수 있는 선택지로는 다음의 두 가지가 있다.</p>
<ol>
<li>문서 저장소: 새 트라이를 매주 만들 것이므로, 주기적으로 트라이를 직렬화하여 데이터베이스에 저장할 수 있따. 몽고디비 같은 문서 저장소를 활용하면 이런 데이터를 편리하게 저장할 수 있다.</li>
<li>키-값 저장소: 트라이는 아래 로직을 적용하면 해시 테이블 형태로 변환 가능하다<ul>
<li>트라이에 보관된 모든 접두어를 해시 테이블 키로 변환</li>
<li>각 트라이 노드에 보관된 모든 데이터를 해시 테이블 값으로 변환</li>
</ul>
</li>
</ol>
<p>아래 그림은 트라이를 해시 테이블로 어떻게 대응할 수 있는지 보여준다</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/2e755162-4ec7-4ba4-8d4a-736aad59a788/image.png" alt=""></p>
<p>위 그림에서 각 트라이 노드는 하나의 &lt;키,값&gt; 쌍으로 변환된다.</p>
<h3 id="질의-서비스-1">질의 서비스</h3>
<p>개략적 설계안에서 살펴본 질의 서비스는 데이터베이스를 활용하여 최고 인기 검색어 다섯 개를 골라냈다. 아래그림은 해당 설계안의 <strong>비효율성을 개선한</strong> 새 설계안이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/04412bf1-1524-4022-a24d-ea13ad467119/image.png" alt=""></p>
<ol>
<li>검색 질의가 로드밸런서로 전송된다</li>
<li>로드밸런서는 해당 질의를 API 서버로 보낸다</li>
<li>API 서버는 트라이 캐시에서 데이터를 가져와 해당 요청에 대한 자동완성 검색어 제안 응답을 구성한다</li>
<li>데이터가 트라이 캐시에 없는 경우에는 데이터를 데이터베이스에서 가져와 캐시에 채운다. 그래야 다음에 같은 접두어에 대한 질의가 오면 캐시에 보관된 데이터를 사용해 처리할 수 있다. 캐시 미스는 캐시 서버의 메모리가 부족하거나 캐시 서버에 장애가 있어도 발생할 수 있다.</li>
</ol>
<p>질의 서비스는 빨라야 한다. 이를 위해 다음과 같은 최적화 방안을 생각해 보자.</p>
<ul>
<li>AJAX 요청: 웹 애플리케이션의 경우 브라우저는 보통 AJAX 요청을 보내어 자동완성된 검색어 목록을 가져온다. 이 방법의 장점은 요청을 보내고 받기 위해 페이지를 새로고침 할 필요가 없다는 것이다</li>
<li>브라우저 캐싱: 대부분 애플리케이션의 경우 자동완성 검색어 제안 결과는 짧은 시간 안에 자주 바뀌지 않는다. 따라서 제안된 검색어들을 브라우저 캐시에 넣어두면 후속 질의의 결과는 해당 캐시에서 바로 가져갈 수 있따. 구글 검색 엔진이 이런 캐시 매커니즘을 사용한다. 아래 그림은 구글 검색 엔진에 system design interview라고 입력했을 때 날아오는 응답 헤더다. 보다시피 구글은 제안된 검색어를 한 시간 동안 캐시해 둔다. cahce-control 헤더 값에 등장하는 private는 해당 응답이 요청을 보낸 사용자의 캐시에만 보관될 수 있으며 공용 캐시에 저장되어서는 안 된다는 뜻이다. max-age=3600은 해당 캐시 항목은 3600초, 즉 한 시간 동안만 유효하다는 뜻이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/187586c2-2da4-4f1c-962a-2a4009379acc/image.png" alt=""></p>
<ul>
<li>데이터 샘플링: 대규모 시스템의 경우, 모든 질의 결과로 로깅하도록 해 놓으면 CPU 자원과 저장공간을 엄청나게 소진하게 된다. 데이터 샘플링 기법은 그럴 때 유용하다. 즉, N개 요청 가운데 1개만 로깅하도록 하는 것이다</li>
</ul>
<h2 id="트라이-연산">트라이 연산</h2>
<p>트라이는 검색어 자동완성의 핵심 컴포넌트다. 지금부터 트라이 관련 연산들이 어떻게 동작하는지 살펴보자</p>
<h3 id="트라이-생성">트라이 생성</h3>
<p>트라이 생성은 작업 서버가 담당하며, 데이터 분석 서비스와 로그나 데이터베이스로부터 취함된 데이터를 이용한다.</p>
<h3 id="트라이-갱신">트라이 갱신</h3>
<p>트라이를 갱신하는 데는 두가지 방법이 있다.</p>
<ol>
<li>매주 한 번 갱신하는 방법. 새로운 트라이를 만든 당므에 기존 트라이를 대체한다.</li>
<li>트라이의 각 노드를 개별적으로 갱신하는 방법. 본 설계안에서는 이 방법을 채택하지 않았는데, 성능이 좋지 않아서다. 하지만 트라이가 작을 경우에는 고려해볼 만한 방안이다. 트라이 노드를 갱신할 때는 그 모든 상위 노드도 갱신해야 하는데, 상위 노드에도 인기 검색어 질의 결과가 보관되기 때문이다. 아래 그림으로 왜 비효율적인지 알아보자.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/f2951103-1efb-465d-9086-bce680e29b2e/image.png" alt=""></p>
<p>‘beer’의 이용 빈도를 10에서 30으로 갱신해야 하는 상황이라면, 위 그림과 같이 해당 노드의 상위 노드들에 기록된 이용 빈도 수치도 전부 30으로 갱신될 것이다.</p>
<h3 id="검색어-삭제">검색어 삭제</h3>
<p>혐오성이 짙거나, 폭력적이거나, 성적으로 노골적이거나, 여러 가지로 위험한 질의어를 자동완성 결과에서 제거해야 한다. 이를 위한 방법으로는 <strong>필터 계층</strong>을 두어 부적절한 질의어가 반환되지 않도록 하는 것이다. 필터 계층을 두면 필터 규칙에 따라 검색 결과를 자유롭게 변경할 수 있다는 장점이 있따. 데이터베이스에서 해당 검색어를 물리적으로 삭제하는 것은 다음번 업데이트 사이클에 비동기적으로 진행하면 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/821c08d1-6b54-44cc-93ea-770e004b3c01/image.png" alt=""></p>
<h2 id="저장소-규모-확장">저장소 규모 확장</h2>
<p>자동완성된 검색어를 사용자에게 제공하는 시스템의 설계를 마쳤으니, 트라이의 크기가 한 서버에 넣기엔 너무 큰 경우에도 대응할 수 있도록 규모 확장성 문제를 해결해보자</p>
<p>영어만 지원하면 되기 때문에, 간단하게는 첫 글자를 기준으로 샤딩하는 방법을 생각해 볼 수 있다.</p>
<ul>
<li>검색어를 보관하기 위해 두 대 서버가 필요하다면 ‘a’부터 ‘m’까지 글자로 시작하는 검색어는 첫 번째 서버에 저장하고, 나머지는 두 번째 서버에 저장한다.</li>
<li>세 대 서버가 필요하다면 ‘a’부터 ‘i’까지는 첫 번째 서버에, ‘j’부터 ‘r’까지는 두 번째 서버에, 나머지는 세 번째 서버에 저장한다</li>
</ul>
<p>이 방법을 사용하면 사용 가능한 최대 서버 개수는 영어 알파벳 개수인 26대로 제한된다. 이 이상으로 서버를 늘리려면 샤딩을 계층적으로 해야한다. 가령 검색어의 첫 번째 글자는 첫 번째 레벨의 샤딩에 쓰고, 두 번째 글자는 두 번째 레벨의 샤딩에 쓰는 것이다. 예를 들어 ‘a’로 시작하는 검색어를 네 대 서버에 나눠 보관하고 싶다고 하면, ‘aa’부터 ‘ag’까지는 첫 번째 서버에, ‘ah’부터’an’까지는 두 번째 서버에, ‘ao’부터’au’까지는 세 번째 서버에, 나머지는 네 번째 서버에 보관하면 될 것이다.</p>
<p>얼핏 생각하기에는 그럴싸해 보이지만, ‘c’로 시작하는 단어가 ‘x’로 시작하는 단어보다 많다는 점을 고려하면 데이터를 각 서버에 균등하게 배분하기가 불가능하다.</p>
<p>이 문제를 해결하기 위해, 본 설계안의 경우 과거 질의 데이터의 패턴을 분석하여 샤딩하는 아래와 같은 그림을 제안한다.</p>
<p>아래 그림에서 검색어 대응 샤드 관리자는 어떤 검색어가 어느 저장소 서버에 저장되는지에 대한 정보를 관리한다. 예를 들어 ‘s’로 시작하는 검색어의 양이 ‘u<del>z’로 시작하는 검색어를 전부 합친 것과 비슷하다면, ‘s’에 대한 샤드 하나와 ‘u</del>z’까지의 검색어를 위한 샤드 하나를 두어도 충분할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/d72ffd4c-44bc-437b-8c13-36da8bb97802/image.png" alt=""></p>
<h1 id="4단계-마무리">4단계 마무리</h1>
<p>상세 설계를마치면 다음과 같은 것을 생각해보아야 한다.</p>
<ol>
<li>다국어 지원이 가능하도록 시스템을 확장하려면 어떻게 해야 하나?</li>
</ol>
<p>비영어권 국가에서 사용하는 언어를 지원하려면 유니코드 데이터를 저장해야 한다.</p>
<ol>
<li>국가별로 인기 검색어 순위가 다르다면 어떻게 해야하나?</li>
</ol>
<p>국가별로 다른 트라이를 사용하면 된다. 트라이를 CDN에 저장하여 응답속도를 높이는 방법도 생각해 볼 수 있다.</p>
<ol>
<li>실시간으로 변하는 검색어의 추이를 반영하려면 어떻게 해야하나요?</li>
</ol>
<p>새로운 뉴스 이벤트가 생긴다든가 하는 이유로 특정 검색어의 인기가 갑자기 높아질 수 있다. 현 설계안은 그런 검색어를 지원하기에 적합하지 않다.</p>
<ul>
<li>작업 서버가 매주 한 번 씩만 갱신 하도록 되어 있기 때문</li>
<li>때맞춰 서버가 실행된다 해도 트라이를 구성하는데 너무 많은 시간이 소요된다</li>
</ul>
<p>실시간 검색어 자동완성 시스템을 구축하는데에 도움될 만한 몇 가지 아이디어만 보자면 다음과 같다</p>
<ul>
<li>샤딩을 통하여 작업 대상 데이터의 양을 줄인다</li>
<li>순위 모델을 바꾸어 최근 검색어에 보다 높은 가중치를 주도록 한다</li>
<li>데이터가 스트림 형태로 올 수 있다는 점, 즉 한번에 데이터를 동시에 사용할 수 없을 가능성이 있다는 점을 고려해야 한다. 데이터가 스트리밍 된다는 것은, <strong>즉 데이터가 지속적으로 생성된다는 뜻</strong>이다. 스트림 프로세싱에는 특별한 종류의 시스템이 필요하다. 예를 들어 아파치 하둡 맵리듀스, 아파치 스파크 스트리밍, 아파치 스톰, 아파치 카프카 등이 그런 부류의 시스템이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[채팅 시스템 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EC%B1%84%ED%8C%85-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EC%B1%84%ED%8C%85-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 07 Aug 2023 03:37:02 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<h1 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h1>
<p>요구사항을 추려보자.</p>
<ul>
<li>1:1 채팅과 그룹 채팅을 모두 지원해야 한다.</li>
<li>모바일앱과 웹앱을 모두 지원해야한다.</li>
<li>일별 능동 사용자수(DAU Daily Active User)는 5천만명을 처리할 수 있어야 한다.</li>
<li>그룹 채팅의 경우 인원제한은 100명까지 참가할 수 있어야 한다.</li>
<li>1:1 채팅, 그룹 채팅, 사용자 접속상태 표시를 지원해야 하며, 텍스트 메시지만 주고받을 수 있다.</li>
<li>메시지 길이의 제한은 100,000자 이하다.</li>
<li>종단 간 암호화는 일단 고려하지 않음.</li>
<li>채팅 이력은 영원히 보관해야 한다.</li>
</ul>
<h1 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h1>
<p>채팅 시스템의 경우 <strong>클라이언트 끼리 서로 직접 통신하지 않는다</strong>. 클라이언트는 채팅 서비스와 통신한다.</p>
<p>일단, 기본 요구사항을 만족시키기 위해 필요한 기능을 살펴보자.</p>
<ul>
<li>클라이언트들로부터 메시지 수신</li>
<li>메시지 수신자 결정 및 전달</li>
<li>수신자가 접속 상태가 아닌 경우에는 접속할 때까지 해당 메시지 보관</li>
</ul>
<p>아래 그림에 클라이언트와 채팅 서비스사이의 관계를 요약한 그림을 보자</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/a466a5af-57ea-4770-b585-8e82dff084f9/image.png" alt=""></p>
<p>대부분의 클라이언트/서버 애플리케이션에서 요청을 보내는 것은 클라이언트인데, 채팅 시스템의 경우도 마찬가지다. <strong>메시지 송신 클라이언트</strong>가 이 역할을 한다. 위 그림에서 송신 클라이언트는 수신 클라이언트에게 전달할 메시지를 채팅 서비스에 보낼 때, 오랜 세월 <strong>검증된</strong> HTTP 프로토콜을 사용한다. 클라이언트는 채팅 서비스에 HTTP 프로토콜로 연결한 다음 메시지를 보내어 수신자에게 해당 메시지를 전달하라고 알린다. 채팅 서비스와의 접속에는 keep-alive 헤더를 사용하면 효율적인데, 클라이언트와 서버 사이의 연결을 끊지 않고 계속 유지할 수 있어서다. TCP 접속 과정에서 발생하는 핸드셰이크 횟수를 줄일 수 있음은 물론이다. HTTP는 메시지 전송 용도로는 괜찮은 ㅅ너택이며, 페이스북 같은 많은 대중적 채팅 프로그램이 초기에 HTTP를 사용했다.</p>
<p>HTTP는 클라이언트가 연결을 만드는 프로토콜이며, 서버에서 클라이언트로 임의 시점에 메시지를 보내는 데는 쉽게 쓰일 수 <strong>없다.</strong> 서버가 연결을 만드는 것처럼 동작할 수 있도록 많은 기법들이 제안되어 있는데</p>
<ul>
<li>폴링</li>
<li>롱 폴링</li>
<li>웹소켓</li>
</ul>
<p>등이 그런 기술이다.</p>
<h2 id="폴링">폴링</h2>
<p>아래 그림에 제시한 대로, 폴링은 클라이언트가 주기적으로 서버에게 새 메시지가 있느냐고 물어보는 방법이다. 폴링 비용은 폴링을 자주하면 할수록 올라간다. 답해줄 메시지가 없는 경우 서버 자원이 불필요하게 낭비된다는 문제도 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/8cabe61b-f16f-4ec5-9d21-f0d4269ecab9/image.png" alt=""></p>
<h2 id="롱-폴링">롱 폴링</h2>
<p>폴링은 여러 가지로 비효율적일 수 있어서 나온 기법이 롱폴링이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c89e3e12-6462-4761-96d0-893e89c74de7/image.png" alt=""></p>
<p>롱 폴링의 경우 클라이언트는 새 메시지가 반환되거나 타임아웃 될 때까지 연결을 유지한다.</p>
<p>클라이언트는 새 메시지를 받으면 기존 연결을 종료하고 서버에 새로운 요청을 보내어 모든 절차를 다시 시작한다. 이러한 방법은 다음과 같은 단점이 있다.</p>
<ul>
<li>메시지를 보내는 클라이언트와 수신하는 클라이언트가 같은 채팅 서버에 접속하게 되지 않을 수도 있다. HTTP 서버들은 보통 무상태 서버다. 로드밸런싱을 위해 라운드 로빈 알고리즘을 사용하는 경우, 메시지를 받은 서버는 해당 메시지를 수신할 클라이언트와의 롱 폴링 연결을 가지고 있지 않은 서버일 수 있다.</li>
<li>서버 입장에서는 클라이언트가 연결을 해제했는지 아닌지 알 방법이 없다.</li>
<li>여전히 비효율적이다. 메시지를 많이 받지 않는 클라이언트도 타임아웃이 일어날 때마다 주기적으로 서버에 다시 접속할 것이다.</li>
</ul>
<h2 id="웹소켓">웹소켓</h2>
<p>웹소켓은 서버가 클라이언트에게 비동기 메시지를 보낼때 가장 널리 사용하는 기술이다. 아래그림을 봐보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/17e3778f-a1d9-44c8-9dc1-f0e7d06074ab/image.png" alt=""></p>
<p>웹소켓 연결은 클라이언트가 시작하낟. 한번 맺어진 연결은 항구적이며 양방향이다. 이 연결으 ㄴ처음에는 HTTP 연결이지만 특정 핸드셰이크 절차를 거쳐 웹소켓 연결로 업그레이드된다. 웹소켓은 일반적으로 방화벽이 있는 환경에서도 잘 동작한다. 80이나 443처럼 HTTP나 HTTPS 프로토콜이 사용하는 기본 포트번호를 그대로 쓰기 때문이다.</p>
<p>앞서 우리는 HTTP 프로토콜이 메시지를 보내려는 클라이언트에게 썩 괜찮은 프로토콜임을 설명해싿. 하지만 웹소켓은 이에 더해 양방향 메시지 전송까지 가능하게 하므로, 웹소켓 대신 HTTP를 굳이 고집할 이유는 없는 셈이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e50c4015-4484-4f55-af61-de0b33d8902f/image.png" alt=""></p>
<p>웹소켓을 사용하면 메시지를 보낼 때나 받을 떄 동일한 프로토콜을 사용할 수 있으므로 설계뿐 아니라 구현도 단순하고 직관적이다. 유의할 것은 웹소켓 연결은 항구적으로 유지되어야 하기 때문에 서버 측에서 연결 관리를 효율적으로 해야 한다는 것이다.</p>
<h2 id="개략적-설계안">개략적 설계안</h2>
<p>채팅 서비스는 웹소켓을 사용하기로 결정했지만 다른 부분(회원가입,로그인)부분과 같은 부분에선 굳이 웹소켓을 사용할 필요는 없다. 대부분의 기능은 일반적인 HTTP상에서 구현해도 된다.</p>
<p>지금부터 전체 시스템의 개략적 설계안을 살펴보자. 아래 그림처럼 채팅 시스템은 세 부분으로 나누어 볼 수 있다. 즉 무상태 서비스, 상태유지 서비스, 그리고 제3자 서비스 연동의 세 부분으로 나누어 볼 수 있다.</p>
<h3 id="무상태-서비스">무상태 서비스</h3>
<p>이 설계안에서 무상태 서비스는 로그인, 회원가입, 사용자 프로파일 표시 등을 처리하는 전통적인 요청/응답 서비스다. 무상태 서비스가 제공하는 기능은 웹사이트와 앱이 보편적으로 제공하는 기능이다.</p>
<p>무상태 서비스는 로드밸런서 뒤에 위치한다. 로드밸런서가 하는 일은 요청을 그 경로에 맞는 서비스로 정확하게 전달하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/b1adb5fa-9b5f-46d2-9ca7-0e97badfbd91/image.png" alt=""></p>
<p>로드밸런서 뒤에 오는 서비스는 모놀리틱 서비스일 수도 있고 마이크로서비스 일 수도 있다. 이 서비스들 가운데 상당수가 시장에 완제품으로 나와 있어서 우리가 직접 구현하지 않아도 쉽게 사서 쓸 수 있다. 이들 가운데 나중에 좀 더 자세히 살펴볼 것은 ‘서비스 탐색’서비스다. 이 서비스는 클라이언트가 접속할 채팅 서버의 DNS 호스트명을 클라이언트에게 알려주는 역할을 한다.</p>
<h3 id="상태-유지-서비스">상태 유지 서비스</h3>
<p>본 설계안에서 유일하게 상태 유지가 필요한 서비스는 채팅 서비스다. 각 클라이언트가 채팅 서버와 독립적인 네트워크 연결을 유지해야 하기 때문이다. 클라이언트는 보통 서버가 살아 있는 한 다른 서버로 연결하지 않으므로, 앞서 간단히 설명한 서비스 탐색 서비스는 채팅 서비스와 긴밀히 협력하여 특정 서버에 <strong>부하가 몰리지 않도록</strong> 한다. 구체적인 내용은 나중에 다시 자세히 살펴볼 것이다.</p>
<h3 id="제3자-서비스-연동">제3자 서비스 연동</h3>
<p>채팅 앱에서 가장 중요한 제3자 서비스는 푸시 알림이다. 새 메시지를 받았다면 설사 앱이 실행 중이지 않더라도 알림을 받아야 해서다. 따라서 푸시 알림 서비스와의 통합은 아주 중요하다.</p>
<h3 id="규모-확장성">규모 확장성</h3>
<p>트래픽 규모가 얼마 되지 않을 때는 위처럼 서버 한대로 구현할 수 있다. 하지만, 트래픽 규모가 많은 대규모 시스템인 경우, 이론적으로는 서버 한대로 처리할 수 있기는 하지만, <strong>서버 한 대로 얼마나 많은 접속을 동시에 허용할 수 있는지 따져보아야 한다.</strong> 동시 접속자가 1M이라고 가정할 때, 접속당 10K 메모리가 필요하다면 10GB 메모리만 있으면 모든 연결을 다 처리할 수 있을 것이다.</p>
<p>하지만 서버 한 대에 담은 설계는 별로 좋지 않은 방법인데, 여러가지 이유가 있지만 SPOF도 단점중 하나이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/1aae6c93-ea8c-4cb2-99dc-9f162b9e7175/image.png" alt=""></p>
<p>위 그림에서 유의할 것은 실시간으로 메시지를 주고받기 위해서는 클라이언트는 채팅 서버와 웹소켓 연결을 끊지 않고 유지해야 한다는 것이다.</p>
<ul>
<li>채팅 서버는 클라이언트 사이에 메시지를 중계하는 역할을 담당</li>
<li>접속상태 서버는 사용자의 접속 여부를 관리</li>
<li>API 서버는 로그인, 회원가입, 프로파일 변경 등 그 외 나머지 전부를 처리</li>
<li>알림 서버는 푸시 알림을 보냄</li>
<li>키-값 저장소에는 채팅 이력을 보관한다. 시스템에 접속한 사용자는 이전 채팅 이력을 전부 보게 될 것이다.</li>
</ul>
<h3 id="저장소">저장소</h3>
<p>데이터 계층을 올바르게 만드는 데는 노력이 필요하다. <strong>중요한 것</strong>은 어떤 데이터베이스를 쓰느냐다. 관계형 데이터베이스를 쓸 것인가 아니면 NoSQL을 채택할 것인가? 이 질문에 대한 올바른 답을 하기 위해 중요하게 따져야 할 것은, 데이터의 유형과 읽기/쓰기 연산의 패턴이다.</p>
<p>채팅 시스템이 다루는 데이터는 보통 두 가지다.</p>
<ol>
<li>사용자 프로파일, 설정, 친구 목록처럼 일반적인 데이터</li>
</ol>
<ul>
<li>이런 데이터는 안정성을 보장하는 관계형 데이터베이스에 보관한다. 다중화와 샤딩은 이런 데이터의 가용성과 규모 확장성을 보증하기 위해 보편적으로 사용되는 기술이다.</li>
</ul>
<ol>
<li>채팅 시스템에 고유한 데이터로, 바로 채팅 이력 데이터</li>
</ol>
<ul>
<li>이 데이터를 어떻게 보관할지 결정하려면 읽기/쓰기 연산 패턴을 이해해야 한다.</li>
<li>채팅 이력 데이터의 양은 엄청나다. 페이스북 메신저나 왓츠앱은 매일 600억 개의 메시지를 처리한다.</li>
<li>이 데이터 가운데 빈번하게 사용되는 것은 주로 최근에 주고받은 메시지다. 대부분의 사용자는 오래된 메시지는 들여다보지 않는다.</li>
<li>사용자는 대체로 최근에 주고받은 메시지 데이터만 보게 되는 것이 사실이나, 검색 기능을 이요하거나, 특정 사용자가 언급된 메시지를 보거나, 특정 메시지로 점프하거나 하여 무작위적인 데이터 접근을 하게 되는 일도 있다. 데이터 계층은 이런 기능도 지원해야 한다.</li>
<li>1:1 채팅 앱의 경우 읽기:쓰기 비율은 대략 1:1 정도다.</li>
</ul>
<p>이 모든 기능을 지원할 데이터베이스를 고르는 것은 아주 중요한 일이다. 본 설계안의 경우에는 키-값 저장소를 추천할 것인데, 다음과 같은 이유가 있다.</p>
<ul>
<li>키-값 저장소는 수평적 규모확장이 쉽다.</li>
<li>키-값 저장소는 데이터 접근 지연시간이 낮다.</li>
<li>관계형 데이터베이스는 데이터 가운데 롱 테일에 해당하는 부분을 잘 처리하지 못하는 경향이 있다. 인덱스가 커지면 데이터에 대한 무작위 접근을 처리하는 비용이 늘어난다.</li>
<li>이미 많은 안정적인 채팅 시스템이 키-값 저장소를 채택하고 있다. 페이스북 메신저나 디스코드 메신저가 그 사례다. 페이스북 메신저는 HBase를 사용하고 디스코드는 카산드라를 이용하고 있다.</li>
</ul>
<h3 id="데이터-모델">데이터 모델</h3>
<p>키-값 저장소를 데이터 계층 기술로 사용하기로 했으니, 메시지 데이터를 어떻게 보관할 것인지 자세히 살펴보자.</p>
<h3 id="11-채팅을-위한-메시지-테이블"><strong>1:1 채팅을 위한 메시지 테이블</strong></h3>
<p>아래 표 1:1 채팅을 지원하기 위한 메시지 테이블의 사례다. 이 테이블의 기본 키는 message_id로, 메시지 순서를 쉽게 정할 수 있도록 하는 역할도 담당한다. created_at을 사용하여 메시지 순서를 정할 수는 없는데, 서로 다른 두 메시지가 동시에 만들어질 수 도 있기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c11e7dbc-0d72-4ac3-8d68-993ade076022/image.png" alt=""></p>
<h3 id="그룹-채팅을-위한-메시지-테이블">그룹 채팅을 위한 메시지 테이블</h3>
<p>아래 표는 그룹 채팅을 위한 메시지 테이블의 사례다. channel_id, message_id의 복합키를 기본키로 사용한다. channel_id는 파티션 키로도 사용할 것인데, 그룹 채팅에 적용될 모든 질의는 특정 채널을 대상으로 할 것이기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e9e835d0-7b29-44ad-bf2c-7b5955981185/image.png" alt=""></p>
<h3 id="메시지-id">메시지 ID</h3>
<p>message_id는 생성될 때 아래와 같은 속성을 만족해야 한다.</p>
<ul>
<li>message_id의 값은 고유해야 한다</li>
<li>ID 값은 정렬 가능해야 하며 시간 순서와 일치해야 한다. 즉, 새로운 ID는 이전 ID보다 큰 값이어야 한다.</li>
</ul>
<p>이 두 조건을 어떻게 만족시킬 것인가? RDBMS라면 auto_increment가 대안이 될 수 있겠지만 NoSQL은 보통 해당 기능을 제공하지 않는다.</p>
<p>두 번째 방법은 <strong>스노플레이크</strong> 같은 전역적 64-bit 순서 번호(sequence number)생성기를 이용하는 것이다.</p>
<p>마지막 방법은 지역적 순서 번호 생성기를 이용하는 것이다. 여기서 지역적이라 함은, ID의 유일성은 같은 그룹 안에서만 보증하면 충분하다 라는 것이다. 이 방법이 통하는 이유는 메시지 사이의 순서는 같은 채널, 혹은 같은 1:1 채팅 세션 안에서만 유지되면 충분하기 때문이다. 전역적 ID 생성기에 비해 구현하기 쉬운 접근법이다.</p>
<h1 id="3단계-상세-설계">3단계 상세 설계</h1>
<p>개략적 설계안에 포함된 컴포넌트 중</p>
<p><strong>서비스 탐색, 메시지 전달 흐름, 그리고 사용자 접속 상태를 표시</strong> 하는 방법 정도를 자세하게 알아보자.</p>
<h2 id="서비스-탐색">서비스 탐색</h2>
<p>서비스 탐색 기능의 주된 역할은 클라이언트에게 가장 적합한 채팅 서버를 추천하는 것이다.</p>
<p>이때 사용되는 기준으로는 클라이언트의 위치, 서버의 용량 등이 있다.</p>
<p>서비스 탐색 기능을 구현하는 데 널리 쓰이는 오픈 소스 솔루션으로는 아파치 주키퍼 같은 것이 있다. 사용 가능한 모든 채팅 서버를 여기 등록시켜 두고, 클라이언트가 접속을 시도하면 사전에 정한 기준에 따라 최적의 채팅 서버를 골라 주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/f1a97429-3ed2-4250-9198-d972214f00d2/image.png" alt=""></p>
<ol>
<li>사용자 A가 시스템에 로그인을 시도한다.</li>
<li>로드밸런서가 로그인 요청을 API 서버들 가운데 하나로 보낸다.</li>
<li>API 서버가 사용자 인증을 처리하고 나면 서비스 탐색 기능이 동작하여 해당 사용자를 서비스할 최적의 채팅 서버를 찾는다.</li>
<li>사용자 A는 채팅 서버 2와 웹소켓 연결을 맺는다.</li>
</ol>
<h2 id="메시지-흐름">메시지 흐름</h2>
<p>채팅 시스템에 있어서 종단 간 메시지 흐름을 이해하는 것은 흥미로운 주제다.</p>
<p>1:1 채팅 메시지의 처리 흐름과 여러 단말 간 메시지 동기화 과정을 살펴본 다음, 그룹 채팅 메시지의 처리 흐름도 살펴보자.</p>
<h3 id="11-채팅-메시지-처리-흐름">1:1 채팅 메시지 처리 흐름</h3>
<p>아래그림은 1:1 채팅에서 사용자 A가B에게 보낸 메시지가 어떤 경로로 처리되는지 보여준다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/f50f36d9-fef2-4102-929d-347022217e00/image.png" alt=""></p>
<ol>
<li>사용자 A가 채팅 서버 1로 메시지 전송</li>
<li>채팅 서버 1은 ID 생성기를 사용해 해당 메시지의 ID결정</li>
<li>채팅 서버 1은 해당 메시지를 메시지 동기화 큐로 전송</li>
<li>메시지가 키-값 저장소에 보관됨</li>
<li>(a) 사용자 B가 접속 중인 경우 메시지는 사용자 B가 접속 중인 채팅 서버로 전송됨                    (b) 사용자 B가 접속중이 아니라면 푸시 알림 메시지를 푸시 알림 서버로 보냄</li>
<li>채팅 서버2는 메시지를 사용자 B에게 전송. 사용자 B와 채팅 서버 2사이에는 웹소켓 연결이 있는 상태이므로 그것을 이용</li>
</ol>
<h3 id="여러-단말-사이의-메시지-동기화">여러 단말 사이의 메시지 동기화</h3>
<p>여러 개 단말을 사용하는 사람은 많다. 지금부터 여러 단말 사이에 메시지 동기화는 어떻게 하는지 설명할 것이다. 아래그림은 한 사례다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/785c4a99-2f00-44f9-9d38-e65c23512aa9/image.png" alt=""></p>
<p>위 그림에서 사용자는 전화기와 랩톱 두개의 단말을 이용하고 있다.</p>
<p>사용자가 전화기에서 채팅 앱에 로그인한 결과로 채팅 서버 1과 해당 단말 사이에 웹소켓 연결이 만들어져 있고, 랩톱에서 로그인한 결과로 역시 별도 웹소켓이 채팅 서버 1에 연결되어 있는 상황이다.</p>
<p>각 단말은 cur_max_message_id라는 변수를 유지하는데, 해당 단말에서 관측된 가장 최신 메시지의 ID를 추적하는 용도이다. 아래 두 조건을 만족하는 메시지는 새 메시지로 간주한다.</p>
<ul>
<li>수신자 ID가 현재 로그인한 사용자 ID와 같다.</li>
<li>키-값 저장소에 보관된 메시지로서, 그 ID가 cur_max_message_id보다 크다.</li>
</ul>
<p>cur_max_message_id는 단말마다 별도로 유지 관리하면 되는 값이라 키-값 저장소에서 새 메시지를 가져오는 동기화 작업도 쉽게 구현할 수 있다.</p>
<h3 id="소규모-그룹-채팅에서의-메시지-흐름">소규모 그룹 채팅에서의 메시지 흐름</h3>
<p>1:1 채팅에 비해 그룹 채팅에서의 메시지 흐름은 조금 더 복잡하다. 아래 그림들을 보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/57ec53f5-d1a2-4b20-baa6-d5bc90705491/image.png" alt=""></p>
<p>위 그림을 보면, 사용자 A가 메시지를 보내면 사용자 B와C의 메시지 동기화 큐에 복사된다.</p>
<p>이 큐를 사용자 각각에 할당된 메시지 수신함으로 생각해도 무방할 것이다.</p>
<p>이 설계안은 소규모 그룹 채팅에 적합한데, 이유는 아래와 같다.</p>
<ul>
<li>새로운 메시지가 왔는지 자기 큐만 보면 되니까 메시지 동기화 흐름이 단순함</li>
<li>그룹이 크지 않으면 메시지를 수신자별로 복사해서 큐에 넣는 작업의 비용이 문제가 되지 않음</li>
</ul>
<p>위챗이 이런 접근법을 쓰고 있으며, 그룹의 크기는 500명으로 제한하고 있다. 하지만 <strong>많은 사용자를 지원해야 하는 경우</strong> 이러한 접근은 바람직하지 않을 것이다.</p>
<p>지금 설명한 메시지 흐름을 수신자 관점에서 살펴보면, 따라서 메시지 동기화 큐는 아래 그림과 같이 여러 사용자로부터 오는 메시지를 받을 수 있어야 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/a2ebeb39-9f04-470b-90f1-4b5dfcd13803/image.png" alt=""></p>
<h2 id="접속상태-표시">접속상태 표시</h2>
<p>접속상태 표시는 상당수 채팅 애플리케이션의 핵심 기능중 하나이다. 접속상태를 표시하기 위해서는 무엇이 필요한지 살펴보자.</p>
<p>개략적 설계안에서는 접속상태 서버를 통해 사용자의 상태를 관리한다고 했는데, 접속상태 서버는 클라이언트와 웹소켓으로 통신하는 실시간 서비스의 일부라는 점에 유의하자. 사용자의 상태가 바뀌는 시나리오는 몇 가지가 있는데 하나씩 살펴보자.</p>
<h3 id="사용자-로그인">사용자 로그인</h3>
<p>클라이언트와 실시간 서비스 사이에 웹소켓 연결이 맺어지고 나면 접속상태 서버는 A의 상태와 last_active_at 타입스탬프 값을 키-값 저장소에 보관한다. 이 절차가 끝나고 나면 사용자는 접속 중인 것으로 표시될 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/53c1390d-432b-437b-b8e8-de0cc3d245e2/image.png" alt=""></p>
<h3 id="로그아웃">로그아웃</h3>
<p>사용자 로그아웃은 아래그림과 같은 절차를 거친다. 키-값 저장소에 보관된 사용자 상태가 online에서 offline으로 바뀌게 된다는 점을 유의하자. 이 절차가 끝나고 나면 UI상에서 사용자의 상태는 접속 중이 아닌 것으로 표시될 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/57295353-a64e-4828-b19e-a9a3bb3e37b2/image.png" alt=""></p>
<h3 id="접속-장애">접속 장애</h3>
<p>인터넷을 통한 연결이 항상 안정적이지 않을 수도 있기 때문에 인터넷이 불안정한 상황도 대응할 수 있게 설계를 해야한다. 사용자의 인터넷 연결이 끊어지면 클라이언트와 서버 사이에 맺어진 웹소켓 같은 지속성 연결도 끊어진다. 간단한 방법으로는 연결이 끊어지면 사용자를 오프라인 상태로 표시하고 연결이 복구되면 다시 온라인 상태로 변경하는 것이다.</p>
<p>하지만 짧은 시간 동안 인터넷 연결이 끊어졌다 복구되는 일은 흔하다. (차를 타고 터널을 지나는 상황) 이런 일이 벌어질 때마다 접속 상태를 변경한다면 너무 지나친 일 일것이고, 사용자 경험 측면에서도 바람직하지 않을 것이다.</p>
<p>아래 설계에서는 <strong>박동 검사</strong>를 통해 이 문제를 해결 할 것이다. 즉, 온라인 상태의 클라이언트로 하여금 주기적으로 <strong>박동 이벤트를 접속상태 서버로 보내도록 하고, 마지막 이벤트를 받은 지 x초 이내에 또 다른 박동 이벤트 메시지를 받으면 해당 사용자의 접속상태를 계속 온라인으로 유지</strong>하는 것이다<strong>.</strong></p>
<p>아래 그림의 예제에 등장하는 클라이언트는 박동 이벤트를 매 5초마다 서버로 보내고 있다. 이 이벤트를 3번 보낸후, x=30초 동안 아무런 메시지를 보내지 않아서 오프라인 상태로 변경되었다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/b1eb9f0b-7cc6-4a32-8b13-5736fa114066/image.png" alt=""></p>
<h3 id="상태-정보의-전송">상태 정보의 전송</h3>
<p>사용자A와 친구관계에 있는 사용자들은 어떻게 해당 사용자의 상태 변화를 알 수 있을까? 아래 그림은 그 원리를 보여준다. 상태정보 서버는 <strong>발행-구독 모델</strong>을 사용하는데, 즉 각각의 친구관계마다 채널을 하나씩 두는 것이다. 만약 사용자 A의 접속상태가 변경되었다면 A-B, A-C, A-D채털에 쓴다.</p>
<p>이 방안은 그룹 크기가 작을 때는 효과적이다. 위챗은 크기 상한을 500으로 제한하고 있어서 이와 유사한 접근법을 사용할 수 있었다. 그룹 크기가 더 커지면 이런 식으로 접속상태 변화를 알려서는 비용이나 시간이 많이 들게 된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/8572585a-840c-4308-960d-d123c86cbd8d/image.png" alt=""></p>
<p>큰 규모의 그룹채팅에서 접속상태 변화를 지원하려면, 그룹 채팅에 입장하는 순간에만 상태정보를 읽어오게 하거나, 친구 리스트에 있는 사용자의 접속상태를 갱신하고 싶으면 <strong>수동으로</strong> 하도록 유도하는게 좋다.</p>
<h1 id="4단계-마무리">4단계 마무리</h1>
<p>1:1 채팅과 그룹 채팅을 전부 지원하는 채팅 시스템의 아키텍처를 살펴보았는데, 클라이언트와 서버 사이의 실시간 통신을 가능하도록 <strong>웹소켓</strong>을 사용했으며 실시간 메시징을 지원하는</p>
<ul>
<li>채팅서버</li>
<li>접속상태 서버</li>
<li>푸시 알림 서버</li>
<li>채팅 이력을 보관할 키-값 저장소</li>
<li>이외의 나머지 기능을 구현하는 데 쓰일 API서버</li>
</ul>
<h2 id="추가적으로-생각해-볼-수-있는-내용">추가적으로 생각해 볼 수 있는 내용</h2>
<ul>
<li>채팅 앱을 확장하여 사진이나 비디오 등의 미디어를 지원하도록 하는 방법</li>
<li>종단 간 암호화: 왓츠앱은 메시지 전송에 있어 종단 간 암호화를 지원한다. 이는 즉, 메시지 발신인과 수신자 이외에는 아무도 메시지 내용을 볼 수 없다는 뜻이다.</li>
<li>캐시: 클라이언트에 이미 읽은 메시지를 캐시해 두면 서버와 주고받는 데이터 양을 줄일 수 있다.</li>
<li>로딩 속도 개선: 슬랙은 사용자의 데이터, 채널 등을 지역적으로 분산하는 네트워크를 구축하여 앱 로딩 속도를 개선했다.</li>
<li>오류 처리<ul>
<li>채팅 서버 오류: 채팅 서버 하나에 수십만 사용자가 접속해 있는 상황에서, 서버 하나가 죽으면 <strong>서비스 탐색 기능</strong>이 동작하여 클라이언트에게 새로운 서버를 배정하고 다시 접속할 수 있도록 해야 한다.</li>
<li>메시지 재전송: 재시도나 큐는 메시지의 안정적 전송을 보장하기 위해 흔히 사용되는 기법이다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[뉴스 피드 시스템 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EB%89%B4%EC%8A%A4-%ED%94%BC%EB%93%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EB%89%B4%EC%8A%A4-%ED%94%BC%EB%93%9C-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 07 Aug 2023 03:32:02 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<aside>
💡 뉴스피드란 무엇인가?
페이스북의 도움말 페이지 에서는
”뉴스 피드는 여러분의 홈페이지 중앙에 지속적으로 업데이트되는 스토리들로, 사용자 상태 정보 업데이트, 사진, 비디오, 링크, 앱 활동, 그리고 여러분이 페이스북에서 팔로하는 사람들, 페이지, 또는 그룹으로부터 나오는 ‘좋아요’등을 포함한다”라고 설명한다.

</aside>

<h1 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h1>
<p>아래와 같은 요구사항이 있다고 가정하자.</p>
<ul>
<li>모바일 앱과 웹 둘다 지원해야 한다</li>
<li>사용자는 뉴스 피드 페이지에 새로운 스토리를 올릴 수 있어야 하고, 친구들이 올리는 스토리를 볼 수도 있어야 한다.</li>
<li>스토리의 정렬 순서는 단순히 시간 흐름 역순으로 표시된다고 가정한다.</li>
<li>한 명의 사용자는 최대 5,000명의 친구를 가질 수 있다.</li>
<li>트래픽 규모는 매일 천만 명이 방문한다고 가정한다.(10million DAU).</li>
<li>스토리에는 이미지나 비디오 등의 미디어 파일이 포함될 수 있어야한다.</li>
</ul>
<h2 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h2>
<p>위의 요구사항으로는</p>
<ol>
<li>피드 발행</li>
<li>뉴스 피드 생성</li>
</ol>
<p>의 두 가지 부분으로 나뉘어져 있다.</p>
<ul>
<li>피드 발행: 사용자가 스토리를 포스팅하면 해당 데이터를 캐시와 데이터베이스에 기록한다. 새 포스팅은 친구의 뉴스 피드에도 전송된다.</li>
<li>뉴스 피드 생성: 지면 관계상 뉴스 피드는 모든 친구의 포스팅을 시간 흐름 역순으로 모아서 만든다고 가정</li>
</ul>
<h2 id="뉴스-피드-api">뉴스 피드 API</h2>
<p>뉴스 피드 API는 클라이언트가 서버와 통신하기 위해 사용하는 수단이다. </p>
<p>HTTP 프로토콜 기반이고, 상태 정보를 업데이트하거나, 뉴스 피드를 가져오거나, 친구를 추가하는 등의 다양한 작업을 수행하는 데 사용한다. 지금부터 이 가운데 가장 중요한 두 가지 API인 피드 발행 API와 피드 읽기 API를 살펴보자.</p>
<h3 id="피드-발행-api">피드 발행 API</h3>
<p>새 스토리를 포스팅하기 위한 APi다. HTTP POST 형태로 요청을 보내면 된다.</p>
<pre><code class="language-bash">POST /v1/me/feed</code></pre>
<p>인자:</p>
<ul>
<li>바디(body): 포스팅 내용에 해당한다.</li>
<li>Authorization 헤더: API 호출을 인증하기 위해 사용한다.</li>
</ul>
<h3 id="피드-읽기-api">피드 읽기 API</h3>
<p>뉴스 피드를 가져오는APi다.</p>
<pre><code class="language-bash">GET /v1/me/feed</code></pre>
<p>인자:</p>
<ul>
<li>Authorization 헤더: API 호출을 인증하기 위해 사용함</li>
</ul>
<h2 id="피드-발행">피드 발행</h2>
<p>피드 발행의 개략적 형태는 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/ce72b0ee-aa0b-4953-b327-10f90e3be20e/image.png" alt=""></p>
<ul>
<li>사용자: 모바일 앱 or 브라우저에서 새 포스팅을 올리는 주체다. POST /v1/me/feed APi를 사용한다</li>
<li>로드밸런서: 트래픽을 웹 서버들로 분산해줌</li>
<li>웹 서버: HTTP 요청을 내부 서비스로 중계하는 역할을 담당</li>
<li>포스팅 저장 서비스: 새 포스팅을 데이터베이스와 캐시에 저장</li>
<li>포스팅 전송 서비스: 새 포스팅을 친구의 뉴스 피드에 푸시한다. 뉴스 피드 데이터는 캐시에 보관하여 빠르게 읽어갈 수 있도록 한다.</li>
<li>알림 서비스: 친구들에게 새 포스팅이 올라왔음을 알리거나, 푸시 알림을 보내는 역할을 담당</li>
</ul>
<h2 id="뉴스-피드-생성">뉴스 피드 생성</h2>
<p>이번 절에서는 사용자가 보는 뉴스피드가 어떻게 만들어지는 살펴보자.</p>
<p>개략적인 형태는 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/ee7f7015-9afd-4ae9-a5d7-4837ebd975ff/image.png" alt=""></p>
<ul>
<li>사용자: 뉴스 피드를 읽는 주체다. GET /v1/me/feed API를 이용한다.</li>
<li>로드 밸런서: 트래픽을 웹 서버로 분산한다.</li>
<li>웹 서버: 트래픽을 뉴스 피드 서비스로 보낸다.</li>
<li>뉴스 피드 서비스: 캐시에서 뉴스 피드를 가져오는 서비스다.</li>
<li>뉴스 피드 캐시: 뉴스 피드를 렌더링할 때 필요한 피드 ID를 보관한다.</li>
</ul>
<h1 id="3단계-상세-설계">3단계 상세 설계</h1>
<p>뉴스 피드 발행과 생성 두 가지 부분의 설계를 상세히 살펴보자.</p>
<h2 id="피드-발행-흐름-상세-설계">피드 발행 흐름 상세 설계</h2>
<p>아래그림은 상세 설계안이다. 대부분의 컴포넌트는 개략적인 설계안으로 충분할 것이라, 웹서버와 포스팅 전송 서비스에 초점을 맞추자.</p>
<h3 id="웹서버">웹서버</h3>
<p>웹서버는 클라이언트와 통신할 뿐 아니라 처리율 제한 등의 기능도 수행한다. 올바른 인증 토큰 Authorization 헤더에 넣고 API를 호출하는 사용자만 포스팅을 할 수 있어야 한다. Authorization 헤더에 넣고 API를 호출하는 사용자만 포스팅할 수 있어야 한다. <strong>스팸 or 유해한 콘텐츠가 자주 올라오는 것을 방지하기 위해</strong> 특정 기간 동안 한 사용자가 올릴 수 있는 포스팅의 수에 제한을 두어야 한다.</p>
<h3 id="포스팅-전송팬아웃-서비스">포스팅 전송(팬아웃) 서비스</h3>
<p>팬아웃 서비스는 새 포스팅을 그 사용자와 친구 관계에 있는 모든 사용자에게 전달하는 과정이다. 팬아웃에는 두 가지 모델이 있는데 하나는 <strong>쓰기 시점에 팬아웃하는 모델(push 모델이라고도 함)</strong>과 <strong>읽기 시점에 팬아웃 하는 모델(pull모델 이라고도함)</strong>이 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/b2d2e688-8b58-4f50-99d7-4a49d96b6e58/image.png" alt=""></p>
<h3 id="쓰기-시점에-팬아웃하는-모델">쓰기 시점에 팬아웃하는 모델</h3>
<p>이 접근법의 경우 새로운 포스팅을 기록하는 시점에 뉴스 피드를 갱신하게 된다. 즉, 포스팅이 완료되면 바로 해당 사용자의 캐시에 해당 포스팅을 기록하는 것이다.</p>
<p><strong>장점</strong></p>
<ul>
<li>뉴스 피드가 실시간으로 갱신되며 친구 목록에 있는 사용자에게 즉시 전송된다.</li>
<li>새 포스팅이 기록되는 순간에 뉴스 피드가 이미 갱신되므로 뉴스 피드를 읽는 데 드는 시간이 짧아진다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>친구가 많은 사용자의 경우 친구 목록을 가져오고 그 목록에 있는 사용자 모두의 뉴스 피드를 갱신하는 데 많은 시간이 소요될 수도 있다. 핫키(hotkey)라고 부르는 문제다.</li>
<li>서비스를 자주 이용하지 않는 사용자의 피드까지 갱신해야 하므로 컴퓨팅 자원이 낭비된다.</li>
</ul>
<h3 id="읽기-시점에-팬아웃하는-모델">읽기 시점에 팬아웃하는 모델</h3>
<p>피드를 읽어야 하는 시점에 뉴스 피드를 갱신한다. 따라서 요청 기반 모델이다. 사용자가 본인 홈페이지나 타임라인을 로딩하는 시점에 새로운 포스트를 가져오게 된다.</p>
<p><strong>장점</strong></p>
<ul>
<li>비활성화된 사용자, 또는 서비스에 거의 로그인하지 않는 사용자의 경우에는 이 모델이 유리하다. 로그인하기까지는 어떤 컴퓨팅 자원도 소모하지 않아서다.</li>
<li>데이터를 친구 각각에 푸시하는 작업이 필요 없으므로 핫키 문제도 생기지 않는다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>뉴스 피드를 읽는 데 많은 시간이 소요될 수 있다.</li>
</ul>
<h2 id="개선된-설계안">개선된 설계안</h2>
<p>아래 설계안의 경우에는 이 두가지 방법을 결합하여 장점은 취하고 단점은 버리는 전략을 취하도록 한 설계안이다.</p>
<p>뉴스 피드를 빠르게 가져올 수 있도록 하는 것은 사용자에게 매우 중요하므로 대부분의 사용자에 대해서는 <strong>푸시 모델을 사용한다</strong> </p>
<p>친구나 팔로워가 아주 많은 사용자의 경우에는 팔로워로 하여금 해당 사용자의 포스팅을 필요할 때 가져가도록 하는 <strong>풀 모델</strong>을 사용하여 시스템의 과부하를 방지할 것이다.</p>
<p>아울러 안정 해시를 통해 요청과 데이터를 보다 고르게 분산하여 핫키 문제를 줄여볼 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/09adf87e-d40c-4280-aab3-7807a8aad432/image.png" alt=""></p>
<p>위 설계안은 아래와 같은 순서로 동작한다.</p>
<ol>
<li>그래프 데이터베이스에서 친구 ID 목록을 가져온다. 그래프 데이터베이스는 친구 관계나 친구 추천을 관리하기 적합하다.</li>
<li>사용자 정보 캐시에서 친구들의 정보를 가져온다. 그런 후에 사용자 설정에 따라 친구 가운데 일부를 걸러낸다. 예를 들어 친구 중 누군가의 피드 업데이트를 무시하기로 설정했다면 친구 관계는 유지될지 언정 해당 사용자의 새 스토리는 여러분의 뉴스 피드에 보이지 않아야 한다.</li>
<li>친구 목록과 새 스토리의 포스팅 ID를 메시지 큐에 넣는다</li>
<li>팬아웃 작업 서버가 메시지 큐에서 데이터를 꺼내어 뉴스 피드 데이터를 뉴스 피드 캐시에 넣는다. 뉴스 피드 캐시는 &lt;포스팅ID,사용자 ID&gt;의 순서쌍을 보관하는 매핑 테이블이라고 볼 수 있다. 아래 표처럼 들어갈 것이다. 이때 사용자 정보와 포스팅 정보 <strong>전부를 이 테이블에 저장하지 않는 이유</strong>는 그렇게 하면 <strong>메모리 요구량이 지나치게 늘어날 수 있기 때문</strong>이다. 따라서 ID만 보관한다. 또한 메모리 크기를 적정 수준으로 유지하기 위해서, 이 캐시의 크기에 제한을 두며, 해당 값은 조정이 가능하도록 한다. 어떤 사용자가 뉴스 피드에 올라운 수 천 개의 스토리를 전부 훑어보는 일이 벌어질 확률은 지극히 낮다. 대부분의 사용자가 보려 하는 것은 최신 스토리다. 따라서 캐시 미스가 일어날 확률은 낮아진다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/d2667558-9e19-4176-9f93-2c2c70e07f2a/image.png" alt=""></p>
<h2 id="피드-읽기-흐름-상세-설계">피드 읽기 흐름 상세 설계</h2>
<p>읽는 과정 전반의 상세 설계안은 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/d12b3378-d62f-4647-a63e-ddd6ff8903f9/image.png" alt=""></p>
<p>그림에 보인 대로, 이미지나 비디오와 같은 미디어 콘텐츠는 CDN에 저장하여 빨리 읽어갈 수 있도록 했다. 이제 클라이언트가 뉴스 피드를 어떻게 읽어가는지 단계별로 알아보자.</p>
<ol>
<li>사용자가 뉴스 피드를 읽으려는 요청을 보낸다. 요청은 /v1/me/feed로 전송될 것이다.</li>
<li>로드밸런서가 요청을 웹 서버 가운데 하나로 보낸다.</li>
<li>웹 서버는 피드를 가져오기 위해 뉴스 피드 서비스를 호출한다.</li>
<li>뉴스 피드에 표시할 사용자 이름, 사용자 사진, 포스팅 콘텐츠, 이미지 등을 사용자 캐시와 포스팅 캐시에서 가져와 완전환 뉴스 피드를 만들어낸다.</li>
<li>뉴스 피드에 표시할 사용자 이름, 사용자 사진, 포스팅 콘텐츠, 이미지 등을 사용자 캐시와 포스팅 캐시에서 가져와 완전한 뉴스 피드를 만든다.</li>
<li>생성된 뉴스 피드를 JSON 형태로 클라이언트에게 보낸다. 클라이언트는 해당 피드를 렌더링한다.</li>
</ol>
<h2 id="캐시-구조">캐시 구조</h2>
<p>캐시는 뉴스 피드 시스템의 핵심 컴포넌트다. 아래 설계안의 경우에는 아래 그림과 같이 캐시를 다섯 계층으로 나눈다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/04e235c8-e10c-4045-ae0b-e87c79cc69e6/image.png" alt=""></p>
<ul>
<li>뉴스 피드: 뉴스 피드의 ID를 보관</li>
<li>콘텐츠: 포스팅 데이터를 보관한다. 인기 콘텐츠는 따로 보관</li>
<li>소셜 그래프: 사용자 간 관계 정보를 보관</li>
<li>행동: 포스팅에 대한 사용자의 행위에 관한 정보를 보관. 포스팅에 대한 ‘좋아요’, 답글 등등이 이에 해당</li>
<li>횟수: ‘좋아요’횟수, 응답 수, 팔로워 수, 팔로잉 수 등의 정보를 보관.</li>
</ul>
<h1 id="4단계-마무리">4단계 마무리</h1>
<p>이번 장에서는 뉴스 피드 시스템을 설계해 보았다. 이번 설계안은 뉴스 피드 발행과 생성의 두 부분으로 구성되어 있다.</p>
<p>다른 설계 면접 문제와 마찬가지로, 이번 문제에도 정답은 없다. 회사마다 독특한 <strong>제약조건 이나 요구조건이 있기 때문</strong>이다. 시스템을 설계할 때는 그런점을 고려해야만 한다. 설계를 진행하고 기술을 선택할 때는 그 배경에 어떤 타협적 결정들이 있었는지 잘 이해하고 설명할 수 있어야 한다.</p>
<h2 id="규모확장성을-고려할-때-논의-사항">규모확장성을 고려할 때 논의 사항</h2>
<h3 id="데이터베이스-규모-확장">데이터베이스 규모 확장</h3>
<ul>
<li>수직적 규모 확장 vs 수평적 규모 확장</li>
<li>SQL vs NoSQL</li>
<li>주-부(master-slave) 다중화</li>
<li>복제본에 대한 읽기 연산</li>
<li>일관성 모델</li>
<li>데이터베이스 샤딩</li>
</ul>
<h3 id="이외의-논의사항">이외의 논의사항</h3>
<ul>
<li>웹 계층을 무상태로 만들지</li>
<li>가능한 많은 데이터를 캐시할 방법</li>
<li>여러 데이터 센터를 지원할 방법</li>
<li>메시지 큐를 사용하여 컴포넌트 사이의 결합도 낮추기</li>
<li>핵심 메트릭에 대한 모니터링, ex) 트래픽이 몰릴 시간대의 QPS, 사용자가 뉴스 피드를 새로고침 할 때의 지연시간 등</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[알림 시스템 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EC%95%8C%EB%A6%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EC%95%8C%EB%A6%BC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 07 Aug 2023 03:28:18 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>알림시스템은 최근 많은 프로그램이 채택한 인기 있는 기능이다.</p>
<p>이 기능을 갖춘 애플리케이션은 최신 뉴스, 제품 업데이트, 이벤트, 선물 등 고객에게 중요할 만한 정보를 <strong>비동기적으로 제공</strong>한다.</p>
<p>알림 시스템은 단순히 모바일 푸시 알림에 한정되지 않는다. 시스템은 모바일 푸시 알림, SMS 메시지, 그리고 이메일의 세 가지로 분류할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/dc2a240c-8535-410d-9548-a79b8e9e923c/image.png" alt=""></p>
<h1 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h1>
<p>하루에 백만 건 이상의 알림을 처리하는 확장성 높은 시스템을 구축하는게 쉬운 과제는 아니다. 알림 시스템이 어떻게 구현되는지에 대한 깊은 이해가 필요한 작업이다. 아래 상황으로, 요구사항의 범위를 좁혀보자.</p>
<p>알림 시스템</p>
<ul>
<li>푸시 알림, SMS 메시지, 이메일을 지원해야 한다.</li>
<li>연성 시스템이라고 가정하자. 알림은 가능한 빨리 전달되어야 하지만 시스템에 높은 부하가 걸렸을 때는 약간의 지연은 무방하다.</li>
<li>IOS 단말, 안드로이드 단말, 그리고 랩톱/데스크톱을 지원해야 한다.</li>
<li>보낼 알림은 클라이언트 애플리케이션에서도 만들 수 있고, 서버 측에서도 스케줄링 할 수 도 있다.</li>
<li>사용자의 선택에 따라 알림을 받지 않도록 설정할 수 도 있어야한다.</li>
<li>하루에 천만 건의 모바일 푸시 알림, 백만 건의 SMS 메시지, 5백만 건의 이메일을 보낼 수 있어야 한다.</li>
</ul>
<h1 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h1>
<p>이번 절에서는 IOS 푸시 알림, 안드로이드 푸시 알림, SMS 메시지, 그리고 이메일을 지원하는 시스템의 개략적인 설계안을 살펴보자.</p>
<ul>
<li>알림 유형별 지원 방안</li>
<li>연락처 정보 수집 절차</li>
<li>알림 전송 및 수신 절차</li>
</ul>
<h2 id="알림-유형별-지원-방안">알림 유형별 지원 방안</h2>
<p>각각의 알림 메커니즘이 어떻게 동작하는지 살펴보자.</p>
<h3 id="ios-푸시-알림">iOS 푸시 알림</h3>
<p>iOS에서 푸시 알림을 보내기 위해서는 세 가지 컴포넌트가 필요하다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/cadf6d7c-f14f-4f6c-981e-5d75ea57f978/image.png" alt=""></p>
<ul>
<li>알림 제공자: 알림 요청을 만들고 알림 서비스(APNS: Apple Push Notification Service)로 보내는 주체다. 알림 요청을 만드려면 다음과 같은 데이터가 필요하다.<ul>
<li>단말 토큰: 알림 요청을 보내는 데 필요한 고유 식별자</li>
<li>페이로드: 알림 내용을 담은 JSON 딕셔너리다. 아래는 그 예다.</li>
</ul>
</li>
</ul>
<pre><code class="language-bash">{
    &quot;apps&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>APNS: 애플이 제공하는 원격 서비스다. 푸시 알림을 iOS 장치로 보내는 역할을 담당한다.</li>
<li>iOS 단말: 푸시 알림을 수신하는 사용자 단말</li>
</ul>
<h3 id="안드로이드-푸시-알림">안드로이드 푸시 알림</h3>
<p>안드로이드 푸시 알림도 비슷한 절차다. APNS대신 FCM(Firebase Cloud Messaging)을 사용하는 점만 다르다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/6fc68dbe-15ed-476c-99ca-dae44b4ff4ab/image.png" alt=""></p>
<h3 id="sms-메시지">SMS 메시지</h3>
<p>SMS  메시지를 보낼 때는 보통 트윌리오, 넥스모 같은 제 3사업자의 서비스를 많이 이용한다. 이런 서비스는 대부분 상용 서비스라서 이용 요금을 내야 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/75d1c7cc-4c01-441c-bb51-edd9a33c7806/image.png" alt=""></p>
<h3 id="이메일">이메일</h3>
<p>대부분의 회사는 고유 이메일 서버를 구축할 역량은 갖추고 있다. 그럼에도 많은 회사가 상용 이메일 서비스를 이용한다. 그중 유명한 서비스로 샌드그리드, 메일침프가 있다. 전송 성공률도 높고, 데이터 분석 서비스도 제공한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/cb9fc20f-f82e-4853-97a7-ea3345cacd53/image.png" alt=""></p>
<h3 id="알림-시스템-별-전체-그림">알림 시스템 별 전체 그림</h3>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/f6d1030e-7a36-4a20-b187-453129c730e7/image.png" alt=""></p>
<h2 id="연락처-정보-수집-절차">연락처 정보 수집 절차</h2>
<p>알림을 보내려면 모바일 단말 토큰, 전화번호, 이메일 주소 등의 정보가 필요하다. 아래 그림과 같이 사용자가 우리 앱을 설치하거나 처음으로 계정을 등록하면 API 서버는 해당 사용자의 정보를 수집하여 데이터베이스에 저장한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/fde94ecd-fd32-4469-8a7d-7cb7d96bddf9/image.png" alt=""></p>
<p>이 데이터베이스에 연락처 정보를 저장할 테이블 구조는 아래그림과 같다. 필수 정보만 담은 개략적인 설계안으로, 이메일 주소와 전화번호는 user테이블에 저장하고, 단말 토큰은 device 테이블에 저장한다. 한 사용자가 여러 단말을 가질 수 있고, 알림은 모든 단말에 전송되어야 한다는 점을 고려한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/0941bcfc-3d5d-49c1-94a5-47e12c2ef751/image.png" alt=""></p>
<h2 id="알림-전송-및-수신-절차">알림 전송 및 수신 절차</h2>
<p>우선 개략적인 설계안부터 살펴보고, 점차로 최적화해 나가도록 할 것이다.</p>
<p>아래 그림은 개략적 설계 초안이다. 각 시스템 컴포넌트에 대한 설명은 그 아래에 두었다.</p>
<ul>
<li>1부터 N까지의 서비스: 이 서비스는 각각의 마이크로서비스일 수도 있고, 크론잡일 수도 있고, 분산 시스템 컴포넌트일 수도 있다. 사용자에게 납기일을 알리고자 하는 과금 서비스, 배송 알림을 보내려는 쇼핑몰 웹사이트 등이 그 예다.</li>
<li>알림 시스템: 알림 시스템은 알림 전송/수신 처리의 핵심이다. 우선은 1개 서버만 사용하는 시스템이라고 가정해 보자. 이 시스템은 서비스 1~N에 알림 전송을 위한 API를 제공해야 하고, 제3자 서비스에 전달할 알림 페이로드를 만들어 낼 수 있어야 한다.</li>
<li>제3자 서비스: 이 서비스들은 사용자에게 알림을 실제로 전달하는 역할을 한다. 제3자 서비스와의 통합을 진행할 때 유의할 것은 <strong>확장성</strong> 이다. 쉽게 새로운 서비스를 만들거나, 기존 서비스를 쉽게 제거할 수 있어야 한다. 또 하나 고려해야 할 것은, 어떤 서비스는 다른 시장에서는 사용할 수 없을 수도 있다는 것이다. 가령 FCM은 중국에서는 사용할 수 없다. 따라서 중국 시장에서는 제이푸시, 푸시와이 같은 서비스를 사용해야만 한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/544d3264-30c5-433b-809c-894bcf71ef8e/image.png" alt=""></p>
<p>이 설계에는 아래와 같은 문제가 있다.</p>
<ul>
<li>SPOF: 알림 서비스에 서버가 하나밖에 없다는 것은, 그 서버에 장애가 생기면 전체 서비스의 장애로 이어진다는 것이다.</li>
<li>규모 확장성: 한 대 서비스로 푸시 알림에 관계된 모든 것을 처리하므로, 데이터베이스나 캐시 등 중요 컴포넌트의 규모를 개별적으로 늘릴 방법이 없다.</li>
<li>성능 병목: 알림을 처리하고 보내는 것은 자원을 많이 필요로 하는 작업일 수 있다. 예를 들어 HTML 페이지를 만들고 제3자 서비스의 응답을 기다리는 일은 시간이 많이 걸릴 가능성이 있다는 것이다. 따라서 모든 것을 한 서버로 처리하면 사용자 트래픽이 많이 몰리는 시간에는 시스템이 과부하 상태에 빠질 수 있다.</li>
</ul>
<h2 id="개략적-설계안-개선된-방안">개략적 설계안 (개선된 방안)</h2>
<p>초안의 문제점을 알아보았으니, 다음과 같은 방향으로 개선해보자.</p>
<ul>
<li>데이터베이스와 캐시를 알림 시스템의 주 서버에서 분리한다.</li>
<li>알림 서버를 증설하고 자동으로 수평적 규모 확장이 이루어질 수 있도록 한다.</li>
<li>메시지 큐를 이용해 시스템 컴포넌트 사이의 강한 결합을 끊는다.</li>
</ul>
<p>위를 적용한 아래 설계안을 봐보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/09d1ca03-9040-42a0-b0e1-6a88992654e4/image.png" alt=""></p>
<ul>
<li>1부터 N까지의 서비스: 알림 시스템 서버의 API를 통해 알림을 보낼 서비스들.</li>
<li>알림 서버: 다음 기능을 제공한다.<ul>
<li>알림 전송 API: 스팸 방지를 위해 보통 사내 서비스 또는 인증된 클라이언트만 이용 가능하다.</li>
<li>알림 검증: 이메일 주소, 전화번호 등에 대한 기본적 검증을 수행한다.</li>
<li>데이터베이스 또는 캐시 질의: 알림에 포함시킬 데이터를 가져오는 기능</li>
<li>알림 전송: 알림 데이터를 메시지 큐에 넣는다. 본 설계안의 경우 하나 이상의 메시지 큐를 사용하므로 알림을 병렬적으로 처리할 수 있다.</li>
</ul>
</li>
</ul>
<p>다음은 이메일 형태의 알림을 보내는 데 사용하는 API 예제다.</p>
<pre><code class="language-bash">POST https://api.example.com/v/sms/send</code></pre>
<p>API 호출 시 전송할 데이터(body)의 사례.</p>
<pre><code class="language-bash">{
&quot;to&quot;: [
    {
    &quot;user id&quot;: 123456
    }
],
&quot;from&quot;: {
    &quot;email&quot;: &quot;from_address@example.com&quot;
},
&quot;subject&quot;: &quot;Hello, World!&quot;,
&quot;content&quot;: [
        {
            &quot;type&quot;: &quot;text/plain&quot;,
            &quot;value&quot;: &quot;Hello, World!&quot;
        }
    ]
}</code></pre>
<ul>
<li>캐시: 사용자 정보, 단말 정보, 알림 템플릿 등을 캐시</li>
<li>데이터베이스: 사용자, 알림, 설정 등 다양한 정보를 저장</li>
<li>메시지 큐: 시스템 컴포넌트 간 의존성을 제거하기 위해 사용한다. 다량의 알림이 전송되어야 하는 경우 대비한 버퍼 역할도 한다. 본 설계안에서는 알림의 종류별로 별도 메시지 큐를 사용했다. 따라서 제 3자 서비스 가운데 하나에 장애가 발생해도 다른 종류의 알림은 정상 동작하게 된다.</li>
<li>작업 서버: 메시지 큐에서 전송할 알림을 꺼내서 제3자 서비스로 전달하는 역할을 담당하는 서비스다.</li>
<li>제3자 서비스: 설계 초안을 이야기할 때 이미 설명했다.</li>
<li>iOS, 안드로읻, SMS, 이메일 단말: 역시 위에 설명되어 있다.</li>
</ul>
<p>이 컴포넌트들이 어떻게 협력하여 알림을 전송하는지 순서을 알아보자.</p>
<ol>
<li>API를 호출하여 알림 서버로 알림을 전송</li>
<li>알림 서버는 사용자 정보, 단말 토큰, 알림 설정 같은 메타데이터를 캐시나 데이터베이스에서 가져온다.</li>
<li>알림 서버는 전송할 알림에 맞는 이벤트를 만들어서 해당 이벤트를 한 큐에 넣는다. 가령 iOS 푸시 알림 이벤트는 iOS 푸시 알림 큐에 넣어야한다.</li>
<li>작업 서버는 메시지 큐에서 알림 이벤트를 꺼낸다.</li>
<li>작업 서버는 알림을 제3자 서비스로 보낸다.</li>
<li>제3자 서비스는 사용자 단말로 알림을 전송한다.</li>
</ol>
<h1 id="3단계-상세설계">3단계 상세설계</h1>
<p>지금까지 개략적 설계를 진행하면서 알림의 종류, 연락처 정보 수집 절차, 그리고 알림 전송/수신 절차에 대해 살펴보았다. 아래 내용들을 좀 더 자세히 알아보자.</p>
<ul>
<li>안정성</li>
<li>추가로 필요한 컴포넌트 및 고려사항: 알림 템플릿, 알림 설정, 전송률 제한, 재시도 메커니즘, 보안, 큐에 보관된 알림에 대한 모니터링과 이벤트 추적 등</li>
<li>개선된 설계안</li>
</ul>
<h2 id="안정성">안정성</h2>
<p>분산 환경에서 운영될 알림 시스템을 설계할 때는 안정성을 확보하기 위한 사항 몇 가지를 <strong>반드시 고려해야 한다.</strong></p>
<h3 id="데이터-손실-방지">데이터 손실 방지</h3>
<p>알림 전송 시스템의 가장 중요한 요구사항 가운데 하나는 어떤 상황에서도 알림이 소실되면 안 된다는 것이다. 알림이 지연되거나 순서가 틀려도 괜찮지만, 사라지면 곤란하다는 것이다. 이 요구사항을 만족하려면 알림 데이터를 데이터베이스에 보관하고 <strong>재시도 매커니즘을 구현</strong>해야한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/9b5136bf-6a0e-4ea5-b264-5d303b675b39/image.png" alt=""></p>
<h3 id="알림-중복-전송-방지">알림 중복 전송 방지</h3>
<p>같은 알림이 여러번 반복되는 것을 <strong>완전히 막는 것은 가능하지 않다.</strong> 대부분의 경우 딱 한번만 전송되겠지만, 분산 시스템의 특성상 가끔은 알림이 중복되어 전송되기도 할 것이다. 그 빈도를 줄이려면 중복을 탐지하는 매커니즘을 도입하고, 오류를 신중하게 처리해야 한다.</p>
<ul>
<li>보내야 할 알림이 도착하면 그 이벤트 ID를 검사하여 이전에 본적 이 있는 이벤트인지 확인한다. 중복된 이벤트는 버리고, 그렇지 않다면 알림을 발송한다.</li>
</ul>
<h2 id="추가로-필요한-컴포넌트-및-고려사항">추가로 필요한 컴포넌트 및 고려사항</h2>
<p>지금까지 연락처 정보를 어떻게 수집하고, 알림은 어떻게 보내고 등을 알아보았는데, 알림 시스템은 사실 이보다 훨씬 복잡하다. 지금부터는 알림 템플릿, 알림 설정, 이벤트 추적, 시스템 모니터링, 처리율 제한 등 알림 시스템 구현을 위해 필요한 추가 컴포넌트들에 대해 알아보자.</p>
<h3 id="알림-템플릿">알림 템플릿</h3>
<p>대형 알림 시스템은 하루에도 수백만 건 이상의 알림을 처리한다. 그런데 그 알림 메시지 대부분은 형식이 비슷하다. 알림 템플릿은 이런 유사성을 고려하여, 알림 메시지의 모든 부분을 처믑퉈 다시 만들 필요 없도록 해 준다. 알림 템플릿은 인자나 스타일, 추적 링크를 조정하기만 하면 사전에 형식에 맞춰 알람을 만들어 내는 틀이다.</p>
<p>템플릿을 사용하면 전송될 알림들의 형식을 일관성 있게 유지할 수 있고, 오류 가능성뿐 아니라 알림 작성에 드는 시간도 줄일 수 있다.</p>
<h3 id="알림-설정">알림 설정</h3>
<p>사용자가 알림 설정을 상세히 조정할 수 있도록 해야한다. 이 정보는 알림 설정 테이블에 보관되며, 이 테이블에는 아마 아래와 같은 필드가 존재할 것이다.</p>
<table>
<thead>
<tr>
<th>user_id</th>
<th>bigInt</th>
</tr>
</thead>
<tbody><tr>
<td>channel</td>
<td>varchar</td>
</tr>
<tr>
<td>opt_in</td>
<td>boolean</td>
</tr>
</tbody></table>
<p>이와 같은 설정을 도입한 뒤에는 특정 종류의 알림을 보내기 전에 반드시 해당 사용자가 해당 알림을 켜 두었는지 확인해야 한다.</p>
<h3 id="전송률-제한">전송률 제한</h3>
<p>사용자에게 너무 많은 알림을 보내지 않도록 하는 방법은, 사용자가 받을 수 있는 알림의 빈도를 제한하는 것이다. 알림을 너무 많이 보내기 시작하면 사용자가 알림 기능을 아예 꺼버릴 수 도 있기 때문이다.</p>
<h3 id="재시도-방법">재시도 방법</h3>
<p>제3자 서비스가 알림 전송에 실패하면, 해당 알림을 <strong>재시도 전용 큐</strong>에 넣는다. 같은 문제가 계속해서 발생하면 <strong>개발자에게 통지한다(alert).</strong></p>
<h3 id="푸시-알림과-보안">푸시 알림과 보안</h3>
<p>iOS와 안드로이드 앱의 경우, 알림 전송 API는 apikey와 appSecret을 사용하여 보안을 유지한다. 따라서 인증된, 혹은 승인된 클라이언트만 해당 API를 사용하여 알림을 보낼 수 있다.</p>
<h3 id="큐-모니터링">큐 모니터링</h3>
<p>알림 시스템을 모니터링 할 때 중요한 메트릭 하나는 큐에 쌓인 알림의 개수이다. 이 수가 너무 크면 작업 서버들이 이벤트를 빠르게 처리하고 있지 못하다는 뜻이다. 그런 경우에는 작업 서버를 증설하는 게 바람직할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e6cfe504-493d-4668-a151-0e4642ab65b7/image.png" alt=""></p>
<h3 id="이벤트-추적">이벤트 추적</h3>
<p>알림 확인율, 클릭율, 실제 앱 사용으로 이어지는 비율 같은 케트릭은 사용자를 이해하는데 중요하다. 데이터 분석 서비스는 보통 이벤트 추적 기능도 제공한다. 데이터 분석 서비스는 보통 이벤트 추적 기능도 제공한다. 따라서 보통 알림 시스템을 만들면 데이터 분석 서비스와도 통합해야만 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/d91f3559-7e08-4de6-81c9-cfdc9acda219/image.png" alt=""></p>
<h2 id="수정된-설계안">수정된 설계안</h2>
<p>위에 추가로 필요한 컴포넌트 및 고려사항을 모두 반영한 설계안은 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/b3b7dcb3-9e68-45c7-966a-a507475d074d/image.png" alt=""></p>
<p>종전 설계안에 없던 많은 컴포넌트가 추가된 것을 확인할 수 있다.</p>
<ul>
<li>알림 서버에 인증과 전송률 제한 기능이 추가되었다.</li>
<li>전송 실패에 대응하기 위한 재시도 기능이 추가되었다. 전송에 실패한 알림은 <strong>재시도 큐에 넣고 지정된 횟수만큼 재시도한다.</strong></li>
<li>전송 템플릿을 사용하여 알림 생성 과정을 단순화하고 알림 내용의 <strong>일관성을 유지</strong>한다.</li>
<li>모니터링과 추적 시스템을 추가하여 시스템 상태를 확인하고 추후 시스템을 개선하기 쉽도록 하였다.</li>
</ul>
<h1 id="4단계-마무리">4단계 마무리</h1>
<p>알림은 중요 정보를 계속 알려준다는 점에서 필요불가결한 기능이다. </p>
<p>이번 장에서는 규모 확장이 쉬울 뿐 아니라 푸시 알림, SMS 메시지, 이메일 등 다양한 정보 전달 방식을 지원하는 알림 시스템을 만들어 보았다. 시스템 컴포넌트 사이의 결합도를 낮추기 위해 메시지 큐를 적극적으로 사용하였다.</p>
<p>개략적 설계안과 더불어 각 컴포넌트의 구현 방법과 최적화 기법에 대해서도 심도 있게 알아보았다.</p>
<ul>
<li>안정성: 메시지 전송 실패율을 낮추기 위해 안정적인 재시도 메커니즘을 도입하였다.</li>
<li>보안: 인증된 클라이언트만이 알림을 보낼 수 있도록 appKey, appSecret등의 메커니즘을 이용하였다.</li>
<li>이벤트 추적 및 모니터링: 알림이 만들어진 후 성공적으로 전송되기까지의 과정을 추적하고 시스템 상태를 <strong>모니터링하기 위해</strong> 이벤트를 추적하고 모니터링할 수 있는 시스템을 통합하였다.</li>
<li>사용자 설정: 사용자가 알림 수신 설정을 조정할 수 있도록 하였다. 따라서 알림을 보내기 전 반드시 해당 설정을 확인하도록 시스템 설계를 변경하였다.</li>
<li>전송률 제한: 사용자에게 알림을 보내는 빈도를 제한할 수 있도록 하였다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[웹 크롤러 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%9F%AC-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EC%9B%B9-%ED%81%AC%EB%A1%A4%EB%9F%AC-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 07 Aug 2023 03:22:36 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>검색엔진에서 널리 쓰는 기술인 웹크롤러를 설계해보자.</p>
<p>웹 크롤러는 웹에 새로 올라오거나 갱신된 콘텐츠를 찾아내는 것이 <strong>주된 목적</strong>이다.</p>
<p>또한, 크롤러는 아래와 같은데에 이용된다.</p>
<ul>
<li><p>검색 엔진 인덱싱(Googlebot은 구글의 검색 엔진이 사용하는 웹 크롤러임)</p>
</li>
<li><p>웹 아카이빙: 나중에 사용할 목적으로 장기보관하기 위해 웹에서 정보를 모으는 절차를 말함. 많은 국립 도서관이 크롤러를 돌려 웹 사이트를 아카이빙 한다.(미국 국회 도서관의 EU 웹 아카이브)
<img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/a8265d8a-ea0b-40ef-a536-7d7d9a3ce309/image.png" alt=""></p>
</li>
<li><p>웹 마이닝: 웹 마이닝을 통해 인터넷에서 유용한 지식을 도출해 낼 수 있다. 일례로, 유명 금융 기업들은 크롤러를 사용해 주주총회 자료나 연차 보고서를 다운 받아 기업의 핵심 사업 방향을 알아내기도 한다.</p>
</li>
<li><p>웹 모니터링: 크롤러를 사용하면 인터넷에서 저작권이나 상푱권이 침해되는 사례를 <strong>모니터링</strong> 할 수 있따. 일례로 디지마크 회사는 웹 크롤러를 사용해 불법으로 사용된 저작물을 찾아내서 보고한다.</p>
</li>
</ul>
<p>웹 크롤러의 복잡도는 웹 크롤러가 처리해야 하는 데이터의 규모에 따라 달라진다. 몇 시간이면 끝낼 수 있는 작업일 수 도 있고, 별도의 엔지니어링 팀을 꾸려서 지속적으로 관리하고 개선해야 하는 초대형 프로젝트 일 수 도 있다.</p>
<h1 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h1>
<p>웹 크롤러의 기본 알고리즘</p>
<ol>
<li>URL 집합이 입력으로 주어지면, 해당 URL들이 가리키는 모든 웹 페이지를 다운로드한다.</li>
<li>다운받은 웹 페이지에서 URL을 추출</li>
<li>추출된 URL들을 다운로드할 URL 목록에 추가하고 위의 과정을 처음부터 반복</li>
</ol>
<p>기본 알고리즘을 보면 단순해 보이지만, 정말 이렇게 간단할까? 엄청난 <strong>규모 확장성을 갖는</strong> 웹 크롤러를 설계하는 것은 엄청나게 어려운 작업이다. 가상 면접 사례로 예시를 들어보고, 설계 범위를 좁혀보자.</p>
<p>위와 같은 질문들을 하여 요구사항을 명확히 하는 반면, 웹 크롤러가 만족시켜야 할 속성에 주의를 기울어야 한다.</p>
<ul>
<li>규모 확장성: 웹에는 수십억 개의 페이지가 존재한다. 따라서 <strong>병행성</strong>을 활용하면 효과적으로 웹 크롤링을 할 수 있을 것이다.</li>
<li>안정성: 웹은 함정으로 가득하다. 잘못 작성된 HTML, 아무 반응 없는 서버, 장애, 악성 코드가 붙어 있는 링크 등. 크롤러는 이런 비정상적 입력이나 환경에 잘 대응해야 한다.</li>
<li>예절: 크롤러는 수집 대상 웹 사이트에 <strong>짧은 시간 너무 많은 요청을 보내면 안된다.</strong></li>
<li>확장성: 새로운 형태의 콘텐츠를 지원하기가 쉬어야 한다. 예를 들면, 이미지 파일도 크롤링 하고싶다고 하면, 이를 위해 전체 시스템을 새로 설계해야하는 상황이면 곤란하다.</li>
</ul>
<h2 id="개략적-규모-추정">개략적 규모 추정</h2>
<p>아래의 추정치는 많은 가정으로부터 나온 것이다. 아래와 같이 추정해보자.</p>
<ul>
<li>매달 10억 개의 웹 페이지를 다운로드한다.</li>
<li>QPS = 10억/30일/24시간/3600초=대략 400페이지/초</li>
<li>최대 QPS = 2 * QPS = 800</li>
<li>웹 페이지의 크기 평균은 500k라고 가정</li>
<li>10억 페이지*500k=500TB/월.</li>
<li>1개월치가 500TB이므로, 5년치를 보관한다면 결국 500TB<em>12개월</em>5년=30PB의 저장용량이 필요하다.</li>
</ul>
<h1 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h1>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/ddf72b6c-b750-42d8-8a4a-1f8dd2233008/image.png" alt=""></p>
<p>우선 이 다이어그램에 등장하는 컴포넌트 각각이 어떤 기능을 수행하는지 살펴보자.</p>
<h2 id="시작-url-집합">시작 URL 집합</h2>
<p>전체 웹을 크롤링해야 하는 경우에는 시작 URL을 고를 때 <strong>크롤러가 가능한 많은 링크를 탐색할 수 있도록 하는 URL을 고르는 것이 바람직</strong>하다. 일반적으로는 <strong>전체 URL 공간을 작은 부분집합으로 나누는 전략을 사용</strong>. 또 다른 방법은 주제별로 다른 시작 URL을 사용하는 것이다. 예를 들어 URL공간을 <strong>쇼핑, 스포츠, 건강</strong> 등등의 주제별로 세분화하고 그 각각에 다른 시작 URL을 사용하는 것.</p>
<p>시작 URL을 무엇으로 쓸 것 인지에 대한 질문에 정답은 없다.</p>
<h2 id="미수집-url-저장소">미수집 URL 저장소</h2>
<p>대부분의 현대적 웹 크롤러는 크롤링 상태를</p>
<ol>
<li>다운로드 할 URL</li>
<li>다운로드된 URL</li>
</ol>
<p>로 나누어 관리한다.</p>
<p>이중 1. 다운로드 할 URL을 관리하는 컴포넌트를 <strong>미수집 URL 저장소</strong>라고 부른다. 그냥 FIFO큐라고 생각하면 된다.</p>
<h2 id="html-다운로더">HTML 다운로더</h2>
<p>HTML 다운로더는 인터넷에서 웹 페이지를 다운로드하는 컴포넌트이다. 다운로드할 페이지의 URl은 미수집 URL 저장소가 제공한다.</p>
<h2 id="도메인-이름-변환기">도메인 이름 변환기</h2>
<p>웹 페이지를 다운받으려면 URL을 IP주소로 변환하는 절차가 필요한데, HTML다운로더는 도메인 이름 변환기를 이용하여 URL에 대응되는 IP주소를 알아낸다.</p>
<h2 id="콘텐츠-파서">콘텐츠 파서</h2>
<p>웹 페이지를 다운로드하면 파싱과 검증 절차를 걸쳐야 하는데, 왜냐하면 이상한 웹 페이지는 문제를 일으킬 수도 있는데다 저장공간만 낭비하게 되기 때문이다. <strong>크롤링 서버 안에 콘텐츠 파서를 구현하면</strong> <strong>문제가 될 수 있으므로, 독리보딘 컴포넌트로</strong> 만들자.</p>
<h2 id="중복-콘텐츠인가">중복 콘텐츠인가?</h2>
<p>웹에 공개된 연구 자료에 의하면 29%는 중복된 콘텐츠이다. 중복된 콘텐츠를 피하기 위한 가장 간단한 방법은 두 HTML 파일을 비교하면 되지만, 10억개 정도에 달하는 경우에는 <strong>느리고 비효율적이어서</strong> 웹 페이지의 해시 값을 비교하는 것이 효율적인 방법일 것이다.</p>
<h2 id="콘텐츠-저장소">콘텐츠 저장소</h2>
<p>콘텐츠 저장소는 HTML 문서를 보관하는 시스템이다. 저장소를 구현하는데 쓰일 기술을 고를 때는 저장할 데이터의 유형, 크기, 저장소 접근 빈도, 데이터의 유효 기간 등을 고려해야한다. 아래에서 할 설계는 메모리와 디스크를 동시에 사용할 때의 설계 예시다.</p>
<ul>
<li>데이터 양이 너무 많으므로 대부분의 콘텐츠는 디스크에 저장</li>
<li>인기 있는 콘텐츠는 메모리에 두어 접근 지연시간을 줄인다</li>
</ul>
<h2 id="url-추출기">URL 추출기</h2>
<p>URL 추출기는 HTML 페이지를 파싱하여 링크들을 골라내는 역할을 한다. 상대 경로는 앞에 도메인 주소를 붙여 <strong>절대 경로로 변환</strong>한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/6ed4de83-96fe-4ae5-ba50-ecea273a8169/image.png" alt=""></p>
<h2 id="url-필터">URL 필터</h2>
<p>URL 필터는 특정한 콘텐츠 타입이나 파일 확장자를 갖는 URL, 접속 시 오류가 발생하는 URL, 접근 제외 목록에 포함된 URL 등을 크롤링 대상에서 배제하는 역할</p>
<h2 id="이미-방문한-url">이미 방문한 URL?</h2>
<p>이미 방문한 URL이나 미수집 URL 저장소에 보관된 URL을 추적할 수 있도록 해야한다. 이미 방문한 적 있는 URL인지 추적하면 같은 URL을 여러 번 처리하는 일을 방지할 수 있으므로 서버 부하를 줄이고 시스템이 <strong>무한 루프에 빠지는</strong>일을 방지할 수 있다.</p>
<p>해당 방법으로는 <strong>블룸 필터나 해시테이블</strong>이 가장 널리 쓰인다.</p>
<h2 id="url-저장소">URL 저장소</h2>
<p>URL 저장소는 이미 방문한 URL을 보관하는 저장소이다.</p>
<p>이제 위에서 보았던 컴포넌트들이 상호 작용하는 작업 흐름 관점에서 살펴보자.</p>
<h2 id="웹-크롤러-작업-흐름">웹 크롤러 작업 흐름</h2>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e10d5346-7e48-4e97-9244-a8ef9cb6aa09/image.png" alt=""></p>
<ol>
<li>시작 URL들이 미수집 URL 저장소에 저장한다.</li>
<li>HTML 다운로더는 미수집 URL 저장소에서 URL 목록을 가져온다</li>
<li>HTML 다운로더는 도메인 이름 변환기를 사용하여 URL의 IP 주소를 알아내고, 해당 IP 주소로 접속하여 웹 페이지를 다운받는다.</li>
<li>콘텐츠 파서는 다운된 HTML 페이지를 파싱하여 올바른 형식을 갖춘 페이지인지 검증한다.</li>
<li>콘텐츠 파싱과 검증이 끝나면 중복 콘텐츠인지 확인하는 절차를 개시한다.</li>
<li>중복 콘텐츠인지 확인하기 위해서, 해당 페이지가 이미 저장소에 있는지 본다.<ul>
<li>이미 저장소에 있는 콘텐츠인 경우에는 처리하지 않고 버린다.</li>
<li>저장소에 없는 콘텐츠인 경우 저장소에 저장한 뒤 URL 추출기로 전달</li>
</ul>
</li>
<li>URL 추출기는 해당 HTML 페이지에서 링크를 골라냄</li>
<li>골라낸 링크를 URL 필터로 전달</li>
<li>필터링이 끝나고 남은 URL만 중복 URL 판별 단계로 전달</li>
<li>이미 처리한 URL인지 확인하기 위하여 URL 저장소에 보관된 URL인지 판단. 이미 저장소에 있는 URL은 버린다</li>
<li>저장소에 없는 URL은 URL 저장소에 저장할 뿐 아니라 미수집 URL 저장소에도 전달한다.</li>
</ol>
<h1 id="3단계-상세설계">3단계 상세설계</h1>
<ul>
<li>DFS vs BFS</li>
<li>미수집 URL 저장소</li>
<li>안정성 확보 전략</li>
<li>확장성 확보 전략</li>
<li>문제 있는 콘텐츠 감지 및 회피 전략</li>
</ul>
<h2 id="dfs를-쓸-것인가-bfs를-쓸-것인가">DFS를 쓸 것인가, BFS를 쓸 것인가</h2>
<p>웹은 유향 그래프나 같다. DFS는 그래프의 크기가 어느 정도로 깊<strong>숙이 가게 될지 가늠하기 어렵다.</strong></p>
<p>따라서 웹 크롤러는 보통 BFS를 사용한다. BFS는 FIFO 큐를 사용하는 알고리즘이다. 이 큐의 한쪽으로는 탐색한 URL을 집어넣고, 다른 한쪽으로는 꺼내기만 한다. 이렇게 하면 두 가지 문제점이 있다.</p>
<ul>
<li>한 페이지에서 나오는 링크의 상당수는 같은 서버로 되돌아간다. 아래의 그림을 보면,</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/53f6cecd-1560-4d1f-866b-7e3bb7df7732/image.png" alt=""></p>
<p><a href="http://wikipedia.com">wikipedia.com</a> 페이지에서 추출한 모든 링크는 내부 링크, 즉 동일한 wikipedia.com 서버의 다른 페이지를 참조하는 링크다. 결국 크롤러는 같은 호스트에 속한 많은 링크를 다운받느라 <strong>바빠지는데</strong> 이 때 링크들을 <strong>병렬적</strong>으로 처리하게 된다면 위키피디아 서버는 수많은 요청으로 과부하에 걸리개 된다. 이런 크롤러를 예의 없는 크롤러로 간주한다.</p>
<ul>
<li>표준적 BFS 알고리즘은 URL 간에 우선순위를 두지 않는다. 처리 순서 관점에서 모든 페이지를 모두 공평하게 대우한다는 뜻인데, 모든 웹 페이지가 같은 수준의 품질, 같은 수준의 중요성을 갖지는 않기에 페이지의 순위, 사용자 트래픽의 양, 업데이트 빈도 등 여러 가지 척에 따라 우선수위를 구별하는 것이 적합하다.</li>
</ul>
<h2 id="미수집-url-저장소-1">미수집 URL 저장소</h2>
<p>미수집 URL 저장소를 활용하면 이런 문제를 좀 더 쉽게 처리할 수 있다. URL 저장소는 다운로드할 URL을 보관하는 장소인데, 이 저장소를 잘 구현한다면 <strong>예의를 갖춘 크롤러</strong>, URL 사이의 우선순위와 신선도를 구별하는 크롤러를 구현할 수 있다. 미수집 URL 저장소의 구현 방법에 대해서는 논문도 다수 나와 있는데, 이 연구 결과중 중요한 것을 중요해보면 아래와 같이 요약할 수 있다.</p>
<h3 id="예의">예의</h3>
<p>웹 크롤러는 수집 대상 서버로 짧은 시간안에 너무 많은 요청을 보내는 것을 삼가해야 한다.
너무 많은 요청을 보내는 것은 ‘무례한’ 일이며, 때로는 이를 Dos공격으로 간주되기도 한다.
아무런 안전장치가 없는 웹 크롤러의 경우, 초당 수천 건의 페이지 요청을 동일한 웹 사이트로 보내어 사이트를 마비시켜버릴 수도 있다.</p>
<p>예의 바른 크롤러를 만드는데 있어서 지켜야할 <strong>한가지 원칙</strong></p>
<ul>
<li>동일 웹 사이트에 대해서는 한 번에 한 페이지만 요청을 한다.</li>
</ul>
<p>위 요구사항을 만족 시키려면, 웹 사이트의 <strong>호스트명</strong>과 다운로드를 수행하는 <strong>작업 스레드</strong>사이의 관계를 유지하면 된다. 즉, 다운로드 스레드는 별도 FIFO 큐를 가지고 있어서, 해당 큐에서 꺼낸 URL만 다운로드한다.</p>
<p>아래는 이 원칙을 지키기 위한 설계를 보여준다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/63a4b76a-a87d-473e-bba6-d46a76342576/image.png" alt=""></p>
<ul>
<li>큐 라우터: 같은 호스트에 속한 URL은 언제나 같은 큐로 가도록 보장하는 역할을 한다.</li>
<li>매핑 테이블: 호스트 이름과 큐사이의 관계를 보관하는 테이블(위의 표다)</li>
<li>FIFO 큐: 같은 호스트에 속한 URL은 언제나 같은 큐에 보관된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/0d253e73-f26a-4c02-bc8a-9a92db3f6056/image.png" alt=""></p>
<ul>
<li>큐 선택기: 큐 선택기는 큐들을 순회하면서 큐에서 URL을 꺼내서 해당 큐에서 나온 URL을 다운로드하도록 지정된 작업 스레드에 전달하는 역할</li>
<li>작업 스레드: 작업 스레드는 전달된 URL을 다운로드하는 작업을 수행한다. 전달된 URL은 순차적으로 처리될 것이며, 작업들 사이에는 일정한 <strong>지연시간을 둘 수 있다.</strong></li>
</ul>
<h3 id="우선순위">우선순위</h3>
<p>예를들어, 애플(Apple) 제품에 대한 사용자 의견이 올라오는 포럼의 한 페이지가 애플 홈페이지와 같은 중요도를 갖는다고 보기는 어려울 것이다. 둘다 ‘애플’이 키워드로 등장하기는 했지만, 크롤러 입장에서는 중요한 페이지 즉, 애플의 홈페이지를 먼저 수집하도록 하는 것이 바람직할 것이다.</p>
<p>유용성에 따라 URL의 우선순위를 나눌 때는 페이지랭크, 트래픽의 양, 갱신 빈도 등 다양한 척도를 사용할 수 있다. 아래 그림의 순위결정장치는 URL의 우선순위를 정해주는 컴포넌트이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/89e81dde-2e0d-427c-b9cf-9973c4f62947/image.png" alt=""></p>
<ul>
<li>순위 결정장치: URL을 입력으로 받아 우선순위를 계산함</li>
<li>큐: 우선순위별로 큐가 하나씩 할당됨. 우선순위가 높으면 선택될 확률도 올라감</li>
<li>큐 선택기: 임의 큐에서 처리할 URL을 꺼내는 역할을 담당. 순위가 높은 큐에서 더 자주 꺼내도록 되어 있다.</li>
</ul>
<p>아래 그림은 이를 반영한 전체 설계이다. 그림을 보면 두 개의 모듈이 존재하는 것을 볼 수 있다.</p>
<ul>
<li>전면 큐: 우선순위 결정 과정을 처리</li>
<li>후면 큐: 크롤러가 예의 바르게 동작하도록 보증한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/fb5da30d-a2a4-4d77-bf84-86ed8c5c3989/image.png" alt=""></p>
<h3 id="신선도">신선도</h3>
<p>웹 페이지는 수시로 추가되고, 삭제되고, 변경된다. 따라서 데이터의 신선함을 유지하기 위해서는 이미 다운로드한 페이지라고 해도 주기적으로 재수집할 필요가 있다. 그러나 모든 URL을 재수집하는 것은 많은 시간과 자원이 필요한 작업이다. 이 작업을 최적화하기 위한 전략으로는 다음과 같은 것이 있다.</p>
<ul>
<li>웹 페이지의 변경 이력활용</li>
<li>우선순위를 활용하여, 중요한 페이지는 좀 더 자주 재수집</li>
</ul>
<h3 id="미수집-url-저장소를-위한-지속성-저장장치">미수집 URL 저장소를 위한 지속성 저장장치</h3>
<p>검색 엔진을 위한 크롤러의 경우, 처리해야 하는 URL의 수는 수억 개에 달한다. 그러니 그 모두를 메모리에 보관하는 것은 안정성이나 규모 확장성 측면에서 <strong>바람직하지 않다.</strong> 전부 디스크에 저장하는 것도 좋은 방법은 아닌데, 느려서 쉽게 성능 병목지점이 되기 때문이다.</p>
<p>따라서 아래 설계안은 절충안을 택했다. 대부분의 URL은 디스크에 두지만 IO 비용을 줄이기 위해 메모리 버퍼에 큐를 두는 것이다. 버퍼에 있는 데이터는 주기적으로 디스크에 기록할 것이다.</p>
<h2 id="html-다운로더-1">HTML 다운로더</h2>
<p>HTML 다운로더는 HTTP 프로토콜을 통해 웹 페이지를 내려 받는데, 다운로더에 대해 알아보기 전 로봇 제외 프로토콜 부터 살펴보자.</p>
<h3 id="robotstxt">Robots.txt</h3>
<p>로봇 제외 프로토콜이라고 부르기도 하는 Robots.txt는 웹 사이트가 크롤러와 소통하는 표준적 방법이다. 이 파일에는 크롤러가 수집해도 되는 페이지 목록이 들어 있다. 따라서 웹 크롤러가 웹 사이트를 긁어 가기 전에 크롤러는 해당 파일에 나열된 규칙을 먼저 확인해야 한다.</p>
<p>Robots.txt 파일을 거푸 다운로드하는 것을 피하기 위해, 이 파일은 주기적으로 다시 다운받아 캐시에 보관할 것이다. 아마존의 Robotstxt (<a href="https://www.amazon.com/robots.txt">/https://www.amazon.com/robots.txt</a>) 파일을 예로 보면 다음과 같은 규칙이 나열되어 있다.</p>
<aside>
💡 아마존의 Robots.txt
https://www.amazon.com/robots.txt
아래 디렉터리의 내용은 다운받을 수 없다.

<p>User-agent: Googlebot
Disallow: /creatorhub/*
Disallow: /rss/people/<em>reviews</em>
Disallow: /gp/pdp/rss/reviews
Disallow: /gp/cdp/member-reviews
Disallow: /gp/aw/cr</p>
</aside>

<p>Robots.txt도 중요하지만 HTML 다운로더를 설계할 때는 성능최적화도 아주 중요하다.</p>
<h3 id="성능-최적화">성능 최적화</h3>
<p>아래는 HTML 다운로더에 사용할 수 있는 성능 최적화 기법들이다.</p>
<ol>
<li>분산 크롤링</li>
</ol>
<p>성능을 높이기 위해 크롤링 작업을 여러 서버에 분산하는 방법. 각 서버는 여러 스레드를 돌려 다운로드 작업을 처리한다. 이 구성을 위해 URL 공간은 작은 단위로 분할하여, 각 서버는 그중 일부의 다운로드를 담당하도록 한다. 아래 그림을 봐보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/55a42c21-97a6-4b36-902f-4161f719eecd/image.png" alt=""></p>
<ol>
<li>도메인 이름 변환 결과 캐시</li>
</ol>
<p>도메인 이름 변환기는 크롤러 성능의 병목 중 하나인데, 이는 DNS 요청을 보내고 결과를 받는 작업의 <strong>동기적 특성 때문</strong>. DNS 요청의 결과를 받기 전까지는 다음 작업을 진행할 수 없다. DNS 요청이 처리되는 보통 10ms에서 200ms가 소요된다. 크롤러 스레드 가운데 어느 하나라도 이 작업을 하고 있다면 다른 스레드의 DNS 요청은 전부 블록된다. 따라서 DNS 조회 결과로 얻어진 도메인 이름과 IP 주소 사이의 관계를 캐시에 보관해 놓고 크론 잡(cron job)등을 돌려 주기적으로 갱신하도록 해 놓으면 <strong>성능을 효과적으로 높일수 있다.</strong></p>
<ol>
<li>지역성</li>
</ol>
<p>크롤링 작업을 수행하는 서버를 직역별로 분산하는 방법이다. 크롤링 서버가 크롤링 대상 서버와 지역적으로 가까우면 페이지 다운로드 시간은 줄어들 것이다. 지역성을 활용하는 전략은 크롤 서버, 캐시, 큐, 저장소 등 대부분의 컴포넌트에 적용 가능하다.</p>
<ol>
<li>짧은 타임아웃</li>
</ol>
<p>어떤 웹 서버는 응답이 느리거나 아예 응답하지 않는다. 이런 경우에 대기시간이 길어지면 좋지 않으므로, 최대 얼마나 기다릴지를 미리 정해두는 것이다. 이 시간 동안 서버가 응답하지 않으면 크롤러는 해당 페이지 다운로드를 중단하고 다음 페이지로 넘어간다.</p>
<h2 id="안정성">안정성</h2>
<p>최적화된 성능뿐 아니라 안정성도 다운로더 설계 시 중요하게 고려해야 할 부분이다. 시스템 안정성을 향상시키기 위한 접근법 가운데 중요한 몇 가지는 아래와 같다.</p>
<ul>
<li>안정 해시: 다운로더 서버들에 부하를 분산할 때 적용 가능한 기술. 이 기술을 이용하면 다운로더 서버를 쉽게 추가하고 삭제할 수 있다.</li>
<li>크롤링 상태 및 수집 데이터 저장: 장애가 발생한 경우에도 쉽게 복구할 수 있도록 크롤링 상태와 수집된 데이터를 지속적 저장장치에 기록해 두는 것이 바람직하다. 저장된 데이터를 로딩하고 나면 중단되었던 크롤링을 쉽게 재시작할 수 있다.</li>
<li>예외 처리: 대규모 시스템에서 에러는 불가피할뿐 아니라 흔하게 벌어지는 일이다. 예외가 발생해도 전체 시스템이 중단되는 일 없이 그 작업을 우아하게 이어나갈 수 있어야 함.</li>
<li>데이터 검증: 시스템 오류를 방지하기 위한 중요 수단 가운데 하나다.</li>
</ul>
<h2 id="확장성">확장성</h2>
<p>진화하지 않는 시스템은 없는법이다. 이런 시스템을 설계할 때는 새로운 형태의 콘텐츠를 쉽게 지원할 수 있도록 신경 써야 한다. 본 예제의 경우에는 새로운 모듈을 끼워 넣음으로써 새로운 형태의 콘텐츠를 지원할 수 있도록 설계하는 방법을 아래 그림으로 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/2c3a0759-4fe1-4188-bdc2-dc2ec9ea148e/image.png" alt=""></p>
<ul>
<li>PNG 다운로더는 PNG 파일을 다운로드하는 플러그인 모듈</li>
<li>웹 모니터는 웹을 모니터링하여 저작권이나 상표권이 침해되는 일을 막는 모듈</li>
</ul>
<h2 id="문제-있는-콘텐츠-감지-및-회피">문제 있는 콘텐츠 감지 및 회피</h2>
<p>이번 절에서는 중복이거나 의미 없는, 또는 유해한 콘텐츠를 어떻게 감지하고 시스템으로부터 차단할지 살펴보자.</p>
<ol>
<li>중복 콘텐츠</li>
</ol>
<p>앞서 살펴본 대로, 웹 콘텐츠의 30%는 중복이다. 해시나 체크섬을 사용하면 중복 콘텐츠를 보다 쉽게 탐지할 수 있다.</p>
<ol>
<li>거미 덫</li>
</ol>
<p>거미 덫은 크롤러를 무한 루프에 빠뜨리도록 설계한 웹 페이지이다. 예를 들어, 다음과 같이 무한히 깊은 디렉터리 구조를 포함하는 링크가 있다고 해 보자.</p>
<ul>
<li>spidertrapexample.com/foo/bar/foo/bar/foo/bar/foo/bar/foo/bar/foo/bar</li>
</ul>
<p>이런 덫은 URL의 최대 길이를 제한하면 회피할 수 있다. 하지만 모든 종류의 덫을 피할 수 있는 만능 해결책은 없다. 이런 덫이 설치된 웹 사이트인지 알아내는 과정은 어렵지 않은데, 기이할 정도로 많은 웹 페이지를 가지고 있는 것이 <strong>일반적</strong>이라서다. 하지만 덫을 <strong>자동으로 피해가는 알고리즘</strong>을 만들어 내는 것은 까다롭다.</p>
<p>한 가지 방법은 사람이 수작업으로 덫을 확인하고 찾아낸 후에 덫이 있는 사이트를 크롤러 탐색 대상에서 제외하거나 URL 필터 목록에 걸어두는 것이다.</p>
<ol>
<li>데이터 노이즈</li>
</ol>
<p>어떤 콘텐츠는 거의 가치가 없다. 광고나 스크립트 코드, 스팸 URL 같은 것이 가치가 없다 라고 볼 수 있다. 이런 콘텐츠는 크롤러에게 도움될것이 없으므로, 가능하다면 제외해야 한다.</p>
<h1 id="마무리">마무리</h1>
<p>이번 장에서 우리는 좋은 크롤러가 갖추어야 하는 특성을 살펴보았다. 규모 확장성, 예의, 확장성, 안정성 등이다. 아울러 크롤러의 설계안을 제시하고, 핵심 컴포넌트에 쓰이는 기술들도 알아보았다. 규모 확장성이 뛰어난 웹 크롤러 설계 작업은 단순하지 않다. 웹이 워낙 방대한 데다, 수없이 많은 덫이 도사리고 있기 때문이다. 추가적으로 웹 크롤러를 설계할 때 생각해보면 좋은것드릉ㄹ 살펴보고 마무리해 보자.</p>
<ul>
<li>서버 측 렌더링(server-side rendering): 많은 웹사이트가 자바스크립트, AJAX 등의 기술을 사용해서 링크를 즉석에서 만들어 낸다. 그러니 웹 페이지를 그냥 있는 그대로 다운받아서 파싱해보면 <strong>동적으로 생성되는 링크는 발견할 수 없다.</strong> 이 문제는 페이지를 파싱하기 전에 서버 측 렌더링(동적 렌더링 dynamic rendering이라고도 불린다)을 적용하면 해결할 수 있다.</li>
<li>원치 않는 페이지 필터링: 저장 공간 등 크롤링에 소요되는 자원은 유한하기 때문에 스팸 방지 컴포넌트를 두어 품질이 조약하거나 스팸성인 페이지를 걸러내도록 해 두면 좋다.</li>
<li>데이터베이스 다중화 및 샤딩: 다중화나 샤딩 같은 기법을 적용 하면 데이터 계층의 강ㅇ성, 규모 확장성, 안정성이 향상된다.</li>
<li>수평적 규모 확장성: 대규모의 크롤링을 위해서는 다운로드를 실행할 서버가 수백 혹은 수천 대 필요하게 될 수도 있다. 수평적 규모 확장성을 달성하는 데 중요한 것은 상태정보를 유지하지 않도록 하는것, 즉 무상태 서버로 만드는 것이다.</li>
<li>가용성, 일관성, 안정성: 이런 개념들은 성공적인 대형 시스템을 만들기 위해 필수적으로 고려해야 하는 것들이다. 이 개념들에 대해서는 1장에서 자세히 다뤘으니 기억이 가물가물하다면 다시 한번 읽어보자.</li>
<li>데이터 분석 솔루션: 데이터를 수집하고 분석하는 것은 어느 시스템에게나 중요하다. 시스템을 세밀히 조정하기 위해서는 이런 데이터와 그 분석 결과가 필수적이라서다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[URL 단축기 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/URL-%EB%8B%A8%EC%B6%95%EA%B8%B0-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/URL-%EB%8B%A8%EC%B6%95%EA%B8%B0-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 07 Aug 2023 03:16:07 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<h1 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h1>
<p>요구사항 도출</p>
<ol>
<li>URL 단축: 주어진 긴 URL을 훨씬 짧게 줄인다.</li>
<li>URL 리디렉션: 축약된 URL로 HTTP 요청이 오면 원래 URL로 안내</li>
<li>높은 가용성과 규모 확장성, 그리고 장애 감내가 요구됨</li>
</ol>
<p><strong>개략적 추정</strong></p>
<ul>
<li>쓰기 연산: 매일 1억 개의 단축 URL 생성</li>
<li>초당 쓰기 연산: 1억/24/3600 = 1160</li>
<li>읽기 연산: 읽기 연산과 쓰기 연산은 10:1이라고 할때 초당 11600회 발생한다</li>
<li>URL 단축 서비스를 10년간 운영한다고 가정하면 1억<em>365</em>10 개의 레코드를 보관해야 한다.</li>
<li>축약 전 URL의 평균 길이는 100이라고 하자.</li>
<li>따라서 10년 동안 필요한 필요한 저장 용량은 3650억*100바이트 = 36.5TB이다.</li>
</ul>
<h1 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h1>
<h3 id="api-엔드포인트">API 엔드포인트</h3>
<p>클라이언트가 서버가 제공하는 API 엔드포인트를 통해 서버와 통신한다. 우리는 이 엔드포인트를 REST 스타일로 설계해볼 것이다.</p>
<ol>
<li>URL 단축용 엔드포인트: 새 단계에 URL을 생성하고자 하는 클라이언트는 이 엔드포인트에 단축할 URL을 인자로 실어서 POST 요청을 보내야 한다. 이 엔드포인트는 다음과 같은 형태를 띤다.</li>
</ol>
<p>POST /api/v1/data/shorten</p>
<ul>
<li>인자: {longUrl: longURLstring}</li>
<li>반환: 단축 URL</li>
</ul>
<ol>
<li>URL 리디렉션용 엔드포인트: 단축 URL에 대해서 HTTP 요청이 오면 원래 URL로 보내주기 위한 용도의 엔드포인트, 다음과 같은 형태를 띤다.</li>
</ol>
<p>GET /api/v1/shortUrl</p>
<ul>
<li>반환: HTTP 리디렉션 목적지가 될 원래 URL</li>
</ul>
<h2 id="url-리디렉션">URL 리디렉션</h2>
<p>아래 그림은 단축 URL을 입력하면 어떤 일이 발생하는지 보여준다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/6aa5664a-80a6-4b98-87b7-2ae4d5cca050/image.png" alt=""></p>
<p>축 URL을 받은 서버는 그 URL을 원래 바꿔서 301 응답의 Location헤더에 넣어 반환한다.</p>
<p>아래 그림은 클라이언트와 서버 사이의 통신 절차를 좀 더 자세히 보여준다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/02287a15-3ffd-4504-88ab-68a249047884/image.png" alt=""></p>
<p>여기서의 차이는 301이냐 아니면 302이냐의 차이다.</p>
<ul>
<li>301: 이 응답은 해당 URL에 대한 HTTP 요청의 처리 책임이 영구적으로 Location 헤더에 반환된 URL로 이전되었다는 응답이다. 영구적으로 이전되었으므로, 이 응답을 <strong>캐시</strong>한다. 따라서 추후 같은 단축 URL에 요청을 보내면 브라우저는 <strong>캐시된 원래 URL로</strong> 요청을 보내게 된다.</li>
<li>302: 이 응답은 주어진 URL로의 요청이 ‘일시적으로’ Location 헤더가 지정하는 URL에 의해 처리되어야 한다는 응답이다. 따라서 클라이언트의 요청은 언제나 단축 URL 서버에 먼저 보내진 후에 원래 URL로 리디렉션 되어야 한다.</li>
</ul>
<p>이 두 방법은 각기 다른 장단점을 갖고 있다. 서버 <strong>부하를 줄이는 것이 중요</strong>하다면 301를 사용하는 것이 좋은데 첫 번째 요청만 단축 URL 서버로 전송될 것이기 때문이다. 하지만 트래픽 분석이 중요할 때는 302를 사용하는 것이 클릭 발생률이나 발생 위치를 추적하는 데 좀 더 좋을 것이다.</p>
<p>URL 리디렉션을 구현하는 가장 <strong>직관적이 방법</strong>은 해시 테이블을 사용하는 것이다. 해시 테이블에 &lt;단축 URL, 원래 URL&gt;의 쌍을 저장한다면 URL 리디렉션은 다음과 같이 구현될 수 있다.</p>
<ul>
<li>원래 URL = hashTable.get(단축 URL)</li>
<li>301 또는 302 응답 Location 헤더에 원래 URL을 넣은 후 전송</li>
</ul>
<h2 id="url-단축">URL 단축</h2>
<p>아래 그림을 보면</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/19775414-d28c-4cc9-aad2-9915bee571b2/image.png" alt=""></p>
<p>결국 중요한 것은 긴 URl을 해시 값으로 대응시킬 수 있는 fx이다.</p>
<p>해시 함수는 다음과 같은 요구사항을 만족해야 한다.</p>
<ul>
<li>입력으로 주어지는 긴 URL이 다른 값이면 해시 값도 달라야함</li>
<li>계산된 해시 값은 원래 입력으로 주어졌던 긴 URL로 복원될 수 있어야함</li>
</ul>
<h1 id="3단계-상세-설계">3단계 상세 설계</h1>
<p>데이터 모델, 해시 함수, URL 단축 및 리디렉션에 관한 구체적인 설계안을 만들어보자.</p>
<h2 id="데이터-모델">데이터 모델</h2>
<p>그동안은 모든 것을 해시 테이블에 저장했는데, 이는 초기 전략으론 괜찮지만 실제 서비스에 쓰기에는 곤란하다. <strong>메모리는 유한한 데다 비싸기 때문</strong>. 더 나은 방법은 &lt;단축 URL, 원래 URL&gt;의 순서쌍을 관계형 데이터베이스에 저장하는 것이다. 아래 그림은 테이블의 간단한 설계 사례다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/2cee208c-ed84-4ee3-92a8-5cb5a163cd59/image.png" alt=""></p>
<h2 id="해시-함수">해시 함수</h2>
<p>해시 함수는 원래 URL을 단축 URL로 변환하는 데 쓰인다. 편의상 해시 함수가 계산하는 단축 URL 값을 hashValue라고 지칭하겠다.</p>
<h2 id="해시-값-길이">해시 값 길이</h2>
<p>hashValue는 요구사항에서 [0-9, a-z, A-Z]의 문자들로 구성하므로, 사용할 수 있는 문자의 개수는 10+10+26=62개다. hashValue의 길이는 62의n승&gt;3650억인 n의 최솟값을 찾아야 한다.</p>
<p>아래 표는 hashValue의 길이와 해시 함수가 만들 수 있는 URL의 개수 사이의 관계다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/7542b20c-0e23-4c47-8ed5-0744de0cd410/image.png" alt=""></p>
<p>n=7이면 3.5조개의 URL을 만들 수 있다. 요구사항을 만족시키기 충분한 값이다. 따라서 hashValue는 7개로 하겠다.</p>
<p>해시 함수 구현에 쓰일 기술로는 두 가지 방법을 살펴보자.</p>
<ol>
<li>해시 후 충돌 해소 방법</li>
<li>base-62 변환 법</li>
</ol>
<h3 id="해시-후-충돌-해소-방법">해시 후 충돌 해소 방법</h3>
<p>긴 URL을 줄이면 원래 URL을 7글자 문자열로 줄이는 해시 함수가 필요하다. 손쉬운 방법은 CRC32, MD5, SHA-1같이 잘 알려진 해시 함수를 이용하는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/6e9c4818-a6f7-4180-89da-c5bf66200727/image.png" alt=""></p>
<p>근데 표를 보면 가장 짧은 길이를 가진 CRC32 해시값조차도 길이가 7을 넘는다. 어떻게 줄일 수 있을까?</p>
<p>이 문제를 해결하려면 계산된 해시 값에서 처음 7개 글자만 이용하는 것이다. <strong>하지만</strong>이렇게 하면 해시 결과가 서로 충돌할 확률이 높아진다. 충돌이 실제로 발생하면, 미리 정한 문자열을 해시값에 덧붙인다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/fc2547b4-1972-44ae-ab5d-b622c02ce77b/image.png" alt=""></p>
<p>이 방법을 쓰면 충돌은 해소할 수 있지만 단축 URL을 생성할 때 한 번 이상 DB질의가 필요하므로 오버헤드가 크다. DB대신 <strong>블룸 필터</strong>를 사용하면 성능을 높일 수 있다. <strong>블룸 필터</strong>는 어떤 집합에 특정 원소가 있는지 검사할 수 있도록 하는, 확률론에 기초한 공간 효율이 좋은 기술이다.</p>
<h3 id="base-62-변환">base-62 변환</h3>
<p>진법 변환은 URL 단축기를 구현할 때 흔히 사용되는 접근법 중 하나다. 이 기법은 <strong>수의 표현 방식이 다른 두 시스템이 같은 수를 공유</strong>하여야 하는 경우에 유용하다. 62진법을 쓰는 이유는 hashValue에 사용할 수 있는 문자 개수가 62개이기 때문이다. 그럼 지금부터 base-62 변환이 어떻게 이루어지는지 살펴보자. ex)11157 → 62진수</p>
<ul>
<li>62진법은 수를 표현하기 위해 총 62개 문자를 사용하는 진법이다. 따라서 0은 0으로, 9는 9로, 10은 a로, 11은 b로, … 35는 z로, 36은 A로, … 61은 Z로 대응시켜 표현하도록 할것이다. 따라서 62진법에서 ‘a’는 1010을 나타내고, ‘Z’는 6110을 나타낸다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/65815266-1c7e-4442-ab37-05651a515752/image.png" alt=""></p>
<h3 id="두-접근법-비교">두 접근법 비교</h3>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/3b13f3ac-68b6-4517-9cd6-5fea1ff9eaa2/image.png" alt=""></p>
<h3 id="url-단축기-상세-설계">URL 단축기 상세 설계</h3>
<p>URL 단축기는 시스템의 핵심 컴포넌트이므로, 그 처리 흐름이 논리적으로는 단순해야 하고 기능적으로는 언제나 동작하는 상태로 유지되어야 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/b3cca6b3-6239-41ba-9bb4-1d0bb2f7a2ad/image.png" alt=""></p>
<ol>
<li>입력으로 긴 URL을 받는다.</li>
<li>데이터베이스에 해당 URL이 있는지 검사한다.</li>
<li>데이터베이스에 있다면 해당 URL에 대한 단축 URL을 만든 적이 있는 것이다. 따라서 데이터베이스에서 해당 단축 URL을 가져와서 클라이언트에게 반환한다.</li>
<li>데이터베이스에 ㅇ벗는 경우에는 해당 URL은 새로 접수된 것이므로 유일한 ID를 생성한다. 이 ID는 데이터베이스의 기본 키로 사용된다.</li>
<li>62진법 변환을 적용, ID를 단축 URL로 만든다.</li>
<li>ID, 단축 URL, 원래 URL로 새 데이터베이스 레코드를 만든 후 단축 URL을 클라이언트에 전달한다.</li>
</ol>
<h3 id="url-리디렉션-상세-설계">URL 리디렉션 상세 설계</h3>
<p>아래 그림은 리디렉션 메커니즘의 상세한 설계를 그리고 있다. 쓰기보다 읽기를 더 자주 하는 시스템이라, &lt;단축 URL, 원래 URL&gt;의 쌍을 캐시에 저장하여 성능을 높였다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/8e01b7eb-ea64-4ef9-aef6-a5a8637f24cc/image.png" alt=""></p>
<p>로드밸런서의 동작 흐름은 다음과 같이 요약할 수 있다.</p>
<ol>
<li>사용자가 단축 URL을 클릭</li>
<li>로드밸런서가 해당 클릭으로 발생한 요청을 웹 서버에 전달</li>
<li>단축 URL이 이미 캐시에 있는 경우에는 URL을 바로 꺼내서 클라이언트에게 전달</li>
<li>캐시에 해당 단축 URL이 없는 경우에는 DB에서 꺼냄, DB에 없다면 사용자가 잘못된 URL을 입력한 경우 일것이다.</li>
<li>DB에 꺼낸 URL을 캐시에 넣은 후 사용자에게 반환</li>
</ol>
<h1 id="4단계-마무리">4단계 마무리</h1>
<p>URL 단축기의 API, 데이터 모델, 해시 함수, URL 단축 및 리디렉션 절차를 설계해 보았다.</p>
<p>그리고, 아래와 같은 것들도 고려해볼 수 있다.</p>
<ul>
<li>처리율 제한 장치</li>
<li>웹 서버의 규모 확장</li>
<li>데이터베이스의 규모 확장</li>
<li>데이터 분석 솔루션</li>
<li>가용성, 데이터 일관성, 안정성</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[분산 시스템을 위한 유일 ID 생성기 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EB%B6%84%EC%82%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9C%A0%EC%9D%BC-ID-%EC%83%9D%EC%84%B1%EA%B8%B0-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EB%B6%84%EC%82%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%9C%A0%EC%9D%BC-ID-%EC%83%9D%EC%84%B1%EA%B8%B0-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 07 Aug 2023 03:12:11 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>분산 시스템에서 사용될 <strong>유일 ID</strong> 생성기를 설계해 보자.</p>
<p>분산 시스템에서 데이터베이스의 <strong>auto_increment</strong> 속성을 사용하면 되지 않을까? 라고도 생각할 수 도 있다.</p>
<blockquote>
<p>왜냐하면, 데이터베이스 서버 한 대로는 그 요구를 감당할 수 없을뿐더러, 여러 데이터베이스 서버를 쓰는 경우에는 <strong>지연 시간(delay)</strong>를 낮추기가 무척 힘들기 때문이다.</p>
</blockquote>
<p>유일성이 보장되는 ID의 예시 그림</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c487180e-b168-4115-bca1-da4ec97729fa/image.png" alt=""></p>
<h1 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h1>
<p>ID가 가져야 하는 특성</p>
<ul>
<li>유일해야한다</li>
<li>발급 날짜에 따라 정렬 가능해야 한다</li>
<li>ID는 숫자로만 구성되어야 한다.</li>
<li>초당 10,000 ID를 생성할 수 있어야 한다.</li>
</ul>
<h1 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h1>
<p>분산 시스템에서 유일성이 보장되는 ID를 만드는 방법은 더욱 다양하지만 이 중 4개를 살펴보자.</p>
<ul>
<li>다중 마스터 복제</li>
<li>UUID</li>
<li>티켓 서버</li>
<li>트위터 스노플레이크 접근법</li>
</ul>
<h2 id="다중-마스터-복제">다중 마스터 복제</h2>
<p>다중 마스터 복제는 대략 아래와 같은 그림과 같은 구성을 갖는다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c410fd02-a06b-4ec8-8b85-1dab6c3cdcaf/image.png" alt=""></p>
<p>이 접근법은 데이터베이스의 auto_increment 기능을 활용하는 것이다. 다만 다음 ID의 값을 구할 때 1만큼 증가시키는 것이 아니라 다중 마스터 서버 갯수 k 만큼 증가 시키면 유일한 ID를 가질 수 있게 할 수 있다.</p>
<p>하지만 이 방법은 <strong>단점이 있다.</strong></p>
<ul>
<li>여러 데이터 센터에 걸쳐 규모를 늘리기 어렵다</li>
<li>ID의 유일성은 보장되겠지만 그 값이 시간 흐름에 맞추어 커지도록 보장할 순 없다</li>
<li>서버를 추가하거나 삭제할 때도 잘 동작하도록 만들기 어렵다.</li>
</ul>
<h2 id="uuid">UUID</h2>
<p>UUID는 컴퓨터 시스템에 저장되는 정보를 유일하게 식별하기 위한 128비트짜리 수다.</p>
<blockquote>
<p>중복 UUID 값이 생길 확률을 50%로 끌어 올리려면 초당 10 억개의 UUID를 100년 동안 계속해서 만들어야 한다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/ea1ba61f-3294-4414-9b74-d5108ff337c5/image.png" alt=""></p>
<ul>
<li>장점<ul>
<li>UUID를 만드는 방법은 단순한다. 서버 사이의 조율이 필요 없으므로 동기화 이슈도 없음.</li>
<li>각 서버가 자기가 쓸 ID를 알아서 만드는 구조이므로 규모 확장도 쉽다.</li>
</ul>
</li>
<li>단점<ul>
<li>ID가 128비트로 길다. 위 사례에서 요구사항은 <strong>64비트였다</strong></li>
<li>ID를 시간순으로 정렬할 수 없음.</li>
<li>ID에 숫자가 아닌 값이 포함될 수 있다.</li>
</ul>
</li>
</ul>
<h2 id="티켓-서버">티켓 서버</h2>
<p>플리커라는 웹 사이트는 분산 키를 만들어 내기 위해 이 기술을 이용했다고 한다.</p>
<p>동작 방식을 그림으로 보면</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/fb7acb33-8ed2-484a-ba84-7df1be2f554e/image.png" alt=""></p>
<p>이 아이디어의 <strong>핵심</strong>은 auto_increment 기능을 갖춘 데이터베이스 서버, 즉 티켓 서버를 <strong>중앙 서버</strong>로 하나만 사용하는 것이다.</p>
<ul>
<li>장점<ul>
<li>유일성이 보장되는 오직 숫자로만 구성된 ID를 쉽게 만들 수 있음</li>
<li>구현하기 쉽고, <strong>중소 규모 애플리케이션</strong>에 적합하다.</li>
</ul>
</li>
<li>단점<ul>
<li>티켓 서버가 SPOF가 된다. 이 서버에 장애가 발생하면 모든 시스템이 멈추게 된다. 이 이슈를 피하고 싶으면 티켓 서버를 여러대 두면 되는데, 여러대 두면 데이터 동기화와 같은 <strong>새로운 문제가</strong> 발생한다.</li>
</ul>
</li>
</ul>
<h2 id="트위터-스노플레키의-접근법">트위터 스노플레키의 접근법</h2>
<p>지금까지 여러 가지 ID 생성기 구현 방법을 살펴보았다. 하지만 그 가운데 이번 장에서 풀어야 하는 문제의 요구사항을 만족하는 접근법은 없었다.</p>
<p>트위터는 스노플레이크라고 부르는 독창적인 ID 생성 기법을 사용한다. 이 기법은 위의 요구사항을 만족시킬 수 있다.</p>
<p>이 기법을 보기전, 각개 격파 전략을 먼저 이용해보자. 생성해야 하는 ID의 구조를 여러 절로 분할하는 것이다. 아래 그림을 봐보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/42122fbb-377b-4b5e-82b2-fb8876b9f9cc/image.png" alt=""></p>
<p>그림의 각 요소를 살펴보면 다음과 같다.</p>
<ul>
<li>사인 비트: 1비트를 할당한다. 지금으로서는 쓰임새가 없지만 나중을 위해 유보해 둔다. 음수와 양수를 구변하는데 사용할 수 있을 것이다.</li>
<li>타임스탬프: 41비트를 할당한다. 기원 시각 이후로 몇 밀리초가 경과했는지를 나타내는 값이다. 본 설계안의 경우에는 기원 시각으로 트위터 스노플레이크 구현에서 사용하는 값을 이용할 것이다.</li>
<li>데이터센터 ID: 5비트를 할당한다. 따라서 2의5승 즉 32개 데이터센터를 지원할 수 있다.</li>
<li>서버 ID: 5비트를 할당한다. 따라서 데이터센터당 32개 서버를 사용할 수 있다.</li>
<li>일련번호: 12비트를 할당한다. 각 서버에서는 ID를 생성할 때마다 이 일련번호를 1만큼 증가시킨다. 이 값은 1밀리초가 경과할 때마다 0으로 초기화 된다.</li>
</ul>
<h1 id="3단계-상세-설계">3단계 상세 설계</h1>
<p>개략적 설계를 진행하면서 우리는 분산 시스템에서 사용할 유일성 보장 ID 생성기의 다양한 방법들을 알아보았다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/4291cf9a-432d-4e28-8230-ac2c5acccf4b/image.png" alt=""></p>
<p>ID 구조 다이어그램을 다시 한번 봐보면, 데이터 센터 ID랑 서버 ID는 시스템이 시작할 때 결정되며, 일반적으로 시스템 운영 중에는 <strong>바뀌지 않는다.</strong> 데이터센터 ID나 서버 ID를 잘못 변경하게 되면 ID 충돌이 일어날 수 있으므로, 그런 작업을 해야 할 때는 <strong>신중해야 한다.</strong></p>
<h2 id="타임스탬프">타임스탬프</h2>
<p>41비트이며, 시간의 흐름에 따라 점점 큰 값을 갖게 되므로, 시간 순으로 정렬 가능하게 될것이다. 이는 UTC로 만들어 지기 때문에 역으로 어떠한 UTC라도 표현할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/2cafa3b8-6993-491f-add1-4dd62bb24e01/image.png" alt=""></p>
<p>41비트로 표현할 수 있는 타임스탬프의 최댓값은 대략 69년에 해당하는데, 따라서 이 ID 생성기는 69년동안만 정상 동작한다. 기원 시각을 현재에 가깝게 맞춰서 오버플로가 발생하는 시점을 늦춰 놓은 것이다. 69년이 지나면 기원 시각을 바꾸거나 ID 체계를 다른 것으로 이전(migration)해주어야 한다.</p>
<h2 id="일련번호">일련번호</h2>
<p>일련번호는 12비트이므로 4096개의 값을 가질 수 있다. 어떤 서버가 같은 밀리초 동안 하나 이상의 ID를 만들어 낸 경우에만 0보다 큰 값을 갖게 된다.</p>
<h1 id="4단계-마무리">4단계 마무리</h1>
<p>이번 장에서 설계를 진행했던 방법은 4가지 중 유일하게 요구사항을 모두 만족 시켜주면서 추가적인 확장이 가능했던 스노플레이크이다.</p>
<p>추가적으로 보아야할 것들을 봐보자.</p>
<ul>
<li>시계 동기화: 이번 설계를 진행하면서 우리는 ID 생성 서버들이 전부 같은 시계를 사용한다고 가정하였다. 하지만 이런 가정은 <strong>하나의 서버가 여러 코어에서 실행될 경우</strong> 유효하지 않을 수 있다. 여러 서버가 물리적으로 독립된 여러 장비에서 실행되는 경우에도 마찬가지다. 시계 동기화의 실제 방법은 이번 장에서 다룰 내용이 아니지만, 그런 문제가 있다는 점을 알아두는 것은 중요하다. <strong>NTP</strong>는 이 문제를 해결하는 가장 보편적인 수단이다.</li>
<li>각 절(section)의 길이 최적화: 예를 들어 동시성이 낮고 수명이 긴 애플리케이션이라면 일련번호 절의 길이를 줄이고 타임스탬프 절의 길이를 늘리는 것이 효과적일 수도 있다.</li>
<li>고가용성: ID 생성기는 필수 불가결 <strong>컴포넌트</strong>이므로 아주 높은 가용성을 제공해야 할 것이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[키-값 저장소 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%ED%82%A4-%EA%B0%92-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/%ED%82%A4-%EA%B0%92-%EC%A0%80%EC%9E%A5%EC%86%8C-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Mon, 07 Aug 2023 03:07:34 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>키-값 저장소는 키-값 데이터베이스라고도 불리는 비 관계형 데이 저장소에 저장되는 값은 고유 식별자를 키로 가져야 한다. 키와 값 사이의 이런 연결 관계를 “키-값”쌍 이라고 자칭한다.</p>
<p>키-값 쌍에서의 키는 유일해야 하며 해당 키에 매달린 값은 키를 통해서만 접근할 수 있다. 키는 일반 텍스트일 수도 있고 해시 값일 수도 있다. 성능상의 이유로, 키는 짧을수록 좋다.</p>
<ul>
<li>일반 텍스트 키: &quot;last_logged_in_at&quot;</li>
<li>해시 키: 253DDEC4</li>
</ul>
<p>키-값 쌍에서의 값은 문자열일 수도 있고 리스트일 수도 있고 객체일 수도 있다. 키-값 저장소는 보통 값으로 무엇이 오든 상관하지 않는다. 키-값 저장소로 널리 알려진 것은 아마존 다이나모, memcached, 레디스 같은 것들이 있다.</p>
<p>다음은 키 값 저장소에 보관된 데이터의 사례다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/b3a39b05-c123-4cb3-bee4-246f86d620a4/image.png" alt=""></p>
<p>이번 장에서 여러분은 다음 연산을 지원하는 키-값 저장소를 설계해 볼 것이다.</p>
<ul>
<li>put(key, value): 키-값 쌍을 저장소에 저장한다.</li>
<li>get(key): 인자로 주어진 키에 매달린 값을 꺼낸다.</li>
</ul>
<h2 id="문제-이해-및-설계-범위-확정">문제 이해 및 설계 범위 확정</h2>
<p>완벽한 설계란 없다. 읽기, 쓰기 그리고 메모리 사용량 사이에 어떤 균형을 찾고, 데이터의 일관성과 가용성 사이에서 타협적 결정을 내린 설계를 만들었다면 쓸만한 답안일 것이다. 이번 장에서는 다음과 같은 특성을 갖는 키-값 저장소를 설계해 볼 것이다.</p>
<ul>
<li>키-값 쌍의 크기는 10KB이하이다.</li>
<li>큰 데이터를 저장할 수 있어야 한다.</li>
<li>높은 가용성을 제공해야 한다. 따라서 시스템은 설사 장애가 있더라도 빨리 응답해야한다.</li>
<li>높은 규모 확장성을 제공해야 한다. 따라서 트래픽 양에 따라 자동적으로 서버 증설/삭제가 이루어져야 한다.</li>
<li>데이터 일관성 수준은 조정이 가능해야 한다.</li>
<li>응답 지연시간이 짧아야 한다.</li>
</ul>
<h2 id="단일-서버-키-값-저장소">단일 서버 키-값 저장소</h2>
<p>한 대 서버만 사용하는 키-값 저장소를 설계하는 것은 쉽다. 가장 직관적인 방법은 키-값 쌍 전보를 메모리에 해시 테이블로 저장하는 것이다. 그러나 이 접근법은 빠른 속도를 보장하긴 하지만 모든 데이터를 메모리 안에 두는 것이 불가능할 수도 있다는 <strong>약점을 갖고있다. 이 문제를 해결하기 위한 개선책으로는 다음과 같은 방법이 있다.</strong></p>
<ul>
<li>데이터 압축</li>
<li>자주 쓰이는 데이터만 메모리에 두고 나머지는 디스크에 저장</li>
</ul>
<p>그러나 이렇게 개선한다고 해도, 한 대 서버로 부족할 때가 곧 찾아온다. 많은 데이터를 저장하려면 분산 키-값 저장소를 만들 필요가 있다.</p>
<h2 id="분산-키-값-저장소">분산 키-값 저장소</h2>
<p>분산 키-값 저장소는 분산 해시 테이블이라고도 부른다. 키-값 쌍을 여러 서버에 분산시키는 탓이다. 분산 시스템을 설계할 때는 CAP정리를 이해하고 있어야 한다.</p>
<h3 id="cap-정리">CAP 정리</h3>
<p>CAP 정리는 데이터 <strong>일관성, 가용성, 파티션 감내</strong>라는 세 가지 요구사항을 동시에 만족하는 분산 시스템을 설계하는 것은 불가능하다는 정리다. 우선 각 요구사항의 의미부터 명확히 정리하고 넘어가자.</p>
<ul>
<li>데이터 일관성: 분산 시스템에 접속하는 모든 클라이언트는 어떤 노드에서 접속했느냐에 관계없이 언제나 같은 데이터를 보게 되어야 한다.</li>
<li>가용성: 분산 시스템에 접속하는 클라이언트는 일부 노드에 장애가 발생하더라도 항상 응답을 받을 수 있어야 한다.</li>
<li>파티션 감내: 파티션은 두 노드 사이에 통신 장애가 발생하였음을 의미한다. 파티션 감내는 네트워크에 파티션이 생기더라도 시스템은 계속 동작하여야 한다는 것을 뜻한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/4b0709de-f3bb-41ac-95d1-9b1a97cd84a8/image.png" alt=""></p>
<p>키-값 저장소는 앞서 제시한 세 가지 요구사항 가운데 어느 두 가지를 만족하느냐에 따라 다음과 같이 분류할 수 있다.</p>
<ul>
<li>CP 시스템: 일관성과 파티션 감내를 지원하는 키-값 저장소, 가용성을 희생한다.</li>
<li>AP 시스템: 가용성과 파티션 감내를 지원하는 키-값 저장소. 데이터 일관성을 희생한다.</li>
<li>CA 시스템: 일관성과 가용성을 지원하는 키-값 저장소. 파티션 감내는 지원하지 않는다. 그러나 통상 네트워크 장애는 피할 수 없는 일로 여겨지므로, 분산 시스템은 반드시 파티션 문제를 감내할 수 있도록 설계되어야 한다. <strong>그러므로 실세계에 CA 시스템은 존재하지 않는다.</strong></li>
</ul>
<p>분산 시스템에서 데이터는 보통 여러 노드에 복제되어 보관된다. 세 대의 복제 노드 n1,n2,n3에 데이터를 복제하여 보관하는 상황을 아래그림과 같이 가정해보자.(p94)</p>
<h3 id="이상적-상태">이상적 상태</h3>
<p>이상적 환경이라면 네트워크가 파티션되는 상황은 절대로 일어나지 않을 것이다.</p>
<p>n1에 기록된 데이터는 자동적으로 n2와 n3에 복제된다. 데이터 일관성과 가용성도 만족된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/0ad1c890-87e5-451d-a016-5df48cfe0a96/image.png" alt=""></p>
<h3 id="실세계의-분산-시스템">실세계의 분산 시스템</h3>
<p>분산 시스템은 파티션 문제를 피할 수 없다. 그리고 파티션 문제가 발생하면 우리는 일관성과 가용성 사이에서 하나를 선택해야 한다. 아래 그림은 n3에 장애가 발생하여 n1 및 n2와 통신할 수 없는 상황의 그림이다. 클라이언트가 n1 또는 n2에 기록한 데이터는 n3에 전달되지 않는다. n3에 기록되었으나 아직 n1 및 n2로 전달되지 않은 데이터가 있다면 n1과 n2는 오래된 사본을 갖고 있을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/2b7295f9-54a8-4f4a-ae0b-b3d4d4d8dbb7/image.png" alt=""></p>
<p>가용성 대신 일관성을 선택한다면(CP 시스템) 세 서버 사이에 생길 수 있는 데이터 불일치 문제를 피하기 위해 n1과 n2에 대해 쓰기 연산을 중단시켜야 하는데, 그렇게 하면 가용성이 깨진다. 은행권 시스템은 보통 데이터 일관성을 양보하지 않는다.</p>
<p>하지만 일관성 대신 가용성을 선택한 시스템(AP 시스템)은 설사 낡은 데이터를 반환할 위험이 있더라도 계속 읽기 연산을 허용해야 한다. 아울러 n1과 n2는 계속 쓰기 연산을 허용할 것이고, 파티션 문제가 해결된 뒤에 새 데이터를 n3에 전송할 것이다.</p>
<h3 id="시스템-컴포넌트">시스템 컴포넌트</h3>
<p>키-값 저장소 구현에 사용될 핵심 컴포넌트들 및 기술들만 살펴보자.</p>
<ul>
<li>데이터 파티션</li>
<li>데이터 다중화</li>
<li>일관성</li>
<li>일관성불일치 해소</li>
<li>장애 처리</li>
<li>시스템 아키텍처 다이어그램</li>
<li>쓰기 경로</li>
<li>읽기 경로</li>
</ul>
<p>이번 절에 다루는 내용은 널리 사용되고 있는 세 가지 키-값 저장소, 즉 다이나모, 카산드라, 빅테이블의 사례를 참고한 것이다.</p>
<h3 id="데이터-파티션">데이터 파티션</h3>
<p>대규모 애플리케이션의 경우 전체 데이터를 한 대 서버에 욱여넣는 것은 불가능하다. 가장 단순한 해결책은 데이터를 작은 파티션들로 분할한 다음 여러 대 서버에 저장하는 것이다. 데이터를 파티션 단위로 나눌 때는 다음 두 가지 문제를 중요하게 따져봐야 한다.</p>
<ul>
<li>데이터를 여러 서버에 고르게 분산할 수 있는가</li>
<li>노드가 추가되거나 삭제될 때 데이터의 이동을 최소화할 수 있는가</li>
</ul>
<p>5장에서 다룬 안정 해시는 이런 문제를 푸는 데 적합한 기술이다. 안정 해시의 동작 원리를 간략히 다시보자.</p>
<ul>
<li>우선 서버를 해시 링에 배치한다. 아래그림에선 s0~s7의 여덟 개가 있다.</li>
<li>어떤 키-값 쌍을 어떤 서버에 저장할지 결정하려면 우선 해당 키를 같은 링위에 배치한다. 그 지점으로부터 링을 시계 방향으로 순회하다 만나는 첫번째 서버가 바로 해당 키-값 쌍을 저장할 서버다. 따라서 key0은 s1에 저장된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/97204918-c71b-462d-9e4e-4646c555873f/image.png" alt=""></p>
<p>안정 해시를 사용하여 데이터를 파티션하면 좋은 점은 다음과 같다.</p>
<ul>
<li>규모 확장 자동화: 시스템 부하에 따라 서버가 자동으로 추가되거나 삭제되도록 만들 수 있다.</li>
<li>다양성: 각 서버의 용량에 맞게 <strong>가상 노드</strong>의 수를 조정할 수 있다. 다시 말해, 고성능 서버는 더 많은 가상 노드를 갖도록 설정할 수 있다.</li>
</ul>
<h3 id="데이터-다중화">데이터 다중화</h3>
<p>높은 가용성과 안정성을 확보하기 위해서는 데이터를 N개 서버에 비동기적으로 다중화할 필요가 있다. 여기서 N은 튜닝 가능한 값이다. N개 서버를 선정하는 방법은 이러하다.</p>
<p>어떤 키를 해시 링 위에 배치한 후, 그 지점으로부터 시계 방향으로 순회하면서 만나는 첫 N개 서버에 데이터 사본을 보관하는 것이다. 따라서 N=3으로 설정한 아래 그림에선 key0은 s1~3에 저장된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/496a9430-4428-45af-b641-2561909fd651/image.png" alt=""></p>
<p>그런데 가상 노드를 사용한다면 위와 같이 선택한 N개의 노드가 대응될 실제 물리 서버의 개수가 N보다 작아질 수 있다. 이 문제를 피하려면 선택할 때 같은 물리 서버를 중복 선택되지 않도록 해야 한다.</p>
<p>같은 데이터 센터에 속한 노드는 <strong>정전, 네트워크 이슈, 자연재해</strong>등의 문제를 동시에 겪을 가능성이 있다. 따라서 안정성을 담보하기 위해 데이터의 사본은 다른 센터의 서버에 보관하고, 센터들은 고속 네트워크로 연결한다.</p>
<h3 id="데이터-일관성">데이터 일관성</h3>
<p>여러 노드에 다중화된 데이터는 적절히 <strong>동기화</strong> 되어야 한다. 정족수 합의 프로토콜을 사용하면 읽기/쓰기 연산 모두에 일관성을 보장할 수 있다.</p>
<ul>
<li>N=사본 개수.</li>
<li>W=쓰기 연산에 대한 정족수. 쓰기 연산이 성공한 것으로 간주되려면 적어도 W개의 서버로부터 쓰기 연산이 성공했다는 응답을 받아야 한다.</li>
<li>R=읽기 연산에 대한 정족수. 읽기 연산이 성공한 것으로 간주되려면 적어도 R개의 서버로부터 응답을 받아야 한다.</li>
</ul>
<p>N=3일경우</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/972d2a3f-25bb-4281-9a74-edf190e85280/image.png" alt=""></p>
<p>W=1은 데이터가 한 대 서버에만 기록된다는 뜻이 아니다. 위 그림 같이 데이터가 s0~s2에 다중화되는 상황을 예로 살펴보자. W=1의 의미는, 쓰기 연산이 성공했다고 판단하기 위해 중재자는 최소 한 대 서버로부터 쓰기 성공 응답을 받아야 한다는 뜻이다. 따라서 s1으로부터 성공 응답을 받았다면 s0,s2로부터의 응답은 기다릴 필요가 없다. 중재자는 클라이언트와 노드 사이에서 <strong>프락시(proxy)</strong>역할을 한다.</p>
<p>W,R,N의 값을 정하는 것은 응답 지연과 데이터 일관성 사이의 타협점을 찾는 전형적인 과정이다. W=1 또는 R=1인 구성의 경우 중재자는 한 대 서버로부터의 응답만 받으면 되니 응답속도는 빠를 것이다. W나 R의 값이 1보다 큰 경우에는 시스템이 보여주는 데이터 일관성의 수준은 향상될 테지만 중재자의 응답 속도는 가장 느린 서버로부터의 응답을 기다려야 하므로 느려질 것이다.</p>
<p>W+R &gt; N인 경우에는 <strong>강한 일관성</strong>이 보장된다.</p>
<p>일관성을 보증할 최신 데이터를 가진 노드가 최소 하나는 <strong>겹칠 것</strong>이기 때문이다.</p>
<p>그렇다면 면접 시에는 N, W, R 값을 어떻게 정해야 할까? 다음에 가능한 몇 가지 구성을 제시하였다.</p>
<ul>
<li>R=1, W=N: 빠른 읽기 연산에 최적화된 시스템</li>
<li>W=1, R=N: 빠른 쓰기 연산에 최적화된 시스템</li>
<li>W + R &gt; N: <strong>강한 일관성이 보장됨</strong> (보통 N=3, W=R=2)</li>
<li>W + R ≤ N: 강한 일관성이 보장되지 않음</li>
</ul>
<p>요구되는 일관성 수준에 따라 W, R, N의 값을 조정하면 된다.</p>
<h3 id="일관성-모델">일관성 모델</h3>
<p>일관성 모델의 종류</p>
<ul>
<li>강한 일관성: 모든 읽기 연산은 가장 최근에 갱신된 결과를 반환한다. 다시 말해서 클라이언트는 절대로 낡은 데이터를 보지 못한다.</li>
<li>약한 일관성: 읽기 연산은 가장 최근에 갱신된 결과를 반환하지 못할 수 있다.</li>
<li>최종 일관성: 약한 일관성의 한 형태로, 갱신 결과가 결국에는 모든 사본에 반영(즉, 동기화)되는 모델이다.</li>
</ul>
<p>강한 일관성을 달성하는 일반적인 방법은, 모든 쓰기 연산의 결과가 반영될 때 까지 해당 데이터에 대한 읽기/쓰기를 금지하는 것이다. 이 방법은 <strong>고가용성 시스템에 부적합</strong>하다. 새로운 요청의 처리가 중단되기 때문이다.</p>
<p>다이나모 또는 카산드라 같은 저장소는 최종 일관성 모델을 택하고 있는데, 이번 장에서도 그 모델에 맞게 키-값 저장소를 설계해볼 것이다.</p>
<h3 id="비-일관성-해소-기법데이터-버저닝">비 일관성 해소 기법:데이터 버저닝</h3>
<p>데이터를 다중화하면 가용성은 높아지지만 사본 간 일관성은 깨질 가능성은 높아진다. <strong>버저닝</strong>과 <strong>벡터 시계</strong>는 그 문제를 해소하기 위해 등장한 기술이다. 버저닝은 데이터를 변경할 때마다 해당 데이터의 새로운 버전을 만드는 것을 의미한다. 따라서 각 버전의 데이터는 변경 불가능하다.</p>
<p>버저닝에 대해 알아보기 전에 우선 데이터 일관성이 어떻게 깨지는지 알아보자.</p>
<p>아래 그림처럼 데이터의 사본이 노드 n1과 n2에 보관되어 있다고 가정하자. 이 데이터를 가져오려는 서버 1, 서버 2는 get(”name”)연산으로 값을 가져온다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/673deb7d-7b66-403f-b458-d5174f2144c6/image.png" alt=""></p>
<p>위 그림과 같이 서버 1은 “name”에 매달린 값을 “johnSanFrancisoco”로 바꾸고, 서버 2는 “johnNewYork”으로 바꾼다고 하자. 그리고 이 두 연산은 동시에 이뤄진다고 하자. 이제 우리는 충돌(conflict)하는 두 값을 가지게 되었다. 각각의 버전을 v1,v2라고 하자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/6e2a3d4d-c089-4949-baf8-76d17d9f0e1b/image.png" alt=""></p>
<p>이 변경이 이루어진 이후에, 원래 값은 무시할 수 있다. 변경이 끝난 옛날 값이어서다. 하지만 마지막 두 버전 v1과 v2 사이의 충돌은 해소하기 어려워 보인다.</p>
<p>이 문제를 해결하려면 충돌을 발견하고 자동으로 해결해 낼 <strong>버저닝 시스템이 필요하다</strong>. <strong>백터 시계</strong>는 이런 문제를 푸는데 보편적으로 사용되는 기술이다. 지금부터 그 동작 원리를 살펴보자.</p>
<p>백터 시계는 [서버, 버전]의 순서쌍을 데이터에 매단 것이다. <strong>어떤 버전이 선행 버전인지, 후행 버전인지, 아니면 다른 버전과 충돌이 있는지 판벼하는데 쓰인다.</strong></p>
<p>백터 시계는 D([S1, v1], [S2, v2], … [Sn, vn])와 같이 표현하다고 가정하자. 여기서 D는 데이터이고 vi는 버전 카운터, Si는 서버 번호이다. 만일 데이터D를 서버 Si에 기록하면 시스템은 아래 작업 가운데 하나를 수행해야 한다.</p>
<ul>
<li>[Si, vi]가 있으면 vi를 증가시킨다.</li>
<li>그렇지 않으면 새 항목 [Si, 1]를 만든다.</li>
</ul>
<p>위의 설명은 추상적이다. 실제 로직이 어떻게 수행되는지 사례를 통해 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/9e517690-55ec-4215-8e6f-bed14fd2ae58/image.png" alt=""></p>
<ol>
<li>클라이언트가 데이터 D1을 시스템에 기록한다. 이 쓰기 연산을 처리한 서버는 Sx이다. 따라서 벡터 시계는 D1[(Sx, 1)]으로 변한다.</li>
<li>다른 클라이언트가 데이터 D1을 읽고 D2로 업데이트한 다음 기록한다. D2는 D1에 대한 변경이므로 D1을 덮어쓴다. 이떄 쓰기 연산은 같은 서버 Sx가 처리한다고 가정하자. 벡터 시계는 D2[Sx, 2])로 바뀔 것이다.</li>
<li>다른 클라이언트가 D2를 읽어 D3로 갱신한 다음 기록한다. 이 쓰기 연산은 Sy가 처리한다고 가정하자. 벡터 시계 상태는 D3([Sx,2], [Sy,1])로 바뀐다.</li>
<li>또 다른 클라이언트가 D2를 읽고 D4로 갱신한 다음 기록한다. 이때 쓰기 연산은 서버 Sz가 처리한다고 가정하자. 벡터 시계는 D4[Sx,2],[Sz,1])일 것이다.</li>
<li>어떤 클라이언트가 D3와 D4를 읽으면 데이터 간 충돌이 있는 것을 알게된다. D2를 Sy와 Sz가 각기 다른 값으로 바꾸었기 때문이다. 이 충돌은 클라이언트가 해소한 후에 서버에 기록한다. 이 쓰기 연산을 처리한 서버는 Sx였다고 하자. 벡터 시계는 D5([Sx, 3], [Sy, 1], [Sz, 1])로 바뀐다. 충돌이 일어났다는 것을 어떻게 감지하는지는 잠시 후에 더 자세히 살펴보자.</li>
</ol>
<p>벡터 시계를 사용하면 어떤 버전 X가 버전 Y의 이전 버전인지 쉽게 판단할 수 있다. 버전 Y에 포함된 모든 구성요소의 값이 X에 포함된 모든 구성요소 값보다 같거나 큰지만 보면 된다. 예를 들어 벡터 시계 D([s0, 1], [s1, 1])은 D([s0, 1], [s1, 2])의 이전 버전이다. 따라서 두 데이터 사이에 충돌은 없다.</p>
<p>어떤 버전 X와 Y 사이에 충돌이 있는지 보려면 Y의 벡터 시계 구성요소 가운데 X의 벡터 시계 동일 서버 구성요소보다 작은 값을 것이 있는지 보면 된다. 예를 들어, D([s0, 1], [s1, 2])와 D([s0, 2], [s1, 1])는 서로 충돌한다.</p>
<p>그러나 벡터 시계를 사용해 충돌을 감지하고 해소하는 방법에는 두 가지 분명한 단점이 있다. 첫 번째는 충돌 감지 및 해소 로직이 클라이언트에 들어가야 하므로, 클라이언트 구현이 복잡해진다는 것이다.</p>
<p>두 번째는 [서버: 버전]의 순서쌍 개수가 굉장히 빨리 늘어난다는 것이다. 이 문제를 해결하려면 그 길이에 어떤 임계치를 설정하고, 임계치 이상으로 길이가 길어지면 오래된 순서쌍을 벡터 시계에서 제거하도록 해야 한다. 그러나 이렇게 하면 버전 간 선후 관계가 정확하게 결정될 수 없기 때문에 충돌 해소 과정의 효율성이 낮아지게 된다. 하지만 다이나모 데이터베이스에 관계된 문헌에 따르면 아마존은 실제 서비스에서 그런 문제가 벌어지는 것을 발견한 적이 없다고 한다. 그러니 대부분의 기업에서 벡터 시계는 적용해도 괜찮은 솔루션일 것이다.</p>
<h3 id="장애-감지"><strong>장애 감지</strong></h3>
<p>분산 시스템에서는 그저 한 대 서버가 “지금 서버 A가 죽었습니다”라고 한다해서 바로 서버 A를 장애처리 하지는 않는다. 보통 두 대 이상의 서버가 똑같이 서버 A의 장애를 보고해야 해당 서버에 실제로 장애가 발생했다고 간주하게 된다.</p>
<p>아래 그림과 같이 모든 노드 사이에 멀티캐스팅 채널을 구축하는 것이 서버 장애를 감지하는 가장 손쉬운 방법이다. 하지만 이 방법은 서버가 많을 때는 분명 비효율적이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/9610669a-a447-4f45-ab01-50387b7c265c/image.png" alt=""></p>
<p>가십 프로토콜 같은 분산형 장애 감지 솔루션을 채택하는 편이 보다 효율적이다. 가십 프로토콜의 동작 원리는 다음과 같다.</p>
<ul>
<li>각 노드는 멤버십 목록를 유지한다. 멤버십 목록은 각 멤버 ID와 그 박동 카운터 쌍의 목록이다.</li>
<li>각 노드는 주기적으로 자신의 박동 카운터를 증가시킨다.</li>
<li>각 노드는 무작위로 선정된 노드들에게 주기적으로 자기 박동 카운터 목록을 보낸다.</li>
<li>박동 카운터 목록을받은 노드는 멤버십 목록을 최신 값으로 갱신한다.</li>
<li>어떤 멤버의 박동 카운터 값이 지정된 시간 동안 갱신되지 않으면 해당 멤버는 장애 상태인 것으로 간주한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/4ca6a5f7-0682-4b71-9cb6-8c278d213136/image.png" alt=""></p>
<p>위 그림의 예제를 보자.</p>
<ul>
<li>노드 s0은 그림 좌측의 테이블과 같은 멤버십 목록을 가진 상태다.</li>
<li>노드 s0은 노드 s2(멤버 ID = 2)의 박동 카운터가 오랫동안 증가되지 않았다는 것을 발견한다.</li>
<li>노드 s0은 노드 s2를 포함하는 박동 카운터 목록을 무작위로 선택된 다른 노드에게 전달한다.</li>
<li>노드 s2의 박동 카운터가 오랫동안 증가되지 않았음을 발견한 모든 노드는 해당 노드를 장애 노드로 표시한다.</li>
</ul>
<h3 id="일시적-장애-처리"><strong>일시적 장애 처리</strong></h3>
<p>가십 프로토콜로 장애를 감지한 시스템은 가용성을 보장하기 위해 필요한 조치를 해야 한다. 엄격한 정족수 접근법을 쓴다면, 앞서 “데이터 일관성” 절에서 설명한 대로, 읽기와 쓰기 연산을 금지해야 할 것이다.</p>
<p>느슨한 정족수 접근법은 이 조건을 완화하여 가용성을 높인다. 정족수 요구사항을 강제하는 대신, 쓰기 연산을 수행할 W개의 건강한 서버와 읽기 연산을 수행할 R개의 건강한 서버를 해시 링에서 고른다. 이때 장애 상태인 서버는 무시한다.</p>
<p>장애 상태인 서버로 가는 요청은 다른 서버가 잠시 맡아 처리한다. 그동안 발생한 변경사항은 해당 서버가 복구 되었을 때 일관 반영하여 데이터 일관성을 보장한다. 이를 위해 임시로 쓰기 연산을 처리한 서버에는 그에 관한 <strong>단서를 남겨둔다.</strong> 이런 장애 처리 기법을 <strong>임시 위탁 기법</strong>이라고 부른다.</p>
<p>아래 그림을 보면 장애 상태인 노드 s2에 대한 읽기 및 쓰기 연산은 일시적으로 노드 s3가 처리한다. s2가 복구되면, s3는 갱신된 데이터를 s2로 인계 할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/24ba1eec-f03b-4bd2-b662-13e643d75266/image.png" alt=""></p>
<h3 id="영구-장애-처리">영구 장애 처리</h3>
<p><strong>임시 위탁 기법은</strong> 일시적으로 장애를 처리하는 기법이다. 영구적인 노드의 장애 상태는 어떻게 처리해야 할까? 그런 상황을 처리하기엔 <strong>반-엔트로피</strong> 프로토콜을 구현하여 사본들을 <strong>동기화</strong> 한다.</p>
<aside>
💡 반-엔트로피 프로토콜이란?
사본들을 비교하여 최신 버전으로 갱신하는 과정을 포함한다. 사본 간의 일관성이 망가진 상태를 탐지하고 전송 데이터의 양을 줄이기 위해서는 **머클**이라는 트리를 사용해야 한다.

</aside>

<aside>
💡 머클 트리란?
해시 트리라고도 부르는 머클 트리는 각 노드에 그 자식 노드들에 보관된 값의 해시, 또는 자식 노드들에 레이블로부터 계산된 해시 값을 레이블로 붙여두는 트리다. 해시 트리를 사용하면 대규모 자료 구조의 내용을 효과적이면서도 보안상 안전한 방법으로 **검증**할 수 있다.

</aside>

<p>키 공간이 1부터 12까지일 때 머클 트리를 만드는 예제를 한번 살펴보자. 일관성이 망가진 데이터가 위치한 상자는 다른 색으로 표시해두었다.</p>
<ol>
<li>키 공간을 아래그림과 같이 버킷으로 나눈다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/1d858bfe-f398-4758-85ce-7de00f75bb6a/image.png" alt=""></p>
<ol start="2">
<li>버킷에 포함된 각각의 키에 균등 분포 해시 함수를 적용하여 해시 값을 계산한다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/45def365-f566-4fd6-8e11-7c538c8bb381/image.png" alt=""></p>
<ol start="3">
<li>버킷별로 해시값을 계산한 후, 해당 해시 값을 레이블로 갖는 노드를 만든다</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/366f5783-f56c-493d-a441-19a81ae75b38/image.png" alt=""></p>
<ol start="4">
<li>자식 노드의 레이블로부터 새로운 해시 값을 계산하여, 이진 트리를 상향식으로 구성해 나간다</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/816715fb-ecc2-4b40-9ca6-c9e2d6acebab/image.png" alt=""></p>
<p>이 두 머클 트리의 비교는 루트(root) 노드의 해시값을 비교하는 것으로 시작한다. 루트 노드의 해시 값이 일치한다면 두 서버는 같은 데이터를 갖는 것이다. 그 값이 다른 경우에는 왼쪽 노드 → 오른쪽 노드 순으로 해시 값을 비교한다. 이런 방식으로 탐색하여 데이터를 갖는 <strong>버킷을 찾고</strong> 그 버킷들만 <strong>동기화 한다</strong></p>
<p>머클 트리를 사용하면 동기화해야 하는 데이터의 양은 실제로 존재하는 차이의 크기에 비례할 뿐, 두 서버에 보관된 데이터의 총량과는 무관해진다. 하지만 실제로 쓰이는 시스템의 경우 버킷 하나의 크기가 꽤 크다는 것은 알아두어야 한다. 가능한 구성 가운데 하나를 예로 들면 10억 개의 키를 백만개의 버킷으로 관리하는 것인데, 그 경우 하나의 버킷은 1,000개 키를 관리하게 된다.</p>
<h3 id="데이터-센터-장애-처리">데이터 센터 장애 처리</h3>
<p>데이터 센터를 다중화 하는 것이 중요하다.</p>
<h3 id="시스템-아키텍처-다이어그램">시스템 아키텍처 다이어그램</h3>
<p>키-값 저장소를 만드는 데 필요한 다양한 기술적 고려사항들을 살펴보았으나,</p>
<p>이제 아키텍처 다이어그램을 봐보자.</p>
<ul>
<li>클라이언트는 키-값 저장소가 제공하는 두 가지 단순한 API, 즉 get(key) 및 put(key, value)와 통신한다.</li>
<li>중재자는 클라이언트에게 키-값 저장소에 대한 프락시 역할을 하는 노드다.</li>
<li>노드는 안정 해시의 해시 링 위에 분포한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/b5621e47-b286-4ac7-8fe1-71fb1983b9d3/image.png" alt=""></p>
<ul>
<li>노드를 자동으로 추가 또는 삭제할 수 있도록, 시스템은 완전히 분산된다.</li>
<li>데이터는 여러 노드에 다중화된다.</li>
<li>모든 노드가 같은 책임을 지므로, <strong>SPOF(Single Point of Failure)</strong>는 존재하지 않는다.</li>
</ul>
<p>완전히 분산된 설계를 채택하였으므로, 모든 노드는 아래그림에 제시된 기능 전부를 지원해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/f6b4f03d-81a1-48df-a78c-88188c6f957e/image.png" alt=""></p>
<h3 id="쓰기-경로">쓰기 경로</h3>
<p>아래그림은 쓰기 요청이 특정 노드에 전달되면 무슨 일이 벌어지는지를 보여준다. 아래 그림에서 보인 구조는 기본적으로 <strong>카산드라</strong>의 사례를 참고한 것임에 유의하기 바란다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/79e523da-d048-464f-97f1-cdd72106184f/image.png" alt=""></p>
<ol>
<li>쓰기 요청이 커밋 로그 파일에 기록됨</li>
<li>데이터가 메모리 캐시에 기록된다.</li>
<li>메모리 캐시가 가득차거나 임계치에 도달하면 데이터는 디스크에 있는 SSTable에 기록된다.</li>
</ol>
<aside>
💡 SSTable이란?
Sorted-String Table의 약어이다. <키, 값>의 순서쌍을 정렬된 리스트 형태로 관리하는 테이블이다.

</aside>

<h3 id="읽기-경로">읽기 경로</h3>
<p>읽기 요청을 받은 노드는 데이터가 메모리 캐시에 있는지 부터 살핀다. 있는 경우에는 아래 그림과 같이 해당 데이터를 클라이언트에게 반환한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/9d6afd98-27be-4c0f-a741-ad178ee3cfaa/image.png" alt=""></p>
<p>데이터가 메모리에 없는 경우에는 디스크에서 가져와야 한다. 아까 말한 SSTable에서 찾아야 하는데 어떤 테이블에 원하는 Key가 있는지 알아낼 효율적인 방법이 필요할 것이다. 이런 문제를 푸는데는 <strong>블룸 필터</strong>가 흔히 사용된다.</p>
<p>데이터가 메모리에 없을 떄 읽기 연산이 처리되는 경로를 보면 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/526f07ca-c612-4ec9-b102-9c4de9434b16/image.png" alt=""></p>
<ol>
<li>데이터가 메모리에 있는지 확인</li>
<li>데이터가 메모리에 없다면 <strong>블룸 필터</strong>로 검사</li>
<li>블룸 필터를 통해 SSTable중 어떤 테이블에 있는지 찾는다</li>
<li>SSTable에서 데이터를 가져옴</li>
<li>해당 데이터를 클라이언트에게 반환한다.</li>
</ol>
<h2 id="요약">요약</h2>
<p>이번 장의 소개된 많은 개녁뫄 기술을 요약해 보자. 아래 표를 보고 <strong>분산 키-값 저장소가 가져야 하는 기능과 그 기능 구현에 이용되는 기술을 정리</strong>를 봐보자.</p>
<table>
<thead>
<tr>
<th>목표/문제</th>
<th>기술</th>
</tr>
</thead>
<tbody><tr>
<td>대규모 데이터 저장</td>
<td>안정 해시를 사용해 서버들에 부하 분산</td>
</tr>
<tr>
<td>읽기 연산에 대한 높은 가용성 보장</td>
<td>데이터를 여러 데이터센터에 다중화</td>
</tr>
<tr>
<td>쓰기 연산에 대한 높은 가용성 보장</td>
<td>버저닝 및 벡터 시계를 사용한 충돌 해소</td>
</tr>
<tr>
<td>데이터 파티션</td>
<td>안정 해시</td>
</tr>
<tr>
<td>점진적 규모 확장성</td>
<td>안정 해시</td>
</tr>
<tr>
<td>다양성</td>
<td>안정 해시</td>
</tr>
<tr>
<td>조절 가능한 데이터 일관성</td>
<td>정족수 합의</td>
</tr>
<tr>
<td>일시적 장애 처리</td>
<td>느슨한 정족수 프로토콜과 단서 후 임시 위탁</td>
</tr>
<tr>
<td>영구적 장애처리</td>
<td>머클 트리</td>
</tr>
<tr>
<td>데이터 센터 장애 대응</td>
<td>여러 데이터 센터에 걸친 데이터 다중화</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[안정 해시 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EC%95%88%EC%A0%95-%ED%95%B4%EC%8B%9C-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EC%95%88%EC%A0%95-%ED%95%B4%EC%8B%9C-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Sat, 05 Aug 2023 14:20:16 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>수평적 규모 확장성을 달성하기 위해서는 요청 또는 데이터를 서버에 균등하게 나누는 것이 중요하다. 안정 해시는 이 목표를 달성하기 위해 보편적으로 사용하는 기술이다. 하지만 우선 이 해시 기술이 풀려고 하는 문제부터 좀 더 자세히 살펴보도록 하자.</p>
<h2 id="해시-키-재배치refresh문제">해시 키 재배치(refresh)문제</h2>
<p>N개의 캐시 서버가 있다고 하자. 이 서버들에 부하를 균등하게 나누는 보편적인 방법은 아래의 해시 함수를 사용하는 것이다.</p>
<pre><code class="language-bash">serverIndex = hash(key) % N # (N은 서버의 개수이다)</code></pre>
<p>4개의 캐시 서버가 있다고 가정하면 아래 표와 같이 각각의 키에 대해서 해시 값과 서버 인덱스를 계산할 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/5229d96f-4635-4329-b7d2-8a6824f8cad2/image.png" alt=""></p>
<p>특정한 키가 보관된 서버를 알아내기 위해, 나머지 연산을 % 4와 같이 적용하였다. 예를 들어 해시 % 4면 캐시 서버1번에 접속해 데이터를 가져와야한다. </p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/92fb53f9-2811-4650-92b0-dc9a71edf76b/image.png" alt=""></p>
<p>이 방법은 <strong>서버 풀</strong>의 크기가 고정되어 있을 때, 그리고 데이터 분포가 균등할 때는 잘 동작한다. 하지만 서버가 추가 되거나 기존 서버가 삭제되면 문제가 생긴다. 만약 이중 1개 서버가 장애를 일으켜 동작을 중단하게 된다면 키에 대한 해시 값은 변하지 않지만, 나머지(%) 연산을 적용하여 서버 인덱스 값은 달라질 것 이다. 서버 수가 1만큼 줄었기 때문이다. 1개가 줄었다고 생각하고 해시 % 3의 결과를 아래 표로 보자.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/89efbce3-b74b-42f4-8a26-a808dbdc7dfc/image.png" alt=""></p>
<p>위 표에서 보듯이 1번 서버에 보관되어 있는 <strong>키 뿐만이 아닌 대부분의 키가 재분배되었다.</strong> 하나의 서버가 죽으면 대부분 캐시 클라이언트가 데이터가 없는 엉뚱한 서버에 접속하게 된다는 뜻이다. 그 결과로 대규모 <strong>캐시 미스</strong>가 발생하게 될것이다. 안정 해시는 이런 문제를 효과적으로 해결하는 기술이다.</p>
<h2 id="안정-해시">안정 해시</h2>
<p>안정 해시는 테이블 크기가 조정 될 때 평균적으로 오직 k/n개의 키만 재배치하는 해시 기술이다. 여기서 k는 키의 개수이고, n은 슬롯의 개수이다. 이와는 달리 대부분의 전통적 해시 테이블은 슬롯의 수가 바뀌면 거의 대부분의 키를 재배치한다.</p>
<h3 id="해시-공간과-해시-링"><strong>해시 공간과 해시 링</strong></h3>
<p>안정 해시의 동작 원리를 살펴보자. 해시 함수 f로는 SHA-1을 사용한다고 하고, 그 함수의 출력 값 범위는 x0,x1,x2,x3,…xn과 같다고 하자. SHA-1의 해시공간 범위는 0부터 2^160-1 까지다. 이 해시 공간을 그림으로 표현하면 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/311e8643-5a21-45e1-aae6-5bc76dc3e07f/image.png" alt=""></p>
<p>이 해시 공간의 양쪽을 구부려 잡으면 아래 그림과 같은 해시 링이 만들어진다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/066a2f06-4db0-42b3-914d-56a6dbcc84f5/image.png" alt=""></p>
<h2 id="해시-서버">해시 서버</h2>
<p>이 해시 함수 f를 사용하면 서버 IP나 이름을 이 링 위의 어떤 위치에 대응시킬 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/3ca498b5-62f4-440b-bea0-070c9a9a8c1b/image.png" alt=""></p>
<h2 id="해시-키">해시 키</h2>
<p>여기 사용된 해시 함수는 “해시 키 재배치 문제”에 언급한 함수와는 다르며, 나머지 연산 %는 사용하지 않고 있음에 유의하자. 아래 그림과 같이, 캐시할 키 key0, key1, key2, key3 또한 해시 링 위의 어느 지점에나 배치할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/330e041a-7e04-4a1c-84e8-32841e358bc7/image.png" alt=""></p>
<h2 id="서버-조회">서버 조회</h2>
<p>어떤 키가 저장되는 서버는, 해당 키의 위치로부터 시계 방향으로 링을 탐색해 나가면서 만나는 첫 번째 서버다. 따라서 key0은 서버0 key1은 서버1 key2는 서버2 key3은 서버3에 저장된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/7ddc97b3-4e85-4df6-943e-2f3cd3ed8f29/image.png" alt=""></p>
<h2 id="서버-추가">서버 추가</h2>
<p>방금 설명한 내용에 따르면, 서버를 추가하더라도 키 가운데 일부만 재배치하면 된다.</p>
<p>아래 그림을 보면 새로운 서버 4가 추가된 뒤에 key0만 재배치됨을 알 수 있는데, key0은 서버 0에 저장되어 있었는데 서버 4가 추가된 뒤에 key0의 위치에서 시계 방향으로 순회했을 때 <strong>처음으로 만나게 되는 서버가 4</strong>이기 때문에 key0만 재배치하면 될 수 있음을 알 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/8d039747-e74f-4668-afa0-9ccba4ce029b/image.png" alt=""></p>
<h2 id="서버-제거">서버 제거</h2>
<p>하나의 서버가 제거되도 키 가운데 일부만 재배치된다. 아래 그림을 보면 서버1이 삭제되어도 key1만이 서버 2로 재배치됨을 알 수 있다. 나머지 키에는 영향이 없다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/46ba6e32-7d8d-4f99-bc9e-56c96907cd9d/image.png" alt=""></p>
<h2 id="기본-구현법의-두-가지-문제">기본 구현법의 두 가지 문제</h2>
<p>안정 해시 알고리즘은 MIT에서 처음 제안되었다. 그 기본 절차는 다음과 같다.</p>
<ul>
<li>서버와 키를 균등 분포 해시 함수를 사용해 해시 링에 배치한다.</li>
<li>키의 위치에서 링을 시계 방향으로 탐색하다 만나는 최초의 서버가 키가 저장될 서버다.</li>
</ul>
<p>이 두 가지 구현법은 문제가 있다.</p>
<ol>
<li><strong>서버가 추가되거나 삭제되는 상황을 감안 하면 파티션(인접한 서버 사이의 해시공간)의 크기를 균등하게 유지하는 게 불가능하다.</strong> 이는 곧, 어느 서버는 굉장히 큰 해시 공간을 할당 받고, 어떤 서버는 굉장히 작은 해시 공간을 할당 받는 상황이 발생한다는 것이다. 아래 그림은 서버1이 삭제되는 바람에 서버2의 <strong>해시 공간이 기존 공간 대비 거의 두 배로 커지는 상황을 보여준다.</strong></li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/97f75319-710f-425a-bb72-98acc930e8ab/image.png" alt=""></p>
<ol>
<li>키의 균등 분포를 달성하기가 어렵다는 것이다. 예를 들어 서버가 아래 그림과 같이 배치되어 있다고 해 보자. 서버1과 서버3은 아무 데이터도 갖지 않는 반면, 대부분의 키는 서버 2에 보관될 것이다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/489fdb62-06e5-4481-8706-22ed2f395918/image.png" alt=""></p>
<p>이 문제를 해결하기 위해 제안된 방법이 가상노드 또는 복제라 불리는 기법이다.</p>
<h2 id="가상-노드">가상 노드</h2>
<p>가상 노드는 실제 노드 또는 서버를 가리키는 노드로서, 하나의 서버는 링 위에 여러 개의 가상 노드를 가질 수 있다. 아래 그림을 보면 서버0과 서버1은 3개의 가상노드를 가질 수 있다. 서버0을 링에 배치하기 위해 s0하나만 쓰는 대신, s0_0, s0_1, s0_2의 세 개 가상 노드를 사용하였다. 따라서 각 서버는 하나가 아닌 여러 개 파티션을 관리해야 한다. 아래 그림에서 s0으로 표시된 파티션은 서버 0이 관리하는 파티션이고, s1로 표시된 파티션은 서버 1이 관리하는 파티션이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/9e637ac7-9751-44e4-b019-5fd5a62a098c/image.png" alt=""></p>
<p>키의 위치로부터 시계방향으로 링을 탐색하다 만나는 최초의 가상 노드가 해당 키가 저장될 서버가 된다. 아래 그림은 그에 해당하는 예제다. k0가 저장되는 서버는 k0의 위치로부터 링을 시계방향으로 탐색하다 만나는 최초의 가상 노드 s1_1가 나타내는 서버, 즉 서버 1이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c644edce-62fb-4f3a-b041-401020210e8f/image.png" alt=""></p>
<p>가상 노드의 개수를 늘리면 <strong>키의 분포는 점점 더 균등해진다</strong>. <strong>표준 편차가 작아져서</strong> 데이터가 고르게 분포되기 때문이다. 표준 편차는 데이터가 어떻게 퍼져 나갔는지를 보이는 척도다. 가상 노드의 개수를 더 늘리면 표준 편차의 값은 더 떨어진다. 그러나 <strong>가상 노드 데이터를 저장할 공간은 더 많이 필요하게 될 것이다.</strong> <strong>타협적 결정이 필요하다는 뜻이다.</strong> 그러니 시스템 요구사항에 맞도록 가상 노드 개수를 적절히 조정해야 할 것이다.</p>
<h2 id="재배치할-키-결정">재배치할 키 결정</h2>
<p>서버가 추가되거나 제거되면 데이터 일부는 재배치해야 한다. 어느 범위의 키들이 재배치되어야 할까?</p>
<ul>
<li>키가 추가 되는 경우는 <strong>추가된 서버를 기준으로 반시계 방향으로 가장 먼저 만다는 서버까지의 키를 재배치 해야한다.</strong></li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c42f8a02-cdc3-4f9e-b949-b518b7952b62/image.png" alt=""></p>
<ul>
<li>키가 삭제 되는 경우는 <strong>키가 삭제된 서버 기준 반시계 첫번째 서버부터 시계방향 첫번째 서버까지</strong> 키를 재할당 해야한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/ce49f686-940d-40ad-8318-c54ddd1216c0/image.png" alt=""></p>
<h2 id="마치며">마치며</h2>
<p>이번 장에서는 안정 해시가 왜 필요하며 ( 나머지 연산%를 활용한 방법은 모든 키를 재할당 해줘야하며 그동안 캐시 미스가 너무많이 일어나 비효율적이다.) 어떻게 동작하는지를 자세히 살펴 보았다.</p>
<p>안정해시의 장점으로는 다음과 같다.</p>
<ul>
<li>서버가 추가되거나 삭제될 때 재배치되는 키의 수가 최소화된다.</li>
<li>데이터가 보다 균등하게 분포하게 되므로 수평적 규모 확장성을 달성하기 쉽다.</li>
<li>핫스팟(hotspot) 키 문제를 줄인다. 특정한 샤드에 대한 접근이 지나치게 빈번하다면 서버 과부하 문제가 생길 수 있다. 유명한 연예인 데이터가 전부 같은 샤드에 몰리는 상황을 생각해보면 이해하기 쉽다. 안정 해시는 데이터를 좀 더 균등하게 분배하므로 이런 문제가 생길 가능성을 줄인다.</li>
</ul>
<p>안정 해시는 널리 쓰이는 기술이다. 그중 유명한 것 몇 가지를 예로 들면 다음과 같다.</p>
<ul>
<li>아마존 다이나모 데이터베이스의 파티셔닝 관련 컴포넌트</li>
<li>아파치 카산드라 클러스터에서의 데이터 파티셔닝</li>
<li>디스코드 채팅 어플리케이션</li>
<li>아카마이 CDN</li>
<li>매그래프 네트워크 부하 분산기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[처리율 제한 장치의 설계]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EC%9E%A5%EC%B9%98%EC%9D%98-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EC%B2%98%EB%A6%AC%EC%9C%A8-%EC%A0%9C%ED%95%9C-%EC%9E%A5%EC%B9%98%EC%9D%98-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Sat, 05 Aug 2023 14:10:56 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>네트워크 시스템에서 트래픽의 처리율을 제어하기 위한 장치다. HTTP를 예로 들면 이 장치는 특정 기간 내에 전송되는 클라이언트의 요청 횟수를 제한한다. HTTP를 예로 들면 이 장치는 특정 기간 내에 전송되는 클라이언트의 요청 횟수를 제한한다. 요청 횟수가 제한 장치에 정의된 임계치를 넘어서면 추가로 도달한 모든 호출은 처리가 <strong>중단</strong>된다.</p>
<ul>
<li>사용자는 초당 2회 이상 새 글을 올릴 수 없다.</li>
<li>같은 IP 주소로는 하루에 10개 이상 계정을 생성할 수 없다.</li>
<li>같은 디바이스로는 주당5회 이상 리워드(reward)를 요청할 수 없다.</li>
</ul>
<p>API에 처리율 제한 장치를 두면 <strong>좋은 점</strong></p>
<ul>
<li>DoS(Denail of Service) 공격에 의한 자원 고갈을 방지할 수 있다.</li>
<li>비용을 절감한다. 추가 요청에 대한 처리를 하면 서버를 많이 두지 않아도 되고, 우선순위가 높은 API에 더 많은 자원을 할당할 수 있다. 제3자 API에 사용료를 지불하고 있는 회사라면 해당 API를 호출하는 데에 제한을 두면 비용 절감을 할 수 있을 것이다.</li>
<li>서버 과부하를 막는다. 봇에서 오는 트래픽이나 사용자의 잘못된 이용 패턴으로 유발된 트래픽을 걸러내는데 처리율 제한 장치를 활용할 수 있다.</li>
</ul>
<h2 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h2>
<p>처리율 제한 장치를 구현할 때</p>
<p><strong>클라이언트 측 제한 장치냐, 서버 측 제한장치 인가?</strong> </p>
<p><strong>시스템의 규모는 어느정도인가?</strong></p>
<p><strong>시스템이 분산 환경에서 동작하는가?</strong></p>
<p><strong>처리율 제한 장치에 의해 걸러지면 사용자에게 그 사실을 알려야 하나?</strong></p>
<p>위 내용을 정리하면</p>
<ul>
<li>설정된 처리율을 초과하는 요청은 피해야 한다.</li>
<li>낮은 응답시간: 이 제한 장치는 HTTP 응답에 나쁜 영향을 주면 안된다.</li>
<li>가능한 적은 메모리를 써야함.</li>
<li>분산형 처리 제한: 하나의 처리율 제한 장치를 여러 서버나 프로세스에서 공유할 수 있어야 한다.</li>
<li>예외 처리: 요청이 제한되었을 때는 그 사실을 사용자에게 분명하게 보여주어야 한다.</li>
<li>높은 결함 감내성: 제한 장치에 장애가 생기더라도 전체 시스템에 영향을 주면 안됨.</li>
</ul>
<h2 id="2단계-개략적-설계안-제시-및-동의-구하기">2단계 개략적 설계안 제시 및 동의 구하기</h2>
<p>복잡하게 설계하지말고, 기본적인 클리아인트-서버 통신 모델을 사용하자.</p>
<p><strong>처리율 제한 장치를 어디에 둘 것인가?</strong></p>
<p>클라이언트 측에 두면 위/변조가 쉽게 가능해 안정적이지 않다.</p>
<p>서버측에 둔다면 위/변조가 쉽지 않아 안정적이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/4fa6e65d-b9da-40a5-9d7e-93a15b9b9ade/image.png" alt=""></p>
<p>위 사진은 서버측에 처리율 제한 장치를 둔 사진이다.</p>
<p>또는, 미들웨어를 만들어 처리할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/54153675-9dfb-4621-b73e-5a2182932191/image.png" alt=""></p>
<p>위 사진이 미들웨어로 처리율 제한을 처리하는 사진이다.</p>
<p>처리율 제한 장치를 <strong>어디에 둘 것인지가 중요하다</strong></p>
<h2 id="처리율-제한-알고리즘">처리율 제한 알고리즘</h2>
<p>널리 알려진 인기 처리율 처리 알고리즘에는 이렇게 있다.</p>
<ul>
<li>토큰 버킷</li>
<li>누출 버킷</li>
<li>고정 윈도 카운터</li>
<li>이동 윈도 로그</li>
<li>이동 윈도 카운터</li>
</ul>
<h3 id="토큰-버킷-알고리즘">토큰 <strong>버킷 알고리즘</strong></h3>
<p>토큰 버킷은 지정된 용량을 갖는 컨테이너다. 이 버킷에는 사전 설정된 양의 토큰이 주기적으로 채워진다. 토큰 공급기는 이 버킷에 매초 2개의 토큰을 추가한다. 버킷이 가득 차면 추가로 공급된 토큰은 버려진다(overflow).</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/8e609605-8ba1-458d-904d-de23bea99f74/image.png" alt=""></p>
<ul>
<li>각 요청은 처리될 때마다 하나의 토큰을 사용한다. 요청이 도착하면 버킷에 충분한 토큰이 있는지 검사하게 된다.<ul>
<li>충분한 토큰이 있는 경우, 버킷에서 토큰 하나를 꺼낸 후 요청을 시스템에 전달한다.</li>
<li>충분한 토큰이 없는 경우, 해당 요청은 버려진다(dropped).</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/0158b2b7-12f1-40aa-8dad-9a796d4ea747/image.png" alt=""></p>
<p>아래 사진은 토큰 공급기는 어떻게 동작하며, 처리 제한 로직은 어떻게 작동하는지 보여준다.</p>
<p>이 사진에서 토큰 버킷의 크기는 4이다. 토큰 공급률은 분당 4이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/69cbf6d4-ab8c-442d-889b-2519c6e1d3dc/image.png" alt=""></p>
<p>이 토큰 버킷 알고리즘은 2개 인자를 받는다.</p>
<ul>
<li>버킷 크기: 버킷에 담을 수 있는 토큰의 최대 개수</li>
<li>토큰 공급률: 초당 몇 개의 토큰이 버킷에 공급되는가</li>
</ul>
<p>버킷은 몇 개나 사용해야 하나? 공급 제한 규칙에 따라 달라진다. 다음 사례들을 살펴보자.</p>
<ul>
<li>통상적으로, <strong>API 엔드포인트마다</strong> 별도의 버킷을 둔다. 예를 들어, 사용자마다 하루에 한 번만 포스팅을 할 수 있고, 친구는 150명까지 추가할 수 있고, 좋아요 버튼은 다섯 번 까지 누를 수 있다면, 사용자마다 3개의 버킷을 두어야 할 것이다.</li>
<li>IP 주소별로 처리율 제한을 적용해야 한다면 IP 주소마다 버킷을 하나씩 할당해야 한다.</li>
<li>시스템의 처리율을 초당 10,000개 요청으로 제한하고 싶다면, 모든 요청이 하나의 버킷을 공유하도록 해야 할 것이다.</li>
</ul>
<p>장점:</p>
<ul>
<li>구현이 쉽다.</li>
<li>메모리 사용 측면에서도 효율적이다.</li>
<li>짧은 시간에 집중되는 트래픽(burst of traffic)도 처리 가능하다. 버킷에 남은 토큰이 있기만 하면 요청은 시스템에 전달 될 것이다.</li>
</ul>
<p>단점:</p>
<ul>
<li>이 알고리즘은 버킷 크기와 토큰 공급률이라는 두 개 인자를 가지고 있는데, 이 값을 적절하게 튜닝하는 것은 까다로운 일이 될 것이다.</li>
</ul>
<h3 id="누출-버킷-알고리즘"><strong>누출 버킷 알고리즘</strong></h3>
<p>토큰 버킷 알고리즘과 비슷하지만 <strong>요청 처리율이 고정</strong>되어 있다는 점이 다르다.</p>
<p>누출 버킷 알고리즘은 보통 <strong>FIFO</strong> 큐로 구현한다.</p>
<ul>
<li>요청이 도착하면 큐가 가득 차 있는지 본다. 빈자리가 있는 경우엔 큐에 요청을 추가</li>
<li>큐가 가득 차 있는 경우에는 새 요청은 버린다.</li>
<li>지정된 시간마다 큐에서 요청을 꺼내어 처리한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/904f8cec-e826-475a-95d5-9c71b790e088/image.png" alt=""></p>
<p>누출 버킷 알고리즘은 다음과 같은 두 인자를 사용한다.</p>
<ul>
<li>버킷 크기: 큐 사이즈와 같은 값이다. 큐에는 처리될 항목들이 보관된다.</li>
<li>처리율: 지정된 시간당 몇 개의 항목을 처리할지 지정하는 값이다. 보통 초단위로 표현한다.</li>
</ul>
<p><strong>장점</strong></p>
<ul>
<li>큐의 크기가 제한되어 있어 메모리 사용량 측면에서 효율적이다.</li>
<li>고정된 처리율을 갖고 있기 때문에 안정적 출력이 필요한 경우에 적합하다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>단시간에 많은 트래픽이 몰리는 경우 큐에는 오래된 요청들이 쌓이게 되고, 그 요청들을 제때 처리 못하면 최신 요청들은 버려지게 된다.</li>
<li>두 개 인자를 갖고 있는데, 이들을 올바르게 튜닝하기가 까다로울 수 있다.</li>
</ul>
<h3 id="고정-윈도-카운터-알고리즘"><strong>고정 윈도 카운터 알고리즘</strong></h3>
<p>고정 윈도 카운터 알고리즘의 동작 방식</p>
<ul>
<li>타임라인을 고정된 간격의 우니도로 나누고, 각 윈도마다 카운터를 붙인다.</li>
<li>요청이 접수될 때마다 이 카운터의 값은 1씩 증가한다.</li>
<li>이 카운터의 값이 사전에 설정된 임계치에 도달하면 새로운 요청은 새 윈도가 열릴 때까지 버려진다.</li>
</ul>
<p>아래 사진은 동작 원리의 사진이다. 타임라인의 시간 단위는 1초다. 시스템은 초당 3개까지의 요청만을 허용한다. 매초마다 열리는 윈도에 3개 이상의 요청이 밀려오면 초과분은 아래 사진에 보인 대로 버려진다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/2925fbb4-152d-4e6b-aacf-e7bfe5236bd4/image.png" alt=""></p>
<p>이 알고리즘의 가장 큰 문제는 윈도의 경계 부근에 순간적으로 많은 트래픽이 집중될 경우 윈도에 할당된 양보다 더 많은 요청이 처리될 수 있다는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/a0a957ce-a0fe-439b-9930-8af75d8251fd/image.png" alt=""></p>
<p>위 사진은 분당 최대 5개의 요청만 허용하는 시스템인데, 2시1분을보면 10개의 요청을 처리했다.</p>
<p><strong>장점</strong></p>
<ul>
<li>메모리 효율이 좋다.</li>
<li>이해하기 쉽다.</li>
<li>윈도가 닫히는 시점에 카운터를 초기화하는 방식은 특정한 트래픽 패턴을 처리하기에 적합하다.</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>우니도 경계 부근에서 일시적으로 많은 트래픽이 몰려들면, 기대했던 시스템의 처리 한도보다 많은 양의 요청을 처리하게 된다.</li>
</ul>
<h3 id="이동-윈도-로깅-알고리즘"><strong>이동 윈도 로깅 알고리즘</strong></h3>
<p>고정 윈도 알고리즘 + 이동 윈도 알고리즘</p>
<h2 id="개략적인-아키텍처">개략적인 아키텍처</h2>
<p>처리율 제한 알고리즘의 기본 아이디어는 단순하다.</p>
<p>얼마나 많은 요청이 접수되었는지를 추적할 수 있는 카운터를 추적 대상별로 두고 할것인가?</p>
<p>IP 주소별로?</p>
<p>API 엔드포인트나 서비스 단위로?</p>
<p>이 카운터의 값이 어떤 한도를 넘어서면 한도를 넘어 도착한 요청은 거부하는 것이다.</p>
<p>그렇다면 이 카운터는 어디에 보관할 것인가?</p>
<p>데이터베이스는 디스크 접근 때문에 느리니까 사용하면 안될 것이다.</p>
<p>메모리상에서 동작하는 캐시가 바람직한데, 빠른데다 시간에 기반한 <strong>만료 정책을 지원하기 때문</strong>이다.</p>
<p>Redis는 처리율 제한 장치를 구현할 때 자주 사용됨으로써, <strong>INCR, EXPIRE</strong> 두 가지 명령어를 지원한다.</p>
<ul>
<li>INCR: 메모리에 저장된 카운터의 값을 1만큼 증가시킨다.</li>
<li>EXPRIE: 카운터에 타임아웃 값을 설정한다. 설정된 시간이 지나면 카운터는 자동으로 삭제된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/1009fb20-c345-442b-be32-7f1835f909ee/image.png" alt=""></p>
<p>동작 원리는 다음과 같다.</p>
<ul>
<li>클라이언트가 처리율 제한 미들웨어에게 요청을 보낸다.</li>
<li>처리율 제한 미들웨어는 레디스의 지정 버킷에서 카운터를 가져와서 한도에 도달했는지 아닌지를 검사한다.<ul>
<li>한도에 도달했다면 요청은 거부된다.</li>
<li>한도에 도달하지 않았다면 요청은 API 서버로 전달된다. 한편 미들웨어는 카운터의 값을 증가시킨 후 다시 레디스에 저장한다.</li>
</ul>
</li>
</ul>
<h2 id="3단계-상세-설계">3단계 상세 설계</h2>
<p>위의 개략적 설계를 봐서는 다음과 같은 사항은 알 수가 없다.</p>
<ul>
<li>처리율 제한 규칙은 어떻게 만들어지는가?</li>
<li>처리가 제한된 요청들은 어떻게 처리되는가?</li>
</ul>
<p>3단계에서는 위 질문부터 처리하고, 제한된 요청 처리 전략을 살펴보겠다. 그리고 분산 환경에서의 처리율 제한 기법에 대해서도 살펴보고, 구체적인 설계와 성능 최적화 방안, 모니터링 방안까지 살펴보자.</p>
<h3 id="처리율-제한-규칙">처리율 제한 규칙</h3>
<pre><code class="language-bash">domain: messaging
descriptors:
    - key: message_type
    - Value: marketing
    rate_limit:
        unit: day
        requests_per_unit: 5</code></pre>
<p>위는 마케팅 메시지의 최대치를 하루 5개로 재한하는 예</p>
<pre><code class="language-bash">domain: auth
descriptors:
    - key: auth_type
    - Value: login
    rate_limit:
        unit: minute
        requests_per_unit: 5</code></pre>
<p>위 규칙은 클라이언트가 분당 5회 이상 로그인할 수 없도록 제한하는 예다. 보통 이러한 규칙은 설정 파일 형태로 디스크에 저장된다.</p>
<h3 id="처리율-한도-초과-트래픽의-처리">처리율 한도 초과 트래픽의 처리</h3>
<p>어떤 요청이 한도 제한에 걸리면 API는 HTTP 429(too many requests)를 응답한다. 경우에 따라서는 메시지를 나중에 처리하기 위해 <strong>큐</strong>에 보관할 수 도 있다. </p>
<h3 id="처리율-제한-장치가-사용하는-http헤더"><strong>처리율 제한 장치가 사용하는 HTTP헤더</strong></h3>
<p>처리율 제한 장치가 클라이언트에게 보내는 헤더</p>
<ul>
<li>X-Ratelimit-Remaining: 윈도 내에 남은 처리 가능 요청의 수</li>
<li>X-Ratelimit-Limit: 매 윈도마다 클라이언트가 전송할 수 있는 요청의 수</li>
<li>X-Ratelimit-Retry-After: 한도 제한에 걸리지 않으려면 몇 초 뒤에 요청을 다시 보내야 하는지 알림.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c7b08b81-33d2-4334-8284-0c873a0eabea/image.png" alt=""></p>
<ul>
<li>클라이언트가 요청을 서버에 보내면 요청은 먼저 처리율 제한 미들웨어에 도달</li>
<li>처리율 제한 미들웨어는 레디스 등의 저장소에서 마지막 요청의 <strong>타임스탬프</strong>를 가져온다. 가져온 값들에 근거하여 처리율 제한 미들웨어는 결정을 내린다.<ul>
<li>해당 요청이 처리율 제한에 걸리지 않으면 API 서버로 요청을 보냄</li>
<li>제한에 걸렸다면 HTTP 429 에러를 클라이언트에게 보낸다. 그 요청은 그대로 버려질 수 도 있고 혹은 <strong>메세지 큐</strong>에 보관할 수 도 있다.</li>
</ul>
</li>
</ul>
<h3 id="분산-환경에서의-처리율-제한-장치의-구현">분산 환경에서의 처리율 제한 장치의 구현</h3>
<p>분산환경에서의 처리율 제한 장치는</p>
<ul>
<li>경쟁 조건</li>
<li>동기화</li>
</ul>
<p>와 같은 어려운 문제를 풀어야 한다.</p>
<p><strong>경쟁조건</strong></p>
<p>처리율 제한장치는 대략적으로 이런 방식으로 작동한다</p>
<ul>
<li>레디스와 같은 저장소에서 카운터 값을 읽는다.</li>
<li>counter + 1의 값이 임계치를 넘는지 본다.</li>
<li>넘지 않는다면 저장소에 보관된 카운터 값을 1을 증가시킨다.</li>
</ul>
<p>병행성이 심한 환경에서는 아래 그림과 같은 상황이 나온다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/a352f29d-6f26-4352-bd17-e5b505dd91ad/image.png" alt=""></p>
<p>요청 1이 먼저 들어오고 요청 2가 요청1의 동작이 끝나지 않았을때 들어오면, 최종 counter 값은 5가 되어야 하지만, 최종 결과는 counter가 4인걸로 끝난다.</p>
<p>이런 문제를 해결하는데 가장 널리 알려진 방법은 <strong>락(lock)이다.</strong> 하지만 락은 시스템의 성능을 상당히 떨어뜨리는다는 문제가 있다. 위 설계의 경우 락 대신에 사용할 수 있는 방법이 2가지 정도 있다.</p>
<ul>
<li>루아 스크립트 이용</li>
<li>레디스의 정렬 집합과 같은 자료 구조 이용하기</li>
</ul>
<p><strong>동기화 이슈</strong></p>
<p>수백만 사용자를 지원하려면 한 대의 처리율 제한 장치 서버로는 충분하지 않을 수 있다.</p>
<p>그래서 처리율 제한 장치 서버를 여러 대 두게 되면 <strong>동기화가 필요해진다.</strong></p>
<p>아래 사진을 보면 클라이언트1 은 제한장치 1에게 요청을 하고, 클라이언트2는 제한장치2에게 요청을 한다.</p>
<p>웹 계층은 무상태이므로 클라이언트는 오른쪽 그림과 같이 각기 다른 제한 장치로 보내질 수 도 있다.</p>
<p>이상황에서 제한장치가 서로 동기화 하지 않는다면 제한장치1은 클라이언트 2에 대해서는 아무것도 모르므로 처리율 제한을 올바르게 수행할 수 없을 것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e51a7e73-334a-4505-b254-0ccbdab5829e/image.png" alt=""></p>
<p>이에 대한 해결책으로는 <strong>고정 세션</strong>을 활용하여 같은 클라이언트로 부터 요청은 항상 같은 제한장치로 보낼 수 있게 하는 것이지만, 이 방법은 추천하지 않는다. 규모면에서 <strong>확장 가능하지도 않고 유연하지도 않기 때문</strong>이다. 더 나은 해결책은 ‘레디스’와같은 중앙 집중형 데이터 저장소를 쓰는 것이다. 이 접근법에 대한 설계는 아래 그림이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/24ad88ed-a707-4cf3-982c-335957b21ab8/image.png" alt=""></p>
<h3 id="성능-최적화">성능 최적화</h3>
<ol>
<li>데이터센터를 지원하는 방법</li>
</ol>
<p>여러 데이터센터를 지원하는 문제는 처리율 제한 장치에 대한 매우 중요한 문제라는 것을 <strong>상기해야한다.</strong></p>
<p>데이터센터에서 멀리 떨어진 사용자를 지원하려다 보면 <strong>지연시간이 증가</strong>할 수 밖에 없기 때문이다.</p>
<p>실제 사례로는 클라우드플레어(Cloudflare)라는 회사는 세계 곳곳에 에지 서버(edge server)를 심어 놓고 있고, 사용자의 트래픽을 가장 가까운 에지 서버로 전달하여 지연시간을 줄이고 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/8902d093-a659-4018-b150-215aaa09e681/image.png" alt=""></p>
<ol>
<li>데이터를 동기화할 때 <strong>최종 일관성 모델</strong>을 사용하는것</li>
</ol>
<p>이는 6장의 “키-값 저장소 설계”의 “데이터 일관성” 항목에 자세히 나와있다.</p>
<h3 id="모니터링">모니터링</h3>
<p>처리율 제한 장치가 효과적으로 동작하고 있는지 데이터를 모을 필요가 있고, 확인 해보고자 하는 항목은 다음 두 가지다.</p>
<ul>
<li>채택된 처리율 제한 알고리즘이 효과적이다.</li>
<li>정의한 처리율 제한 규칙이 효과적이다.</li>
</ul>
<p>깜짝 세일 같은 이벤트 때문에 트래픽이 급증할 때 처리율 제한 장치가 비효율적으로 동작한다면, 그런 트래픽 패턴을 잘 처리할 수 있도록 알고리즘을 바꾸는 것을 생각해 봐야 하며, 그럴 때는 <strong>토큰 버킷</strong>이 적합할 것이다.</p>
<h2 id="4단계-마무리">4단계 마무리</h2>
<p>이번 장에서 다루었던 처리율 제한을 구현하는 여러 알고리즘과, 그 장단점을 살펴보자.</p>
<ul>
<li>토큰 버킷</li>
<li>누출 버킷</li>
<li>고정 윈도 카운터</li>
<li>이동 윈도 로그</li>
<li>이동 윈도 카운터</li>
</ul>
<p>알고리즘 이외에도 해당 알고리즘을 구현하는 아키텍처, 분산환경에서의 처리율 제한 장치, 성능 최적화와 모니터링 등의 주제를 살펴보았다. 여느 시스템 설게 문제와 마찬가지로, 시간이 허락한다면 다음과 같은 부분을 언급하면 도움이 될 것이다.</p>
<ul>
<li>경성 또는 연성 처리율 제한<ul>
<li>경성 처리율 제한: 요청의 개수는 임계치를 절대 넘을 수 없다.</li>
<li>연성 처리율 제한: 요청 개수는 잠시 동안은 임계치를 넘어설 수 있다.</li>
</ul>
</li>
<li>다양한 계층에서의 처리율 제한<ul>
<li>이번 장에서는 OSI 7계층의 애플리케이션 계층(HTTP)에서의 처리율 제한에 대해서만 알아보았다. 하지만 다른 계층에서도 처리율 제한이 가능하다. 예를 들어, Iptables를 사용하면 IP는 OSI 3계층에 속하므로 3계층 처리율 제한을 적용하는 것이 가능하다.</li>
</ul>
</li>
<li>처리율 제한을 회파하는 방법. 클라이언트는 어떻게 설계하는것이 최선인가?<ul>
<li>클라이언트 측 캐시를 사용하여 API 호출 횟수를 제한한다.</li>
<li>처리율 제한의 임게치를 이해하고, 짧은 시간 동안 너무 많은 메시지를 보내지 않도록 한다.</li>
<li>예외나 에러를 처리하는 코드를 도입하여 클라이언트가 예외적 상황으로 부터 우아하게 복구될 수 있도록 해야한다.</li>
<li>재시도 로직을 구현할 때는 충분한 <strong>백오프</strong>시간을 둔다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[시스템 설계 면접 공략법]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EB%A9%B4%EC%A0%91-%EA%B3%B5%EB%9E%B5%EB%B2%95</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%84%A4%EA%B3%84-%EB%A9%B4%EC%A0%91-%EA%B3%B5%EB%9E%B5%EB%B2%95</guid>
            <pubDate>Sat, 05 Aug 2023 14:04:50 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>시스템 설계 면접은 정해진 결말도 없고, 정답도 없는 모호한 면접이다. 이 면접의 핵심은 설계 기술을 시연하는 자리이고, 설계 과정에서 내린 결정들에 대한 방어 능력을 보이는 자리이며, 면접관의 피드백을 ‘건설적인’방식으로 처리할 자질이 있음을 보이는 자리이다.</p>
<p>시스템 설계에서 평가하고자 하는 것은</p>
<ul>
<li>지원자가 협력에 적합한 사람인가?</li>
<li>압박이 심한 상황도 잘 헤쳐 나갈 자질이 있는가?</li>
<li>모호한 문제를 건설적으로 해결할 능력이 있는가?</li>
<li><strong>좋은 질문을 던질 능력이 있는가?</strong></li>
</ul>
<p>훌륭한 면접관은 부정적인 신호도 놓치지않는다. 설계의 순수성에 집착한 나머지 타협적 결정을 도외시하고 <strong>과도한 엔지니어링</strong>을 하고 마는 엔지니어가 현업에도 많기 때문이다. 그런 엔지니어들은 과도한 엔지니어링의 결과로 시스템 전반의 비용이 올라간다는 사실을 <strong>알아 채지 못한다</strong> 이런 경향이 있다는 것을 보이고 싶지 않을것이고, 이것 외에도 부정적 신호로는 <strong>완고함, 편헙함</strong>같은 것들도 있다.</p>
<h2 id="효과적-면접을-위한-4단계-접근법">효과적 면접을 위한 4단계 접근법</h2>
<h3 id="1단계-문제-이해-및-설계-범위-확정">1단계 문제 이해 및 설계 범위 확정</h3>
<p>요구사항을 완전히 이해하고 답을 내놓아라.</p>
<p>요구사항을 정확히 이해하는 데 필요한 질문을하라. 예를들면,</p>
<ul>
<li>구체적으로 어떤 기능들을 만들어야 하나?</li>
<li>제품 사용자 수는 얼마나 되나?</li>
<li>회사의 규모는 얼마나 빨리 커지리라 예상하나? 석 달, 여섯 달, 일년 뒤의 규모는 얼마가 되리라 예상하는가?</li>
<li>회사가 주로 사용하는 기술 스택(technology stack)은 무엇인가? 설계를 단순화하기 위해 활용할 수 있는 기존 서비스로는 어떤 것들이 있는가?</li>
</ul>
<h3 id="2단계-개략적인-설계안-제시-및-동의-구하기">2단계 개략적인 설계안 제시 및 동의 구하기</h3>
<p>면접관과 협력하며 진행하면 좋다.</p>
<ul>
<li>설계안에 대한 청사진을 제시하고 의견을 구하라. 면접관을 팀원인 것처럼 대하라. 훌륭한 면접관들은 지원자들과 대화하고 설계 과정에 개입하기를 즐긴다.</li>
<li>화이트보드나 종이에 핵심 컴포넌트를 포함하는 다이어그램을 그려라. 클라이언트(모바일/웹), API, 웹 서버, 데이터 저장소, 캐시, CDN, 메시지 큐 같은 것들이 포함될 수 있을 것이다.</li>
<li>이 최초 설계안이 시스템 규모에 관계된 제약사항들을 만족하는지 개략적으로 계산해 보라. 계산 과정은 소리 내어 설명하라. 아울러, 이런 개략적 추정이 필요한지는 면접관에게 미리 물어보도록 하자.</li>
</ul>
<p>이 단계에서 API 엔드포인트나 뎅티ㅓ베이스 스키마보 보여야하는가? 이건 물어보자.</p>
<h3 id="3단계-상세-설계">3단계 상세 설계</h3>
<ul>
<li>시스템에서 전반적으로 달성해야 할 목표와 기능 범위 확인</li>
<li>전체 설계의 개략적 청사진 마련</li>
<li>해당 청사진에 대한 면접관의 의견 청취</li>
<li>상세 설계에서 집중해야 할 영역들 확인</li>
</ul>
<p>위 목표는 달성한 상태일것이다.</p>
<p>면접 시에는 시간 관리에도 특별히 주의를 기울여야 한다. 사소한 세부사항을 설명하느라 정작 여러분의 능력을 보일 기회를 놓쳐버릴 수도 있기 때문이다.</p>
<h3 id="4단계-마무리">4단계 마무리</h3>
<p>이 마지막 단계에선 면접관은 설계 결과물에 관련된 몇 가지 후속 질문을 던질 수도 있고 여러분 스스로 추가 논의를 진행하도록 할 수 도 있다.</p>
<ul>
<li>면접이 끝나고 시스템 병목구간, 혹은 좀 더 개선 가능한 지점을 찾아내라 주문할 수 있다. 완벽한 설계라고 하지말고, 개선할 점은 언제나 있다. 이런 질문은 여러분의 <strong>비판적 사고 능력을 보이고, 마지막으로 좋은 인상을 남길 기회</strong>다.</li>
<li>만든 설계를 다시 한번 요약해주는 것도 도움이 될 수 있다. 면접관의 기억을 환기시켜주는 효과가 있을 것 이다.</li>
<li>오류가 발생하면 무슨 일이(서버 오류, 네트워크 장애 등) 따져보면 흥미로울 것이다.</li>
<li>운영 이슈도 논의할 가치가 충분하다. 메틝은 어떻게 수집하고 모니터링 할 것인가? 로그는? 시스템은 어떻게 배포해 나갈 것인가?</li>
<li>미래에 닥칠 규모 확장 요구에 어떻게 대처할 것인지도 흥미로운 주제다.</li>
<li>시간이 좀 남았다면, 필요하지만 다루지 못했던 세부적 개선사항들을 제안할 수 있다.</li>
</ul>
<h3 id="해야-할것">해야 할것</h3>
<ul>
<li>질문을 통해 확인해라. 스스로 내린 가정이 옳다 믿고 진행하지 말라.</li>
<li>문제의 요구사항을 이해하라.</li>
<li>정답이나 최선의 답안 같은 것은 없다는 점을 명심하라. 스타트업을 위한 설계안과 수백만 사용자를 지원해야 하는 중견 깅버을 위한 설계안이 같을리가 없다.</li>
<li>면접관이 여러분의 사고 <strong>흐름</strong>을 이해할 수 있도록 하라.</li>
<li>가능하다면 여러 해법을 함께 제시하라.</li>
<li>가능하다면 여러 해법을 함께 제시하라.</li>
<li>개략적 설계에 면접관이 동의하면, 각 컴포넌트의 세부사항을 설명하기 시작하라.</li>
<li>면접관의 아이디어를 이끌어 내라. 좋은 면접관은 여러분과 같은 팀원처럼 협력하라.</li>
<li>포기하지 말라.</li>
</ul>
<h3 id="하지-말아야-할것">하지 말아야 할것</h3>
<ul>
<li>전형적인 면접 문제들에도 대비하지 않은 상태에서 면접장에 가지 마라.</li>
<li>요구사항이나 가정들을 분명히 하지 않은 상태에서 설계를 제시하지 마라.</li>
<li>처음부터 특정 컴포넌트의 세부사항을 너무 깊이 생각하지마라.</li>
<li>진행 중에 막혔다면, 힌트를 청하기를 주저하지 말라.</li>
<li>다시 말하지만, 소통을 주저하지 말라. 침묵 속에 설계를 진행하지 말라.</li>
<li>설계안을 내놓는 순간 면접이 끝난다고 생각 하지말라.</li>
</ul>
<h2 id="시간-배분">시간 배분</h2>
<p>대략적인 추정치일 뿐이다.</p>
<ol>
<li>문제 이해 및 설계 범위 확정: 3분에서 10분</li>
<li>개략적 설계안 제시 및 동의 구하기: 10분에서 15분</li>
<li>상세 설계: 10분에서 25분</li>
<li>마무리: 3분에서 5분</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[개략적인 규모 측정]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EA%B0%9C%EB%9E%B5%EC%A0%81%EC%9D%B8-%EA%B7%9C%EB%AA%A8-%EC%B8%A1%EC%A0%95</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EA%B0%9C%EB%9E%B5%EC%A0%81%EC%9D%B8-%EA%B7%9C%EB%AA%A8-%EC%B8%A1%EC%A0%95</guid>
            <pubDate>Sat, 05 Aug 2023 14:02:39 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<p>개략적인 규모 추정을 효과적으로 해 내려면 규모 확장성을 표현하는 데 필요한 기본기에 능숙해야 한다. 특히, <strong>2의 제곱수</strong>나 <strong>응답지연 값</strong>, 그리고 <strong>가용성</strong>에 관계된 수치들을 기본적으로 잘 이해하고 있어야 한다.</p>
<h2 id="2의-제곱수">2의 제곱수</h2>
<p>분산 시스템에서 데이터 양은 엄청나게 커질 수 있으나 그 계산법은 기본을 크게 벗어나지 않는다. 제대로 된 계산 결과를 얻으려면 <strong>데이터 볼륨의</strong>단위를 2의 제곱수로 표현하면 어떻게 되는지 알아야 한다. 최소 단위는 1바이트고, 8비트로 구성된다. ASCII 문자 하나가 차지하는 메모리 크기가 1바이트다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/e6b7761c-477a-4d4f-af17-8c74267ff6a5/image.png" alt=""></p>
<p>데이터 볼륨 단위다.</p>
<h2 id="모든-프로그래머가-알아야-하는-응답지연-값">모든 프로그래머가 알아야 하는 응답지연 값</h2>
<p>구글의 제프 딘은 2010년에 통상적인 컴퓨터에서 구현된 연산들의 응답지연 값을 공개한 바 있다. 이들 가운데 몇몇은 더 빠른 컴퓨터가 등장하면서 더이상 유효하지 않게 되었지만, 아직도 이 수치들은 컴퓨터 연산들의 처리속도가 어느 정도인지 짐작할 수 있게 해준다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/1abee46e-e851-4615-9710-cc4b19d38f21/image.png" alt=""></p>
<p>아래는 위 표를 시각화 한것이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/26d2a233-f72e-4845-9cdd-3c25f9387935/image.png" alt=""></p>
<p>위 두 사진을 보면</p>
<ul>
<li>메모리는 빠르지만 디스크는 아직도 느리다</li>
<li>디스크 탐색(seek)은 가능한 피해라</li>
<li>단순한 압축 알고리즘은 빠르다</li>
<li>데이터를 인터넷으로 전송전 압축해라</li>
<li>데이터 센터는 보통 여러 지역에 분산되어 있고, 센터들 간에 데이터를 주고받는 데에 시간이 걸린다.</li>
</ul>
<p>라는 결론이 나온다.</p>
<h2 id="가용성에-관계된-수치들">가용성에 관계된 수치들</h2>
<p>고가용성은 시스템이 오랜 시간 동안 지속적으로 중단 없이 운영될 수 있는 수치인데 이는 %(percent)로 나타낸다. 100%는 시스템이 한 번도 중단된 적이 없었음을 의미한다.</p>
<p>대부분의 서비스는 99%~100%의 값을 갖는다.</p>
<p>아마존, 구글 ms같은 사업자는 SLA(Service Level Argreement)를 제공해주는데</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/a7c2fc0c-f6e0-454f-9722-14dd69eb8d55/image.png" alt=""></p>
<p>9가 많을수록 좋다.</p>
<h2 id="예제-트위터-qps와-저장소-요구량-추정">예제: 트위터 QPS와 저장소 요구량 추정</h2>
<p>가정</p>
<ul>
<li>월간 능동 사용자는 3억 명이다.</li>
<li>50%의 사용자가 트위터를 매일 사용한다.</li>
<li>평균적으로 각 사용자는 매일 2건의 트윗을 올린다</li>
<li>미디어를 포함하는 트윗은 10% 정도다.</li>
<li>데이터는 5년간 보관된다.</li>
</ul>
<p>추정</p>
<p>QPS(Query Per Second) 추정치</p>
<ul>
<li>일간 능동 사용자 = 3억 * 50% = 1.5억</li>
<li>QPS=1.5억*2트윗/24시간/3600초=약 3500</li>
<li>최대 QPS(Peek QPS)=2*QPS=약 7000</li>
</ul>
<p>미디어 저장을 위한 저장소 요구량</p>
<ul>
<li>평균 트윗 크기<ul>
<li>tweet_id에 64바이트</li>
<li>텍스트에 140바이트</li>
<li>미디어에 1MB</li>
</ul>
</li>
<li>미디어 저장소 요구량: 1.5억 <em>2</em>10%*1MB=30TB/일</li>
<li>5년간 미디어를 보관하기 위한 저장소 요구량: 30TB<em>365</em>5=약 55PB</li>
</ul>
<h2 id="팁">팁</h2>
<p>면접자가 보고싶어 하는 것은 여러분의 문제 해결 능력일 것이다. 이에 도움되는 몇 가지 꿀팁을 공유한다.</p>
<ul>
<li>단위를 붙여라. 5라고만 적으면 kb인지mb인지 알수 없다. 나중엔 스스로 헷갈리게 될 것이다.</li>
<li>많이 출제되는 개략적 규모 추정 문제는 QPS, 최대 QPS, 저장소 요구량, 캐시 요구량, 서버 수 등을 추정하는 것이다. 면접 전에 계산하는 연습을 미리 하도록 하자.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[사용자 수에 따른 규모 확장성]]></title>
            <link>https://velog.io/@dev_dc_hyeon/%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%88%98%EC%97%90-%EB%94%B0%EB%A5%B8-%EA%B7%9C%EB%AA%A8-%ED%99%95%EC%9E%A5%EC%84%B1</link>
            <guid>https://velog.io/@dev_dc_hyeon/%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%88%98%EC%97%90-%EB%94%B0%EB%A5%B8-%EA%B7%9C%EB%AA%A8-%ED%99%95%EC%9E%A5%EC%84%B1</guid>
            <pubDate>Fri, 28 Jul 2023 05:27:17 GMT</pubDate>
            <description><![CDATA[<p>포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.</p>
<h1 id="❓수백만-사용자를-지원하는-시스템을-만드려면-어떻게-설계해야-할까">❓수백만 사용자를 지원하는 시스템을 만드려면 어떻게 설계해야 할까</h1>
<h2 id="기본적인-단일-서버-예">기본적인 단일 서버 예</h2>
<p>웹, 앱, 데이터베이스, 캐시 등이 전부 서버 한 대에서 실행된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c9cdee96-ef22-494a-a0d0-790b2a81ce33/image.png" alt=""></p>
<ol>
<li>사용자는 웹 브라우저나 모바일 앱으로 api.mysite.com 도메인으로 접속을 한다</li>
<li>api.mystie.com 도메인은 DNS서버에 의해 15.125.23.214로 번역된다.</li>
<li>15.125.23.214라는 IP를 가진 웹 서버로 전달이 된다.</li>
<li>웹서버는 사용자의 기기에 <strong>JSON</strong> OR HTML 페이지를 전달해 준다.</li>
</ol>
<h2 id="데이터베이스">데이터베이스</h2>
<p>사용자가 많으면 서버 하나로는 충분하지 않아 <strong>여러 서버</strong>를 두어야 한다.</p>
<p>하나는 웹/모바일 <strong>트래픽 처리 용도</strong>고 하나는 <strong>데이터 저장 용</strong>이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/9ecb8488-2aae-4b58-a701-924f76068c2b/image.png" alt=""></p>
<p>그림에서 보면 웹 서버는 트래픽 처리 용, 데이터베이스가 데이터베이스가 된다.</p>
<h3 id="❓어떤-데이터베이스를-사용-해야-할까">❓어떤 데이터베이스를 사용 해야 할까</h3>
<p>관계형 데이터베이스 (RDBMS)와 비-관계형 데이터베이스 (NOSQL) 사이에서 고를 수 있다.</p>
<p>➡️ 대표적인 RDMBS</p>
<ul>
<li>MySQL, Orale, PostgresSQL</li>
</ul>
<p>이들은 여러 테이블에 있는 관계를 <strong>join</strong>해서 사용한다.</p>
<p>➡️ 대표적인 비 관계형 데이터베이스 NoSQL</p>
<ul>
<li><p>CouchDB, Neo4j, Cassandra, HBase, Amazon DynamoDB, MongoDB 등이 있다.</p>
<p>  이는 다시 4가지 부류로 나눌 수 있다.</p>
<ul>
<li>키-값 저장소</li>
<li>그래프 저장소</li>
<li>칼럼 저장소</li>
<li>문서 저장소</li>
</ul>
</li>
</ul>
<p>💡 대부분의 개발자가 관계형 데이터베이스(RDBMS)를 사용하는 이유는 <strong>40년 이상 시장에서 살아남았기 때문이다.</strong></p>
<p>비-관계형(NOSQL)을 사용하는 이유❓</p>
<ul>
<li>아주 낮은 응답 지연시간(latency)이 요구됨</li>
<li>다루는 데이터가 비정형(unstructured)이라 관계형이 아님 <strong>join</strong>이 필요없다.</li>
<li>데이터(JSON, YAML, XML 등)을 직렬화 하거나 역직렬화 할 수 있기만 하면 됨</li>
<li>아주 많은 양의 데이터를 저장할 필요가 있음</li>
</ul>
<h2 id="수직적-vs-수평적-규모-확장">수직적 vs 수평적 규모 확장</h2>
<h3 id="수직적스케일-업">수직적(스케일 업)</h3>
<p><strong>서버로 유입되는 트랙픽의 양이 적을 때는 수직적 확장이 좋은 선택이다</strong> 왜냐하면 <strong>단순하기 때문이다</strong></p>
<p>하지만 아래와 같은 심각한 단점이 있다.</p>
<ul>
<li>수직적 규모확장은 한계가 있다. 서버 한대에 무한대로 CPU와 같은 하드웨어를 무한대로 추가할 수 없기 때문</li>
<li>장애에 대한 자동복구나 다중화 방안을 제시하지 않는다. 장애가 생기면 웹사이트/앱은 완전히 중단된다.</li>
</ul>
<h3 id="수평적스케일-아웃">수평적(스케일 아웃)</h3>
<ul>
<li>위 수직적 규모 확장의 단점들을 보완해 줄 수 있다.</li>
</ul>
<h2 id="로드밸런서">로드밸런서</h2>
<p>로드밸런서는 웹 서버들에게 트래픽 부하를 고르게 분산하는 역할을 해준다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/04265e2a-ddc5-4826-9ff4-c8d2db2f9e96/image.png" alt=""></p>
<p>그림처럼 사용자의 단말기 들은 로드밸런서의 Public (공용 IP)로 접속하게 되고 로드밸런서는 트래픽을 적절히 분배하여 사설 IP(Private IP)를 사용하는 서버들로 사용자의 요청을 분배하게 된다.</p>
<p>💡 사설 IP는 같은 네트워크에 속한 서버 사이의 통신에만 사용할 수 있기에 보안이 향상됨</p>
<p>이렇게 부하 분산 집합에 웹 서버를 추가하고 나면 장애를 자동복구하지 못하는 문제(no failover)는 해소되고, 웹 계층의 <strong>가용성</strong>은 향상된다.</p>
<ul>
<li>서버 1이 다운되면 부하 분산을 시켜주는 로드밸런서는 서버2 에게 모든 트래픽을 요청한다 따라서 웹사이트 전체가 다운되는 일이 방지 됨.</li>
<li>웹사이트로 유입되는 트래픽이 가파르게 증가하면 웹 서버 계층에 더 많은 서버를 추가하기만 하면 로드밸런서가 알아서 트래픽을 분산시켜 주기 때문에 확장이 쉽다.</li>
</ul>
<h2 id="데이터베이스-다중화">데이터베이스 다중화</h2>
<p>대부분의 데이터베이스 관리 시스템들은 <strong>다중화</strong>를 지원한다. 보통은 서버 사이에 주(master)-부(slave) 관계를 설정하고 데이터 원본은 주 서버에, 사본은 부 서버에 저장하는 방식이다.</p>
<p>쓰기(업데이트,삭제) 연산은 master서버에서만 지원하며 slave 데이터베이스는 master로부터 그 사본을 전달받으며 오직 <strong>read(읽기)연산 만을 지원한다</strong>. 대부분의 애플리케이션은 <strong>읽기 연산의 비중이 CUD(Create, Update, Delte)연산보다 비중이 훨씬 높기</strong>에 읽기만을 지원하는 slave 데이터베이스가 master 데이터베이스보다 더 많다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/419db25e-be22-449c-b99c-64533b39c700/image.png" alt=""></p>
<p>위 그림과 같이 slave db가 master db보다 더 많다.</p>
<h3 id="데이터베이스-다중화의-장점">데이터베이스 다중화의 장점</h3>
<ul>
<li>더 나은 성능: master-slave 모델에서 CUD연산은 master에서만 지원하고 <strong>수많은</strong> slave db가 받으므로 성능이 좋아짐</li>
<li>안정성: 자연재해 등으로 db 서버 가운데 일부가 파괴되어도 데이터는 보존된다. 데이터를 지역적으로 멀리 떨어진 여러 장소에 다중화 시켜 놓을 수 있기 때문이다.</li>
<li>가용성: 데이터를 여러 지역에 복제해 둠으로써, 특정 서버가 다운되어도 다른 서버의 데이터를 가져다가 쓰면 되기 때문에 계속 서비스를 할 수 있다.</li>
</ul>
<h3 id="❗데이터베이스-서버-중-하나가-다운될-경우의-상황">❗데이터베이스 서버 중 하나가 다운될 경우의 상황</h3>
<ul>
<li>slave 서버가 한대 뿐인데 다운되면 일시적으로 모두 master가 읽기 연산도 포함하여 장애를 대처함</li>
<li>master 서버가 다운될 경우 slave 서버 중 하나가 새로운 master 서버가 되어 장애를 대처 할 것이다.<ul>
<li>실제 운영 환경에서 벌어지는 환경은 더 복잡하다, slave db의 데이터가 최신 상태가 아닐 수도 있기 때문이다. 없는 데이터는 <strong>복구 스크립트를 돌려서 추가</strong> 해야한다. 다중 마스터나 원형 마스터 방식을 도입하면 이런 상황을 대처하는 데 도움이 되지만 해당 구성들은 훨씬 복잡하다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c77f7503-63ad-466a-8b59-a0c6b80a37de/image.png" alt=""></p>
<h2 id="캐시">캐시</h2>
<p><strong>응답시간 개선에 사용</strong></p>
<p>캐시는 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고, 뒤이은 요청이 보다 빨리 처리될 수 있는 저장소이다.</p>
<h3 id="캐시-계층">캐시 계층</h3>
<p>캐시 계층은 데이터가 잠시 보관되는 곳으로 데이터베이스보다 훨씬 빠르다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c0e1c750-1a90-4e70-a262-ae0d3a95e8fe/image.png" alt=""></p>
<p>위 사진과 같이 계시 계층을 중간에 두면 데이터베이스의 부하를 줄일 수 있으며, 캐시 계층을 독립적으로 확장할 수 있다.</p>
<aside>
💡 **주도형 캐시 전략**
위 사진의 나와있는 1→2.1or2.2의 순서를 주도형 캐시 전략을 이라고 부른다.
</aside>

<p>이것외에도 다양한 캐시 전략이 있는데, 캐시할 데이터 종류, 크기, 엑세스 패턴에 맞는 캐시 전략을 선택하면 된다.</p>
<h3 id="❗캐시-사용-시-유의할-점">❗캐시 사용 시 유의할 점</h3>
<ul>
<li>캐시는 어떤상황에 바람직한가? 데이터 갱신은 자주 일어나지 않지만 참조는 빈번하게 일어난다면 고려해볼만 하다.</li>
<li>어떤 데이터를 캐시에 두어야 하는가? 캐시는 데이터를 휘발성 메모리에 두므로, 영속적으로 보관할 데이터를 캐시에 두는 것은 바람직하지 않다. 예를 들어, 캐시 서버가 재시작 되면 캐시 내의 모든 데이터는 사라진다. 중요 데이터는 여전히 지속적 저장소에 두어야한다.</li>
<li>캐시에 보관된 데이터는 어떻게 만료(expire)되는가? 이에 대한 정책을 마련해 두는 것은 좋은 습관이다. 만료된 데이터는 캐시에서 삭제되어야 한다. 만료 정책이 없으면 데이터는 캐시에 계속 남아있기 떄문이다. 만료 기한은 너무 짧으면 곤란한데, 데이터베이스를 <strong>너무 자주 읽게 될 것이기 때문</strong>이다. 너무 길어도 곤란한데, 원본과 차이가 날 가능성이 높기 때문이다.</li>
<li>일관성은 어떻게 유지되는가? 일관성은 데이터 저장소의 원본과 캐시 내의 사본이 같은지 여부다. 저장소의 원본을 갱신하는 연산과 캐시를 갱신하는 연산이 <strong>단일 트랜잭션으로</strong> 처리되지 않는 경우 이 일관성은 깨질 수 있다. 여러 지역에 걸쳐 시스템을 확장해 가는 경우 캐시와 저장소 사이의 일관성을 유지하는 것은 어려운 문제가 된다.</li>
<li>장애에는 어떻게 대처할 것인가? 캐시 서버를 한 대만 두는 경우 해당 서버는 단일 장애 지점(SPOF)이 되어버릴 가능성이 있다.</li>
</ul>
<aside>
💡 SPOF란?
어떤 특정한 지점 에서의 장애가 **전체 시스템의 동작을 중단**시켜버릴 수 있다 라는 것이다.
</aside>

<p><strong>SPOF(단일 장애 지점)</strong>를 피하려면 여러 지역에 걸쳐 캐시 서버를 분산시켜야한다.</p>
<ul>
<li>캐시 메모리는 얼마나 크게 잡을 것인가?</li>
<li>데이터 방출 정책은 무엇인가? 대표적으로 널리 쓰이는<ul>
<li>LRU(Leaset Recently Used -마지막으로 사용된 지점이 가장 오래된 데이터를 내보내는 정책)이다.</li>
<li>LFU(Lease Frequently Used -사용된 빈도가 가장 낮은 데이터를 내보내는 정책)</li>
<li>FIFO(First In First Out - 가장 먼저 캐시에 들어온 데이터를 가장 먼저 내보내는 정책) 같은 것들이 있다.</li>
</ul>
</li>
</ul>
<h2 id="콘텐츠-전송-네트워크cdn">콘텐츠 전송 네트워크(CDN)</h2>
<p>CDN은 정적 콘텐츠를 전송할 때 쓰이는 분산 네트워크다. HTML, 이미지, 비디오, CSS, JS 파일등을 캐시할 수 있다.</p>
<aside>
💡 CDN은 정적 콘텐츠를 캐시한다.
동적 콘텐츠 캐싱은 어떻게 할까?
</aside>

<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/90b6f0a3-b53f-4431-ab17-8b6fecebb8a1/image.png" alt=""></p>
<p>CDN 이 대충 어떻게 동작하는 가에 대한 사진이다. 당연히 거리가 멀면 멀수록 웹사이트는 천천히 로드된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/1502db4d-37d2-4f5a-92cf-b0b30ba3e46c/image.png" alt=""></p>
<p>CDN이 어떻게 동작되는가에 대해 자세히 알아보자.</p>
<ol>
<li>사용자는 이미지 URL을 이용해 image.png에 접근한다</li>
<li>CDN 서버의 캐시에 이미지가 없는 경우 원본서버에 접근하여 이미지를 요청한다</li>
<li>CDN 서버는 요청한 파일을 서버 캐시에 저장한다. 응답의 HTTP 헤더에는 파일이 얼마나 오래되었는지에 대한 TTL(Time-To_Live) 값이 들어있다.</li>
<li>CDN 서버는 파일을 캐시하고 사용자에게 반환한다. 이미지는 TIL에 명시된 시간이 끝날 떄 까지 캐시된다.</li>
<li>다른 사용자가 같은 이미지에 요청을 하면</li>
<li>CDN은 캐시에 저장되어있는 이미지를 바로 반환 해준다.</li>
</ol>
<h3 id="cdn-사용-시-고려해야할-상황">CDN 사용 시 고려해야할 상황</h3>
<ul>
<li>비용: CDN은 보통 제 3 사업자에의해 운영되며, 데이터 전송 양에 따라 요금을 내게된다. 자주 사용되지 않는 파일을 캐싱하는것은 이득이 크지 않으므로 CDN에서 빼는것을 고려해야한다.</li>
<li>적절한 만료 시한 설정: 시의성이 중요한(time-sensitive) 콘텐츠의 경우 만료 시점을 잘 정해야 한다. 길면 콘텐츠의 신서도가 떨어질 것이고, 너무 짧으면 서버에 빈번히 접속하게 되어 좋지않다.</li>
<li>CDN 장애에 대한 대처 방안: CDN 자체가 죽었을 경우 웹사이트/애플리케이션이 어떻게 동작해야 하는지 고려해야 한다. CDN서버에 장애가 발생해도 <strong>클라이언트 단에선 장애를 인지</strong>하고 원본서버에서 직접 콘텐츠를 가져오도록 구성해야 한다.</li>
<li>콘텐츠 무효화 방법: 아직 만료되지 않은 콘텐츠라 하더라도 아래 방법 중 하나를 쓰면 CDN에서 제거할 수 있다.<ul>
<li>CDN 서비스 사업자가 제공하는 API를 이용하여 콘텐츠 무효화</li>
<li>콘텐츠의 다른 버전을 서비스하도록 오브젝트 버저닝이용. 콘텐츠의 새로운 버전을 지정하기 위해서는 URL 마지막 버전 번호를 인자로 주면 된다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/5943e18a-a621-42d8-8984-503b2ad589e1/image.png" alt=""></p>
<p>변화된 부분은</p>
<ol>
<li>정적 콘텐츠는 더 이상 웹서버를 통해 서비스하지 않으며 CDN을 통해 더 나은 성능을 보장</li>
<li>캐시가 DB 부하를 줄여줌</li>
</ol>
<h2 id="무상태-웹-계층">무상태 웹 계층</h2>
<p>이젠 웹 계층을 수평적으로 확장하는 방법을 고민해보자. 이를 위해서는 상태 정보(사용자 세션 데이터와 같은)를 웹 계층에서 제거해야 한다. 바람직한 전략은 상태 정보를 관계형 DB나 NoSQL같은 지속성 저장소에 저장하고, 필요할 때 가져오도록 해야 한다. 이렇게 상태 정보가 없는 웹 계층을 무 상태 웹 계층이라고 표현한다.</p>
<h3 id="상태-정보-의존적인-아키텍처">상태 정보 의존적인 아키텍처</h3>
<p>상태 정보를 보관하는 서버와 그렇지 않은 서버 사이에는 몇 가지 중요한 차이가 있다. 상태 정보를 보관하는 서버는 클라이언트 정보, 즉 상태를 유지하여 요청들 사이에 공유되도록 해야한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/d44a7376-70b4-48a1-a23a-00cd1a455bca/image.png" alt=""></p>
<p>상태 정보 의존적인 아키텍처 사진</p>
<p>이렇게 되면 A는 무조건 서버 1에서 요청을 해줘야 하고, B는 서버2, C는 서버 3에서만 응답을 해주어야 된다. 로드밸런서는 트래픽 부하를 줄이기 위해 사용하는 것인데, 이미 세션이 물려있으면 같은 서버에서만 계속 응답을 해주어야 한다. 이를 위해 로드밸런서는 <strong>고정 세션</strong>이라는 기능을 제공하지만, 이는 <strong>로드밸런서에 부담을 주게된다.</strong> 또한, 로드밸런서 뒷단에 서버를 추가하거나 제거하기도 까다로워진다.</p>
<h3 id="무상태-아키텍처">무상태 아키텍처</h3>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/848129cd-219e-4a57-adfe-097eb27e8949/image.png" alt=""></p>
<p>반면, 무상태 아키텍처에서는 어떤 사용자든 어떤 서버로도 응답이 가능해진다. 웹 서버는 상태 정보가 필요할 경우 공유저장소(데이터베이스)로 부터 데이터를 가져오면 되기 때문이다.</p>
<p>이러한 구조는 단순하고, 안정적이며, 규모 확장이 쉽다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/7e4a0c14-f4a7-4ed8-b723-ae0ad91f9909/image.png" alt=""></p>
<p>이 공우저장소는 RDBMS 일 수도 있고 NoSQL일 수도 있다. 위 사진은 NoSQL을 선택한 것을 볼 수있는데, 규모 확장이 편해서이다. 1 자동 규모 확장(auto scaling)은 <strong>트래픽 양</strong>에 따라 웹 서버를 넣거나 빼기만 하면 자동으로 규모를 확장할 수 있게 되었따.</p>
<p>만약, 자신이 만든 서비스가 전세계에서 많이 사용되는 서비스라면 쾌적한 환경을 위해 <strong>여러 데이터센터</strong>를 지원하는 것이 필수다.</p>
<h2 id="데이터-센터">데이터 센터</h2>
<p>아래 그림은 두 개의 데이터 센터를 이용하는 사례다. 장애가 없는 상황에서 사용자는 가장 가까운 데이터 센터로 안내되는데, 이를 지리적 라우팅(geoDNS-routing 또는 geo-routing)이라고 부른다. 지리적 라우팅에서의 geoDNS는 사용자의 위치에 따라 도메인 이름을 어떤 IP주소로 변환할지 결정할 수 있도록 해 주는 DNS 서비스다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/fcc52dad-4b66-4fce-a93e-e2e6c9e93610/image.png" alt=""></p>
<p>이들 데이터 센터 중 하나에 심각한 장애가 발생하면 모든 트래픽은 장애가 없는 데이터 센터로 전송된다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/1e862ae2-764e-4efa-8c1a-afe4a50b01c3/image.png" alt=""></p>
<p>이렇게 구성하고자 하면 몇 가지 기술적 난제를 해결해야 한다.</p>
<ul>
<li>트래픽 우회: 올바른 데이터 센터로 트래픽을 보내는 효과적인 방법을 찾아야 한다. GeoDNS는 사용자에게서 가장 가까운 데이터센터로 트래픽을 보낼 수 있도록 해준다.</li>
<li>데이터 동기화: 데이터 센터마다 별도의 데이터베이스를 사용하고 있는 상황이라면, 장애가 자동으로 복구되어 트래픽이 다른 db를 우회된다 해도, 해당 데이터센터에는 찾는 데이터가 없을 수도 있다. 이런 상황을 막는 보편적인 전략은 데이터를 여러 데이터센터에 걸쳐 다중화하는 것이다.</li>
<li>테스트와 배포: 여러 데이터 센터를 사용하도록 구성한 상태라면 웹 사이트 또는 애플리케이션을 여러 위치에서 테스트 해 보는 것이 중요하다. <strong>자동화된 배포 도구</strong>는 모든 데이터 센터에 동일한 서비스가 설치되도록 하는 데 중요한 역할을 한다.</li>
</ul>
<p>시스템을 더 큰 규모로 확장하기 위해서는 시스템의 컴포넌트를 분리하여, 각기 독립적으로 확장될 수 있도록 해야 한다. <strong>메시지 큐는</strong> 많은 실제 분산 시스템이 이 문제를 풀기 위해 채용하고 있는 <strong>핵심적 전략</strong>가운데 하나다.</p>
<h2 id="메시지-큐">메시지 큐</h2>
<p>메시지 큐는 메시지의 <strong>무손실</strong> 즉, 메시지 큐에 일단 보관된 메시지는 소비자가 꺼낼 때까지 안전히 보관된다는 특성을 보장하는, 비동기 통신을 지원하는 컴포넌트다. 메시지 큐의 기본 아키텍처는 간단하다. 생성자 or 발행자 (producer/publisher) 라고 불리는 입력 서비스가 메시지를 만들어 메시지를 발행(publish)한다. 큐에는 보통 소비자 또는 구독자(consumer/subscriber)라 불리는 서비스 혹은 서버가 연결되어 있는데, 메시지를 받아 그에 맞는 동작을 수행한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/c2483d62-9367-4585-8ab5-33f3f706816d/image.png" alt=""></p>
<p>메시지 큐를 이용하면 서비스 또는 서버 간의 결합이 느슨해져서, 규모 확장성이 보장되어야 하는 애플리케이션을 구성하기 좋다. ‘소비자’는 생산자 서비스가 가용한 상태가 아니더라도 메시지를 수신할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/d45f2f64-1b62-469f-90a1-a9901089b156/image.png" alt=""></p>
<p>위 사진 처럼 웹서버가 발행한 메시지를 사진 보정 작업 프로세스(worker)들이 각각 독립적으로 확장 될 수 있다. <strong>큐의 크기가 커지면 더많은 작업 프로세스가 추가해야 처리</strong> 시간을 줄일 수 있다. 반대로 큐가 거의 항상 비어있는 상태라면 작업 프로세스의 수는 줄일 수 있다.</p>
<h2 id="로그-메트릭-그리고-자동화">로그, 메트릭 그리고 자동화</h2>
<p>규모가 커지면 로그, 메트릭, 자동화 에 필수적으로 설계 해야 한다.</p>
<ul>
<li>로그: 시스템의 오류와 문제들을 쉽게 찾아낼 수 있게 해준다. 서버 단위로 모니터링 할 수도 있지만 로그를 단일 서버로 모아주는 도구를 활용하면 더 편리하게 검색 및 조회할 수 있다.</li>
<li>메트릭: 사업 현황에 관한 유용한 정보를 얻을 수도 있고, 시스템의 현재 상태를 손쉽게 파악할 수 있다.<ul>
<li>호스트 단위 메트릭: CPU, 메모리, 디스크 I/O에 관한 메트릭이 겨이 해당</li>
<li>종합 메트릭: 데이터베이스 계층의 성능, 캐시 계층의 성능같은 것이 여기 해당</li>
<li>핵심 비즈니스 메트릭: 일별 능동 사용자, 수익, 재방문 같은 것이 여기 해당</li>
</ul>
</li>
<li>자동화: 시스템이 크고 복잡해지면 생산성을 높이기 위해 자동화 도구를 활용해야 한다. 지속적 통합(continuous integration)을 도와주는 도구를 활용하면 개발자가 만드는 코드가 어떤 검증 절차를 자동으로 거치도록 할 수 있어서 문제를 쉽게 감지할 수 있다. 이 외에도 빌드, 테스트, 배포 등의 절차를 자동화할 수 있어서 개발 생산성을 크게 향상시킬 수 있다.</li>
</ul>
<h3 id="메시지-큐로그메트릭자동화-등을-반영하여-수정한-설계안">메시지 큐,로그,메트릭,자동화 등을 반영하여 수정한 설계안</h3>
<ol>
<li>메시지 큐는 각 컴포넌트가 느슨히 결합될 수 있도록 하고, 결함에 대한 내성을 높인다.</li>
<li>로그, 모니터링, 메트릭, 자동화 등을 지원하기 위한 장치를 추가했다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/30a5ac30-fed2-4f45-8ccc-9cab0d6585c0/image.png" alt=""></p>
<h2 id="데이터베이스의-규모-확장">데이터베이스의 규모 확장</h2>
<p>저장할 데이터가 많으면 데이터베이스에 대한 부하도 증가한다. 데이터베이스는 어떻게 증설해야할까? 데이터베이스 수직적/수평적 규모 확장을 알아보자.</p>
<h3 id="수직적-확장">수직적 확장</h3>
<p>스케일 업이라고도 부르는 수직적 규모 확장법은 hardware를 늘리는것이다.</p>
<h3 id="수평적-확장">수평적 확장</h3>
<p>데이터베이스의 수평적 확장을 <strong>샤딩</strong>이라고도 부르는데, 더 많은 서버를 추가함으로써 성능을 향상시킬 수 있도록 한다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/a944cf52-227b-4ca4-a2f0-6324346880c2/image.png" alt=""></p>
<h3 id="데이터베이스-수평적-확장---샤딩">데이터베이스 수평적 확장 - 샤딩</h3>
<p><strong>샤딩</strong>은 대규모 데이터베이스를 <strong>샤드</strong>라고 부르는 작은 단위로 분할하는 기술을 일컫는다. 모든 샤드의 스키마는 동일하지만 데이터의 중복은 없다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/fbc36f1e-11bb-497b-aab7-39eb8e24eb2a/image.png" alt=""></p>
<p>위 그림은, 샤드로 분할된 데이터베이스를 어떤식으로 사용 되는지에 대한 사진이다.</p>
<p>샤딩 전략을 구현할 때 가장 중요한 것은 <strong>샤딩 키</strong>를 어떻게 정하느냐 이다.</p>
<p><strong>샤딩 키</strong>는 <strong>파티션 키</strong>라고도 부르는데, 데이터가 어떻게 분산될지 정ㅎ는 하나 이상의 컬럼으로 구성된다. <strong>샤딩 키</strong>는 데이터를 고르게 분할 할 수 있도록 하는게 가장 중요하다.</p>
<p>샤딩은 DB 규모를 확장시키는데 훌륭한 기술이지만 완벽하진 않다. 샤딩을 도입하면 시스템이 복잡해지고 풀어야 할 새로운 문제도 생긴다.</p>
<ul>
<li>데이터의 재 샤딩: 데이터가 너무 많아져서 하나의 샤드로는 감당이 안될때. 샤드 간 데이터 분포가 균등하지 못해서 특정 샤드에 할당된 공간 소모가 다른 샤드보다 빠르게 진행될 때(<strong>샤드 소진</strong>) 이런 문제가 생기면 <strong>샤드키 계산 방법을 다시 바꿔야한다.</strong> (안정 해시 기법을 활용하면 이 문제 해결 가능)</li>
<li>유명인사 문제: <strong>핫스팟 키</strong>라고도 부른다. 특정 샤드에 질의가 집중되어 서버에 과부하가 걸리는 문제다. 유명한 저스틴 비버, 레이디 가가 같은 유명인사가 모두 같은 샤드에 저장되어 있다고 가정하면, 이 샤드는 read 연산 때문에 과부하가 걸리게 될 것이다. 이 문제는 유명인사 각각에 샤드를 하나씩 할당해야 할 수도 있고, 심지어는 더 잘게 쪼개야 할 수도 있다.</li>
<li>조인과 비정규화: 1개의 데이터베이스를 여러 샤드 서버로 쪼개면, 여러 샤드에 걸친 데이터를 조인하기 힘들어진다. 이를 해결하는 한 가지 방법은 <strong>DB를 비정규화 하나의 테이블에서 질의 수행</strong>되도록 하는 것이다.</li>
</ul>
<h1 id="최종-구조">최종 구조</h1>
<p>사진은 데이터베이스에 대한 부하를 줄이기 위해 RDBMS가 아닌 NoSQL로 이전한 사진이다.</p>
<p><img src="https://velog.velcdn.com/images/dev_dc_hyeon/post/90bab2a7-2df1-48b5-867d-f79ab0c8f2ec/image.png" alt=""></p>
<p>트래픽이 몰릴 경우에 대비한 <strong>모든 계층의 다중화</strong>와 캐시를 활용한 <strong>캐시서버, CDN</strong>등을 활용한 구조이다.</p>
]]></description>
        </item>
    </channel>
</rss>